summaryrefslogtreecommitdiff
path: root/legacy
diff options
context:
space:
mode:
authorZach van Rijn <me@zv.io>2023-05-04 09:57:57 -0500
committerZach van Rijn <me@zv.io>2023-05-04 09:57:57 -0500
commitf9ba261c26e2bb56d28654a4fdf6180c6eb218df (patch)
treedd0ac975149ab2be0ac07ad49af3224662aeadf8 /legacy
parente61495d789e6dfbbef272095a63a026cf98e15e0 (diff)
downloadpackages-f9ba261c26e2bb56d28654a4fdf6180c6eb218df.tar.gz
packages-f9ba261c26e2bb56d28654a4fdf6180c6eb218df.tar.bz2
packages-f9ba261c26e2bb56d28654a4fdf6180c6eb218df.tar.xz
packages-f9ba261c26e2bb56d28654a4fdf6180c6eb218df.zip
{user => legacy}/mistserver: 32-bit headache. see #1008.
Diffstat (limited to 'legacy')
-rw-r--r--legacy/mistserver/APKBUILD72
-rw-r--r--legacy/mistserver/add-dtls-srtp-cmake-option.patch15
-rw-r--r--legacy/mistserver/fix-cmake-test-format.patch37
-rw-r--r--legacy/mistserver/gizahnl-mbedtls-dev-from-origin-master.patch13180
-rw-r--r--legacy/mistserver/link-execinfo.patch11
-rw-r--r--legacy/mistserver/mistserver.confd7
-rwxr-xr-xlegacy/mistserver/mistserver.initd29
-rw-r--r--legacy/mistserver/mistserver.pre-install7
8 files changed, 13358 insertions, 0 deletions
diff --git a/legacy/mistserver/APKBUILD b/legacy/mistserver/APKBUILD
new file mode 100644
index 000000000..093d88f4e
--- /dev/null
+++ b/legacy/mistserver/APKBUILD
@@ -0,0 +1,72 @@
+# Contributor: Síle Ekaterin Liszka <sheila@vulpine.house>
+# Maintainer: Síle Ekaterin Liszka <sheila@vulpine.house>
+pkgname=mistserver
+pkgver=3.1
+pkgrel=0
+pkgdesc="Multimedia streaming services"
+url="https://mistserver.org"
+arch="all"
+options=""
+license="Zlib AND AGPL-3.0-only"
+depends=""
+install="$pkgname.pre-install"
+makedepends="cmake libexecinfo-dev mbedtls-dev libsrtp-dev"
+pkgusers="mistserver"
+pkggroups="mistserver"
+subpackages="$pkgname-dev $pkgname-openrc"
+# ATTENTION MAINTAINERS: Generate a new aggregate patch from:
+# (assuming rebased on upstream correctly)
+# git clone https://github.com/gizahNL/mistserver.git
+# cd mistserver
+# git format-patch -M origin/master --stdout > gizahnl-mbedtls-dev-from-origin-master.patch
+source="mistserver-$pkgver.tar.gz::https://github.com/DDVTECH/mistserver/archive/refs/tags/$pkgver.tar.gz
+ mistserver.confd
+ mistserver.initd
+ link-execinfo.patch
+
+ add-dtls-srtp-cmake-option.patch
+ gizahnl-mbedtls-dev-from-origin-master.patch
+ fix-cmake-test-format.patch
+ "
+
+build() {
+ if [ "$CBUILD" != "$CHOST" ]; then
+ CMAKE_CROSSOPTS="-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_HOST_SYSTEM_NAME=Linux"
+ fi
+ cmake \
+ -DCMAKE_INSTALL_PREFIX=/usr \
+ -DCMAKE_INSTALL_LIBDIR=lib \
+ -DBUILD_SHARED_LIBS=True \
+ -DCMAKE_BUILD_TYPE=RelWithDebugInfo \
+ -DCMAKE_CXX_FLAGS="$CXXFLAGS -fPIC" \
+ -DCMAKE_C_FLAGS="$CFLAGS -fPIC" \
+ -DUSE_MBEDTLS_SSL_DTLS_SRTP=True \
+ ${CMAKE_CROSSOPTS} \
+ .
+ make -j1 # do not increase this (race conditions)
+}
+
+check() {
+ # FIXME!!!
+ #CTEST_OUTPUT_ON_FAILURE=TRUE ctest
+
+ # temporary sanity check
+ ./MistSession -v | grep Built
+}
+
+package() {
+ make DESTDIR="$pkgdir" install
+
+ # OpenRC
+
+ install -Dm755 "$srcdir"/mistserver.initd "$pkgdir"/etc/init.d/mistserver
+ install -Dm644 "$srcdir"/mistserver.confd "$pkgdir"/etc/conf.d/mistserver
+}
+
+sha512sums="efcac86cf031c5cc13dd274a4d63292122f1ef3d46faea0457e075898cda01bdea29f011699b595e07c8ed984886a33da2a04395a67698d6b2b405325f1b9715 mistserver-3.1.tar.gz
+7288adab6589f2facc1cb794057b1c5d9ec94e12e60d6afc8f6f25c54a8e908cc9841b83b5a6e608fa799fd6aa11767e92a963004182d45f7be9ccd3b65097e7 mistserver.confd
+e0c7df42f4d486983ece1ea50ab8f3006ebab5386881c14c4b2ff1246b6dd38ace935dc54f8f8a7687edb7ca5975b8c26abd6e99957b8c892862732263d49eb9 mistserver.initd
+a27bac965078f7eafb339ae7be9e50519d5728ae4f5d725905d5eecbb3fdf048df3e150cfa881be4bab754ca674a11271343156d5d97758d2ca65bef5bff55a6 link-execinfo.patch
+f90737722ac4a2ecff64386a9287ce0ddd48e7b176239f3de26725cadace52667ab44febe536738d8e0dba1fee2047e8f65caa8a2f282c7c6e9dbcc4f8daa23a add-dtls-srtp-cmake-option.patch
+48e835a09b8096f78e94f25429768debf65ab9f4b3152c45b69eb072ac5d3abc0036daae894e6b85c57ad7ae7993bf2940c730fbb02dcafecd2ed9716b86dfb9 gizahnl-mbedtls-dev-from-origin-master.patch
+de8bc5279426c7fab58074a4e73001c590dd386c0d7cd1747e6fff74278aa901662e7720bf97ea645eb28fc1a6c3e4830afdcaf5e4a770c996c639936dd3e28b fix-cmake-test-format.patch"
diff --git a/legacy/mistserver/add-dtls-srtp-cmake-option.patch b/legacy/mistserver/add-dtls-srtp-cmake-option.patch
new file mode 100644
index 000000000..8704b5a08
--- /dev/null
+++ b/legacy/mistserver/add-dtls-srtp-cmake-option.patch
@@ -0,0 +1,15 @@
+diff --git a/CMakeLists.txt b/CMakeLists.txt
+index 6af16808..a12aa8cf 100644
+--- a/CMakeLists.txt
++++ b/CMakeLists.txt
+@@ -91,6 +91,10 @@ else()
+ message("SSL/TLS support is turned OFF")
+ endif()
+
++if (USE_MBEDTLS_SSL_DTLS_SRTP)
++ add_definitions(-DHAVE_UPSTREAM_MBEDTLS_SRTP=1)
++endif()
++
+ if (DEFINED DATASIZE )
+ add_definitions(-DSHM_DATASIZE=${DATASIZE})
+ endif()
diff --git a/legacy/mistserver/fix-cmake-test-format.patch b/legacy/mistserver/fix-cmake-test-format.patch
new file mode 100644
index 000000000..07161a2c5
--- /dev/null
+++ b/legacy/mistserver/fix-cmake-test-format.patch
@@ -0,0 +1,37 @@
+I don't know if this is correct.
+
+https://gitlab.kitware.com/cmake/cmake/-/issues/19109
+
+--- a/CMakeLists.txt
++++ b/CMakeLists.txt
+@@ -931,24 +931,24 @@
+ ########################################
+ add_executable(urltest test/url.cpp ${BINARY_DIR}/mist/.headers)
+ target_link_libraries(urltest mist)
+-add_test(URLTest COMMAND urltest)
++add_test(NAME URLTest COMMAND urltest)
+ add_executable(logtest test/log.cpp ${BINARY_DIR}/mist/.headers)
+ target_link_libraries(logtest mist)
+-add_test(LOGTest COMMAND logtest)
++add_test(NAME LOGTest COMMAND logtest)
+ add_executable(downloadertest test/downloader.cpp ${BINARY_DIR}/mist/.headers)
+ target_link_libraries(downloadertest mist)
+-add_test(DownloaderTest COMMAND downloadertest)
++add_test(NAME DownloaderTest COMMAND downloadertest)
+ add_executable(urireadertest test/urireader.cpp ${BINARY_DIR}/mist/.headers)
+ target_link_libraries(urireadertest mist)
+-add_test(URIReaderTest COMMAND urireadertest)
++add_test(NAME URIReaderTest COMMAND urireadertest)
+ add_executable(jsontest test/json.cpp ${BINARY_DIR}/mist/.headers)
+ target_link_libraries(jsontest mist)
+-add_test(JSONTest COMMAND jsontest)
++add_test(NAME JSONTest COMMAND jsontest)
+ add_executable(resolvetest test/resolve.cpp ${BINARY_DIR}/mist/.headers)
+ target_link_libraries(resolvetest mist)
+ add_executable(bitwritertest test/bitwriter.cpp ${BINARY_DIR}/mist/.headers)
+ target_link_libraries(bitwritertest mist)
+-add_test(BitWriterTest COMMAND bitwritertest)
++add_test(NAME BitWriterTest COMMAND bitwritertest)
+ add_executable(streamstatustest test/status.cpp ${BINARY_DIR}/mist/.headers)
+ target_link_libraries(streamstatustest mist)
+ add_executable(websockettest test/websocket.cpp ${BINARY_DIR}/mist/.headers)
diff --git a/legacy/mistserver/gizahnl-mbedtls-dev-from-origin-master.patch b/legacy/mistserver/gizahnl-mbedtls-dev-from-origin-master.patch
new file mode 100644
index 000000000..29a6914e2
--- /dev/null
+++ b/legacy/mistserver/gizahnl-mbedtls-dev-from-origin-master.patch
@@ -0,0 +1,13180 @@
+From 0430a644dfcd0fcc0ea6494bdb8d431073b8361d Mon Sep 17 00:00:00 2001
+From: Matthew James <matt@Matthews-MacBook-Pro.local>
+Date: Fri, 22 Jul 2022 00:44:28 +0800
+Subject: [PATCH 01/38] Enable Parameters on TSSRT listener
+
+---
+ lib/socket_srt.cpp | 7 +++----
+ lib/socket_srt.h | 2 +-
+ src/input/input_tssrt.cpp | 4 +++-
+ src/output/output_tssrt.cpp | 3 ++-
+ 4 files changed, 9 insertions(+), 7 deletions(-)
+
+diff --git a/lib/socket_srt.cpp b/lib/socket_srt.cpp
+index a01117db..3e03bce4 100644
+--- a/lib/socket_srt.cpp
++++ b/lib/socket_srt.cpp
+@@ -462,12 +462,11 @@ namespace Socket{
+
+ SRTServer::SRTServer(int fromSock){conn = SRTConnection(fromSock);}
+
+- SRTServer::SRTServer(int port, std::string hostname, bool nonblock, const std::string &_direction){
++ SRTServer::SRTServer(int port, std::string hostname, std::map<std::string, std::string> _params, bool nonblock, const std::string &_direction){
+ // We always create a server as listening
+- std::map<std::string, std::string> listenMode;
+- listenMode["mode"] = "listener";
++ _params["mode"] = "listener";
+ if (hostname == ""){hostname = "0.0.0.0";}
+- conn.connect(hostname, port, _direction, listenMode);
++ conn.connect(hostname, port, _direction, _params);
+ conn.setBlocking(true);
+ if (!conn){
+ ERROR_MSG("Unable to create socket");
+diff --git a/lib/socket_srt.h b/lib/socket_srt.h
+index 5101d38d..07e947f8 100644
+--- a/lib/socket_srt.h
++++ b/lib/socket_srt.h
+@@ -97,7 +97,7 @@ namespace Socket{
+ public:
+ SRTServer();
+ SRTServer(int existingSock);
+- SRTServer(int port, std::string hostname, bool nonblock = false, const std::string &_direction = "input");
++ SRTServer(int port, std::string hostname, std::map<std::string, std::string> params, bool nonblock = false, const std::string &_direction = "input");
+ SRTConnection accept(bool nonblock = false, const std::string &direction = "input");
+ void setBlocking(bool blocking);
+ bool connected() const;
+diff --git a/src/input/input_tssrt.cpp b/src/input/input_tssrt.cpp
+index 4a7adcef..d12f9cba 100644
+--- a/src/input/input_tssrt.cpp
++++ b/src/input/input_tssrt.cpp
+@@ -137,7 +137,9 @@ namespace Mist{
+ HTTP::URL u(source);
+ INFO_MSG("Parsed url: %s", u.getUrl().c_str());
+ if (Socket::interpretSRTMode(u) == "listener"){
+- sSock = Socket::SRTServer(u.getPort(), u.host, false);
++ std::map<std::string, std::string> arguments;
++ HTTP::parseVars(u.args, arguments);
++ sSock = Socket::SRTServer(u.getPort(), u.host, arguments, false);
+ struct sigaction new_action;
+ struct sigaction cur_action;
+ new_action.sa_sigaction = signal_handler;
+diff --git a/src/output/output_tssrt.cpp b/src/output/output_tssrt.cpp
+index aa06189e..fb07b11e 100644
+--- a/src/output/output_tssrt.cpp
++++ b/src/output/output_tssrt.cpp
+@@ -434,7 +434,8 @@ int main(int argc, char *argv[]){
+ sigaction(SIGUSR1, &new_action, NULL);
+ }
+ if (conf.getInteger("port") && conf.getString("interface").size()){
+- server_socket = Socket::SRTServer(conf.getInteger("port"), conf.getString("interface"), false, "output");
++ std::map<std::string, std::string> arguments;
++ server_socket = Socket::SRTServer(conf.getInteger("port"), conf.getString("interface"), arguments, false, "output");
+ }
+ if (!server_socket.connected()){
+ DEVEL_MSG("Failure to open socket");
+--
+2.25.1
+
+
+From 508506c241b2980d1aed607cd269ea60e5265e41 Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Tue, 5 Jan 2021 15:14:04 +0100
+Subject: [PATCH 02/38] Fixes to UDP socket behaviour
+
+---
+ lib/socket.cpp | 25 ++++++++++++++++++++-----
+ 1 file changed, 20 insertions(+), 5 deletions(-)
+
+diff --git a/lib/socket.cpp b/lib/socket.cpp
+index 22f5e32b..865c3be7 100644
+--- a/lib/socket.cpp
++++ b/lib/socket.cpp
+@@ -1713,15 +1713,19 @@ void Socket::UDPConnection::SetDestination(std::string destIp, uint32_t port){
+ memset(&hints, 0, sizeof(struct addrinfo));
+ hints.ai_family = family;
+ hints.ai_socktype = SOCK_DGRAM;
+- hints.ai_flags = AI_ADDRCONFIG | AI_V4MAPPED;
++ hints.ai_flags = AI_ADDRCONFIG | AI_ALL;
+ hints.ai_protocol = IPPROTO_UDP;
+ hints.ai_canonname = NULL;
+ hints.ai_addr = NULL;
+ hints.ai_next = NULL;
+ int s = getaddrinfo(destIp.c_str(), ss.str().c_str(), &hints, &result);
+ if (s != 0){
+- FAIL_MSG("Could not connect UDP socket to %s:%i! Error: %s", destIp.c_str(), port, gai_strmagic(s));
+- return;
++ hints.ai_family = AF_UNSPEC;
++ s = getaddrinfo(destIp.c_str(), ss.str().c_str(), &hints, &result);
++ if (s != 0){
++ FAIL_MSG("Could not connect UDP socket to %s:%i! Error: %s", destIp.c_str(), port, gai_strmagic(s));
++ return;
++ }
+ }
+
+ for (rp = result; rp != NULL; rp = rp->ai_next){
+@@ -1745,7 +1749,12 @@ void Socket::UDPConnection::SetDestination(std::string destIp, uint32_t port){
+ bind(boundPort, boundAddr, boundMulti);
+ }
+ }
+- HIGH_MSG("Set UDP destination: %s:%d (%s)", destIp.c_str(), port, addrFam(family));
++ {
++ std::string trueDest;
++ uint32_t truePort;
++ GetDestination(trueDest, truePort);
++ HIGH_MSG("Set UDP destination: %s:%d => %s:%d (%s)", destIp.c_str(), port, trueDest.c_str(), truePort, addrFam(family));
++ }
+ freeaddrinfo(result);
+ return;
+ //\todo Possibly detect and handle failure
+@@ -1881,6 +1890,12 @@ uint16_t Socket::UDPConnection::bind(int port, std::string iface, const std::str
+ for (rp = addr_result; rp != NULL; rp = rp->ai_next){
+ sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+ if (sock == -1){continue;}
++ if (rp->ai_family == AF_INET6){
++ const int optval = 0;
++ if (setsockopt(sock, SOL_SOCKET, IPV6_V6ONLY, &optval, sizeof(optval)) < 0){
++ WARN_MSG("Could not set IPv6 UDP socket to be dual-stack! %s", strerror(errno));
++ }
++ }
+ checkRecvBuf();
+ char human_addr[INET6_ADDRSTRLEN];
+ char human_port[16];
+@@ -1909,7 +1924,7 @@ uint16_t Socket::UDPConnection::bind(int port, std::string iface, const std::str
+ if (multicast){
+ const int optval = 1;
+ if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0){
+- WARN_MSG("Could not set multicast UDP socket re-use!");
++ WARN_MSG("Could not set multicast UDP socket re-use! %s", strerror(errno));
+ }
+ }
+ if (::bind(sock, rp->ai_addr, rp->ai_addrlen) == 0){
+--
+2.25.1
+
+
+From ed9910d587b5504d38e4f7ef2777f2ea7bd9b760 Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Wed, 3 Aug 2022 14:58:05 +0200
+Subject: [PATCH 03/38] Fix for list limit in HLS/CMAF outputs when using
+ non-live
+
+---
+ src/output/output_cmaf.cpp | 2 +-
+ src/output/output_hls.cpp | 2 +-
+ 2 files changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/src/output/output_cmaf.cpp b/src/output/output_cmaf.cpp
+index 756fceb8..4582a8b6 100644
+--- a/src/output/output_cmaf.cpp
++++ b/src/output/output_cmaf.cpp
+@@ -284,7 +284,7 @@ namespace Mist{
+ requestTid,
+ M.biggestFragment(timingTid) / 1000,
+ atol(H.GetVar("iMsn").c_str()),
+- config->getInteger("listlimit"),
++ M.getLive()? config->getInteger("listlimit") : 0,
+ urlPrefix,
+ systemBoot,
+ bootMsOffset,
+diff --git a/src/output/output_hls.cpp b/src/output/output_hls.cpp
+index 93410f54..19d083e6 100644
+--- a/src/output/output_hls.cpp
++++ b/src/output/output_hls.cpp
+@@ -173,7 +173,7 @@ namespace Mist{
+ requestTid,
+ M.biggestFragment(timingTid) / 1000,
+ atol(H.GetVar("iMsn").c_str()),
+- config->getInteger("listlimit"),
++ M.getLive()? config->getInteger("listlimit") : 0,
+ urlPrefix,
+ systemBoot,
+ bootMsOffset,
+--
+2.25.1
+
+
+From c37aac8898484903da8c2095d29125bf69888c0b Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Thu, 4 Aug 2022 09:14:51 +0200
+Subject: [PATCH 04/38] Remove "Rate=..." debug message from RTMP push output
+
+---
+ src/output/output_rtmp.cpp | 1 -
+ 1 file changed, 1 deletion(-)
+
+diff --git a/src/output/output_rtmp.cpp b/src/output/output_rtmp.cpp
+index 69d5f91e..16a400d0 100644
+--- a/src/output/output_rtmp.cpp
++++ b/src/output/output_rtmp.cpp
+@@ -618,7 +618,6 @@ namespace Mist{
+
+ if (type == "audio"){
+ uint32_t rate = M.getRate(thisIdx);
+- WARN_MSG("Rate=%i", rate);
+ rtmpheader[7] = 0x08;
+ if (codec == "AAC"){
+ dataheader[0] += 0xA0;
+--
+2.25.1
+
+
+From 4084768a3eb72026e901118e05fd17115dd785b3 Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Tue, 5 Jul 2022 15:21:04 +0200
+Subject: [PATCH 05/38] Improved incoming push accept timing
+
+---
+ src/output/output.cpp | 7 ++++---
+ 1 file changed, 4 insertions(+), 3 deletions(-)
+
+diff --git a/src/output/output.cpp b/src/output/output.cpp
+index 5c785bb0..1fa86eee 100644
+--- a/src/output/output.cpp
++++ b/src/output/output.cpp
+@@ -2050,7 +2050,7 @@ namespace Mist{
+ INFO_MSG("Waiting for stream reset before attempting push input accept (%" PRIu64 " <= %" PRIu64 "+500)", twoTime, oneTime);
+ while (streamStatus != STRMSTAT_OFF && keepGoing()){
+ userSelect.clear();
+- Util::wait(1000);
++ Util::wait(250);
+ streamStatus = Util::getStreamStatus(streamName);
+ }
+ reconnect();
+@@ -2059,14 +2059,15 @@ namespace Mist{
+ while (((streamStatus != STRMSTAT_WAIT && streamStatus != STRMSTAT_READY) || !meta) && keepGoing()){
+ INFO_MSG("Waiting for %s buffer to be ready... (%u)", streamName.c_str(), streamStatus);
+ disconnect();
+- Util::wait(1000);
+ streamStatus = Util::getStreamStatus(streamName);
+ if (streamStatus == STRMSTAT_OFF || streamStatus == STRMSTAT_WAIT || streamStatus == STRMSTAT_READY){
+ INFO_MSG("Reconnecting to %s buffer... (%u)", streamName.c_str(), streamStatus);
+- Util::wait(500);
+ reconnect();
+ streamStatus = Util::getStreamStatus(streamName);
+ }
++ if (((streamStatus != STRMSTAT_WAIT && streamStatus != STRMSTAT_READY) || !meta) && keepGoing()){
++ Util::wait(100);
++ }
+ }
+ if (streamStatus == STRMSTAT_READY || streamStatus == STRMSTAT_WAIT){reconnect();}
+ if (!meta){
+--
+2.25.1
+
+
+From 14bc94ece51affb7cb6d52a491b97921ed420587 Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Tue, 12 Jul 2022 15:47:29 +0200
+Subject: [PATCH 06/38] Prevent eternal sleep when waiting for RelAccX
+ structure to become ready
+
+---
+ lib/util.cpp | 10 +++++++++-
+ 1 file changed, 9 insertions(+), 1 deletion(-)
+
+diff --git a/lib/util.cpp b/lib/util.cpp
+index d522a151..8dd75727 100644
+--- a/lib/util.cpp
++++ b/lib/util.cpp
+@@ -446,7 +446,15 @@ namespace Util{
+ hdrOffset = (uint16_t*)(p+26);
+ hdrEndPos = (uint64_t*)(p+28);
+ if (waitReady){
+- while (!isReady()){Util::sleep(50);}
++ uint64_t maxWait = Util::bootMS() + 10000;
++ while (!isReady()){
++ if (Util::bootMS() > maxWait){
++ FAIL_MSG("Waiting for RelAccX structure to be ready timed out, aborting");
++ p = 0;
++ return;
++ }
++ Util::sleep(50);
++ }
+ }
+ if (isReady()){
+ uint16_t offset = RAXHDR_FIELDOFFSET;
+--
+2.25.1
+
+
+From 5bdd4a416e964ae0cfcc56d99c062691d1b3931d Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Thu, 14 Jul 2022 10:45:53 +0200
+Subject: [PATCH 07/38] Fix load balancer CPU usage
+
+---
+ src/input/input_balancer.cpp | 3 +++
+ 1 file changed, 3 insertions(+)
+
+diff --git a/src/input/input_balancer.cpp b/src/input/input_balancer.cpp
+index 448e6eff..36db5709 100644
+--- a/src/input/input_balancer.cpp
++++ b/src/input/input_balancer.cpp
+@@ -140,6 +140,9 @@ namespace Mist{
+ startTime = 0; // note success
+ break; // break out of while loop
+ }
++ }else{
++ //Prevent 100% CPU usage if the response is slow
++ Util::sleep(25);
+ }
+ }
+ if (startTime){
+--
+2.25.1
+
+
+From fffe98804cb9c126ec91b7a7b67f2c917561ce02 Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Thu, 30 Jun 2022 14:04:34 +0200
+Subject: [PATCH 08/38] Fixed TS SRT input not closing the connection when
+ stopping for internal reasons rather than external reasons
+
+---
+ src/input/input_tssrt.cpp | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/src/input/input_tssrt.cpp b/src/input/input_tssrt.cpp
+index d12f9cba..6cd614e7 100644
+--- a/src/input/input_tssrt.cpp
++++ b/src/input/input_tssrt.cpp
+@@ -241,6 +241,7 @@ namespace Mist{
+ }
+ // If we are here: we have a proper connection (either accepted or pull input) and should start parsing it as such
+ Input::streamMainLoop();
++ srtConn.close();
+ }
+
+ bool inputTSSRT::needsLock(){return false;}
+--
+2.25.1
+
+
+From 267a74f0f6d44b928b99312b356a3080579fdc1c Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Thu, 4 Aug 2022 16:43:04 +0200
+Subject: [PATCH 09/38] Fix track selector logic when multiple selections are
+ considered
+
+---
+ lib/stream.cpp | 33 +++++++++++++++++++++++++++++++--
+ 1 file changed, 31 insertions(+), 2 deletions(-)
+
+diff --git a/lib/stream.cpp b/lib/stream.cpp
+index 6036a8fd..715015d6 100644
+--- a/lib/stream.cpp
++++ b/lib/stream.cpp
+@@ -1249,7 +1249,7 @@ std::set<size_t> Util::wouldSelect(const DTSC::Meta &M, const std::map<std::stri
+ std::set<size_t> result;
+
+ /*LTS-START*/
+- bool noSelAudio = false, noSelVideo = false, noSelSub = false;
++ bool noSelAudio = false, noSelVideo = false, noSelSub = false, noSelMeta = false;
+ // Then, select the tracks we've been asked to select.
+ if (targetParams.count("audio") && targetParams.at("audio").size()){
+ if (targetParams.at("audio") != "-1" && targetParams.at("audio") != "none"){
+@@ -1270,6 +1270,11 @@ std::set<size_t> Util::wouldSelect(const DTSC::Meta &M, const std::map<std::stri
+ result.insert(tracks.begin(), tracks.end());
+ noSelSub = true;
+ }
++ if (targetParams.count("meta") && targetParams.at("meta").size()){
++ std::set<size_t> tracks = Util::findTracks(M, capa, "meta", targetParams.at("meta"), UA);
++ result.insert(tracks.begin(), tracks.end());
++ noSelMeta = true;
++ }
+ /*LTS-END*/
+
+ std::set<size_t> validTracks = M.getValidTracks();
+@@ -1291,6 +1296,7 @@ std::set<size_t> Util::wouldSelect(const DTSC::Meta &M, const std::map<std::stri
+ // loop through all codec combinations, count max simultaneous active
+ unsigned int bestSoFar = 0;
+ unsigned int bestSoFarCount = 0;
++ unsigned int bestSoFarCountExtra = 0;
+ unsigned int index = 0;
+ bool allowBFrames = true;
+ if (capa.isMember("methods")){
+@@ -1319,6 +1325,7 @@ std::set<size_t> Util::wouldSelect(const DTSC::Meta &M, const std::map<std::stri
+ if (problems){continue;}
+ if (noSelAudio && M.getType(*trit) == "audio"){continue;}
+ if (noSelVideo && M.getType(*trit) == "video"){continue;}
++ if (noSelMeta && M.getType(*trit) == "meta"){continue;}
+ if (noSelSub && (M.getType(*trit) == "subtitle" || M.getCodec(*trit) == "subtitle")){continue;}
+ result.insert(*trit);
+ }
+@@ -1328,6 +1335,7 @@ std::set<size_t> Util::wouldSelect(const DTSC::Meta &M, const std::map<std::stri
+
+ jsonForEachConst(capa["codecs"], it){
+ unsigned int selCounter = 0;
++ unsigned int extraCounter = 0;
+ if ((*it).size() > 0){
+ jsonForEachConst((*it), itb){
+ if ((*itb).size() > 0){
+@@ -1360,6 +1368,26 @@ std::set<size_t> Util::wouldSelect(const DTSC::Meta &M, const std::map<std::stri
+ }
+ if (problems){break;}
+ selCounter++;
++ extraCounter++;
++ if (!multiSel){break;}
++ }
++ }
++ for (std::set<size_t>::iterator itd = validTracks.begin(); itd != validTracks.end(); itd++){
++ if ((!byType && M.getCodec(*itd) == strRef.substr(shift)) ||
++ (byType && M.getType(*itd) == strRef.substr(shift)) || strRef.substr(shift) == "*"){
++ // user-agent-check
++ bool problems = false;
++ if (capa.isMember("exceptions") && capa["exceptions"].isObject() &&
++ capa["exceptions"].size()){
++ jsonForEachConst(capa["exceptions"], ex){
++ if (ex.key() == "codec:" + strRef.substr(shift)){
++ problems = !Util::checkException(*ex, UA);
++ break;
++ }
++ }
++ }
++ if (problems){break;}
++ extraCounter++;
+ if (!multiSel){break;}
+ }
+ }
+@@ -1367,8 +1395,9 @@ std::set<size_t> Util::wouldSelect(const DTSC::Meta &M, const std::map<std::stri
+ }
+ }
+ if (selCounter == result.size()){
+- if (selCounter > bestSoFarCount){
++ if (selCounter > bestSoFarCount || (selCounter == bestSoFarCount && extraCounter > bestSoFarCountExtra)){
+ bestSoFarCount = selCounter;
++ bestSoFarCountExtra = extraCounter;
+ bestSoFar = index;
+ HIGH_MSG("Matched %u: %s", selCounter, (*it).toString().c_str());
+ }
+--
+2.25.1
+
+
+From 3734c90544a68d03a1a69438f00f215c517fec01 Mon Sep 17 00:00:00 2001
+From: Phencys <phencys@phencys.com>
+Date: Sun, 20 Sep 2020 20:31:17 +0200
+Subject: [PATCH 10/38] Added support for raw passthrough of MPEG2-TS data
+
+---
+ src/input/input_ts.cpp | 73 ++++++++++++++++++++++++++++++++---
+ src/input/input_ts.h | 5 +++
+ src/input/input_tsrist.cpp | 54 +++++++++++++++++++++++++-
+ src/input/input_tsrist.h | 6 +++
+ src/input/input_tssrt.cpp | 35 +++++++++++++++++
+ src/input/input_tssrt.h | 4 ++
+ src/io.cpp | 7 ++++
+ src/io.h | 1 +
+ src/output/output_httpts.cpp | 1 +
+ src/output/output_ts.cpp | 1 +
+ src/output/output_ts_base.cpp | 5 +++
+ src/output/output_tsrist.cpp | 1 +
+ src/output/output_tssrt.cpp | 1 +
+ 13 files changed, 187 insertions(+), 7 deletions(-)
+
+diff --git a/src/input/input_ts.cpp b/src/input/input_ts.cpp
+index 98300411..cfe3f7cd 100644
+--- a/src/input/input_ts.cpp
++++ b/src/input/input_ts.cpp
+@@ -134,6 +134,13 @@ void parseThread(void *mistIn){
+ }
+ }
+ }
++
++ //On shutdown, make sure to clean up stream buffer
++ if (idx != INVALID_TRACK_ID){
++ tthread::lock_guard<tthread::mutex> guard(threadClaimMutex);
++ input->liveFinalize(idx);
++ }
++
+ std::string reason = "unknown reason";
+ if (!(Util::bootSecs() - threadTimer[tid] < THREAD_TIMEOUT)){reason = "thread timeout";}
+ if (!cfgPointer->is_active){reason = "input shutting down";}
+@@ -155,6 +162,9 @@ namespace Mist{
+ /// Constructor of TS Input
+ /// \arg cfg Util::Config that contains all current configurations.
+ inputTS::inputTS(Util::Config *cfg) : Input(cfg){
++ rawMode = false;
++ rawIdx = INVALID_TRACK_ID;
++ lastRawPacket = 0;
+ capa["name"] = "TS";
+ capa["desc"] =
+ "This input allows you to stream MPEG2-TS data from static files (/*.ts), streamed files "
+@@ -188,6 +198,7 @@ namespace Mist{
+ capa["codecs"][0u][1u].append("AC3");
+ capa["codecs"][0u][1u].append("MP2");
+ capa["codecs"][0u][1u].append("opus");
++ capa["codecs"][1u][0u].append("rawts");
+ inFile = NULL;
+ inputProcess = 0;
+ isFinished = false;
+@@ -232,6 +243,16 @@ namespace Mist{
+ "Alternative stream to load for playback when there is no active broadcast";
+ capa["optional"]["fallback_stream"]["type"] = "str";
+ capa["optional"]["fallback_stream"]["default"] = "";
++
++ capa["optional"]["raw"]["name"] = "Raw input mode";
++ capa["optional"]["raw"]["help"] = "Enable raw MPEG-TS passthrough mode";
++ capa["optional"]["raw"]["option"] = "--raw";
++
++ JSON::Value option;
++ option["long"] = "raw";
++ option["short"] = "R";
++ option["help"] = "Enable raw MPEG-TS passthrough mode";
++ config->addOption("raw", option);
+ }
+
+ inputTS::~inputTS(){
+@@ -257,6 +278,10 @@ namespace Mist{
+ /// Live Setup of TS Input
+ bool inputTS::preRun(){
+ INFO_MSG("Prerun: %s", config->getString("input").c_str());
++
++ rawMode = config->getBool("raw");
++ if (rawMode){INFO_MSG("Entering raw mode");}
++
+ // streamed standard input
+ if (config->getString("input") == "-"){
+ standAlone = false;
+@@ -520,9 +545,28 @@ namespace Mist{
+ }
+ if (tcpCon.Received().available(188) && tcpCon.Received().get()[0] == 0x47){
+ std::string newData = tcpCon.Received().remove(188);
+- tsBuf.FromPointer(newData.data());
+- liveStream.add(tsBuf);
+- if (!liveStream.isDataTrack(tsBuf.getPID())){liveStream.parse(tsBuf.getPID());}
++ if (rawMode){
++ keepAlive();
++ rawBuffer.append(newData);
++ if (rawBuffer.size() >= 1316 && (lastRawPacket == 0 || lastRawPacket != Util::bootMS())){
++ if (rawIdx == INVALID_TRACK_ID){
++ rawIdx = meta.addTrack();
++ meta.setType(rawIdx, "meta");
++ meta.setCodec(rawIdx, "rawts");
++ meta.setID(rawIdx, 1);
++ userSelect[rawIdx].reload(streamName, rawIdx, COMM_STATUS_SOURCE);
++ }
++ uint64_t packetTime = Util::bootMS();
++ thisPacket.genericFill(packetTime, 0, 1, rawBuffer, rawBuffer.size(), 0, 0);
++ bufferLivePacket(thisPacket);
++ lastRawPacket = packetTime;
++ rawBuffer.truncate(0);
++ }
++ }else {
++ tsBuf.FromPointer(newData.data());
++ liveStream.add(tsBuf);
++ if (!liveStream.isDataTrack(tsBuf.getPID())){liveStream.parse(tsBuf.getPID());}
++ }
+ }
+ }
+ noDataSince = Util::bootSecs();
+@@ -543,7 +587,26 @@ namespace Mist{
+ gettingData = true;
+ INFO_MSG("Now receiving UDP data...");
+ }
+- assembler.assemble(liveStream, udpCon.data, udpCon.data.size());
++ if (rawMode){
++ keepAlive();
++ rawBuffer.append(udpCon.data, udpCon.data.size());
++ if (rawBuffer.size() >= 1316 && (lastRawPacket == 0 || lastRawPacket != Util::bootMS())){
++ if (rawIdx == INVALID_TRACK_ID){
++ rawIdx = meta.addTrack();
++ meta.setType(rawIdx, "meta");
++ meta.setCodec(rawIdx, "rawts");
++ meta.setID(rawIdx, 1);
++ userSelect[rawIdx].reload(streamName, rawIdx, COMM_STATUS_SOURCE);
++ }
++ uint64_t packetTime = Util::bootMS();
++ thisPacket.genericFill(packetTime, 0, 1, rawBuffer, rawBuffer.size(), 0, 0);
++ bufferLivePacket(thisPacket);
++ lastRawPacket = packetTime;
++ rawBuffer.truncate(0);
++ }
++ }else{
++ assembler.assemble(liveStream, udpCon.data, udpCon.data.size());
++ }
+ }
+ if (!received){
+ Util::sleep(100);
+@@ -578,7 +641,7 @@ namespace Mist{
+ }
+
+ std::set<size_t> activeTracks = liveStream.getActiveTracks();
+- {
++ if (!rawMode){
+ tthread::lock_guard<tthread::mutex> guard(threadClaimMutex);
+ if (hasStarted && !threadTimer.size()){
+ if (!isAlwaysOn()){
+diff --git a/src/input/input_ts.h b/src/input/input_ts.h
+index e810ddb4..3116725d 100644
+--- a/src/input/input_ts.h
++++ b/src/input/input_ts.h
+@@ -41,6 +41,11 @@ namespace Mist{
+ pid_t inputProcess;
+ size_t tmpIdx;
+ bool isFinished;
++
++ bool rawMode;
++ Util::ResizeablePointer rawBuffer;
++ size_t rawIdx;
++ uint64_t lastRawPacket;
+ };
+ }// namespace Mist
+
+diff --git a/src/input/input_tsrist.cpp b/src/input/input_tsrist.cpp
+index 18b05c9d..5b95aa9f 100644
+--- a/src/input/input_tsrist.cpp
++++ b/src/input/input_tsrist.cpp
+@@ -66,6 +66,10 @@ namespace Mist{
+ /// Constructor of TS Input
+ /// \arg cfg Util::Config that contains all current configurations.
+ inputTSRIST::inputTSRIST(Util::Config *cfg) : Input(cfg){
++ rawMode = false;
++ rawIdx = INVALID_TRACK_ID;
++ lastRawPacket = 0;
++ hasRaw = false;
+ connPtr = this;
+ cnfPtr = config;
+
+@@ -96,6 +100,7 @@ namespace Mist{
+ capa["codecs"][0u][1u].append("AC3");
+ capa["codecs"][0u][1u].append("MP2");
+ capa["codecs"][0u][1u].append("opus");
++ capa["codecs"][1u][0u].append("rawts");
+
+ JSON::Value option;
+ option["arg"] = "integer";
+@@ -132,6 +137,15 @@ namespace Mist{
+ capa["optional"]["profile"]["type"] = "select";
+ capa["optional"]["profile"]["option"] = "--profile";
+
++ capa["optional"]["raw"]["name"] = "Raw input mode";
++ capa["optional"]["raw"]["help"] = "Enable raw MPEG-TS passthrough mode";
++ capa["optional"]["raw"]["option"] = "--raw";
++
++ option["long"] = "raw";
++ option["short"] = "R";
++ option["help"] = "Enable raw MPEG-TS passthrough mode";
++ config->addOption("raw", option);
++
+ lastTimeStamp = 0;
+ timeStampOffset = 0;
+ receiver_ctx = 0;
+@@ -146,6 +160,9 @@ namespace Mist{
+
+ /// Live Setup of SRT Input. Runs only if we are the "main" thread
+ bool inputTSRIST::preRun(){
++ rawMode = config->getBool("raw");
++ if (rawMode){INFO_MSG("Entering raw mode");}
++
+ std::string source = config->getString("input");
+ standAlone = false;
+ HTTP::URL u(source);
+@@ -161,6 +178,20 @@ namespace Mist{
+ // Retrieve the next packet to be played from the srt connection.
+ void inputTSRIST::getNext(size_t idx){
+ thisPacket.null();
++ if (rawMode){
++ //Set to false so the other thread knows its safe to fill
++ hasRaw = false;
++ while (!hasRaw && config->is_active){
++ Util::sleep(50);
++ if (!bufferActive()){
++ Util::logExitReason("Buffer shut down");
++ return;
++ }
++ }
++ //if hasRaw, thisPacket has been filled by the other thread
++ return;
++ }
++
+ while (!thisPacket && config->is_active){
+ if (tsStream.hasPacket()){
+ tsStream.getEarliestPacket(thisPacket);
+@@ -228,8 +259,27 @@ namespace Mist{
+ }
+
+ void inputTSRIST::addData(const char * ptr, size_t len){
+- for (size_t o = 0; o <= len-188; o += 188){
+- tsStream.parse((char*)ptr+o, 0);
++ for (size_t o = 0; o+188 <= len; o += 188){
++ if (rawMode){
++ rawBuffer.append(ptr+o, 188);
++ if (!hasRaw && rawBuffer.size() >= 1316 && (lastRawPacket == 0 || lastRawPacket != Util::bootMS())){
++ if (rawIdx == INVALID_TRACK_ID){
++ rawIdx = meta.addTrack();
++ meta.setType(rawIdx, "meta");
++ meta.setCodec(rawIdx, "rawts");
++ meta.setID(rawIdx, 1);
++ userSelect[rawIdx].reload(streamName, rawIdx, COMM_STATUS_SOURCE);
++ }
++ thisTime = Util::bootMS();
++ thisIdx = rawIdx;
++ thisPacket.genericFill(thisTime, 0, 1, rawBuffer, rawBuffer.size(), 0, 0);
++ lastRawPacket = thisTime;
++ rawBuffer.truncate(0);
++ hasRaw = true;
++ }
++ }else{
++ tsStream.parse((char*)ptr+o, 0);
++ }
+ }
+ }
+
+diff --git a/src/input/input_tsrist.h b/src/input/input_tsrist.h
+index 3bc29e1c..731f9b04 100644
+--- a/src/input/input_tsrist.h
++++ b/src/input/input_tsrist.h
+@@ -33,6 +33,12 @@ namespace Mist{
+ virtual void connStats(Comms::Statistics &statComm);
+
+ struct rist_ctx *receiver_ctx;
++
++ bool rawMode;
++ Util::ResizeablePointer rawBuffer;
++ size_t rawIdx;
++ uint64_t lastRawPacket;
++ bool hasRaw;
+ };
+ }// namespace Mist
+
+diff --git a/src/input/input_tssrt.cpp b/src/input/input_tssrt.cpp
+index 6cd614e7..da69be29 100644
+--- a/src/input/input_tssrt.cpp
++++ b/src/input/input_tssrt.cpp
+@@ -25,6 +25,7 @@
+ Util::Config *cfgPointer = NULL;
+ std::string baseStreamName;
+ Socket::SRTServer sSock;
++bool rawMode = false;
+
+ void (*oldSignal)(int, siginfo_t *,void *) = 0;
+
+@@ -49,6 +50,8 @@ namespace Mist{
+ /// Constructor of TS Input
+ /// \arg cfg Util::Config that contains all current configurations.
+ inputTSSRT::inputTSSRT(Util::Config *cfg, SRTSOCKET s) : Input(cfg){
++ rawIdx = INVALID_TRACK_ID;
++ lastRawPacket = 0;
+ capa["name"] = "TSSRT";
+ capa["desc"] = "This input allows for processing MPEG2-TS-based SRT streams. Use mode=listener "
+ "for push input.";
+@@ -66,6 +69,7 @@ namespace Mist{
+ capa["codecs"][0u][1u].append("AC3");
+ capa["codecs"][0u][1u].append("MP2");
+ capa["codecs"][0u][1u].append("opus");
++ capa["codecs"][1u][0u].append("rawts");
+
+ JSON::Value option;
+ option["arg"] = "integer";
+@@ -103,7 +107,16 @@ namespace Mist{
+ capa["optional"]["acceptable"]["select"][2u][0u] = 2;
+ capa["optional"]["acceptable"]["select"][2u][1u] = "Disallow non-matching streamid";
+
++ capa["optional"]["raw"]["name"] = "Raw input mode";
++ capa["optional"]["raw"]["help"] = "Enable raw MPEG-TS passthrough mode";
++ capa["optional"]["raw"]["option"] = "--raw";
+
++ option.null();
++ option["long"] = "raw";
++ option["short"] = "R";
++ option["help"] = "Enable raw MPEG-TS passthrough mode";
++ config->addOption("raw", option);
++
+ // Setup if we are called form with a thread for push-based input.
+ if (s != -1){
+ srtConn = Socket::SRTConnection(s);
+@@ -131,6 +144,8 @@ namespace Mist{
+
+ /// Live Setup of SRT Input. Runs only if we are the "main" thread
+ bool inputTSSRT::preRun(){
++ rawMode = config->getBool("raw");
++ if (rawMode){INFO_MSG("Entering raw mode");}
+ if (srtConn.getSocket() == -1){
+ std::string source = config->getString("input");
+ standAlone = false;
+@@ -183,6 +198,26 @@ namespace Mist{
+
+ size_t recvSize = srtConn.RecvNow();
+ if (recvSize){
++ if (rawMode){
++ keepAlive();
++ rawBuffer.append(srtConn.recvbuf, recvSize);
++ if (rawBuffer.size() >= 1316 && (lastRawPacket == 0 || lastRawPacket != Util::bootMS())){
++ if (rawIdx == INVALID_TRACK_ID){
++ rawIdx = meta.addTrack();
++ meta.setType(rawIdx, "meta");
++ meta.setCodec(rawIdx, "rawts");
++ meta.setID(rawIdx, 1);
++ userSelect[rawIdx].reload(streamName, rawIdx, COMM_STATUS_SOURCE);
++ }
++ uint64_t packetTime = Util::bootMS();
++ thisPacket.genericFill(packetTime, 0, 1, rawBuffer, rawBuffer.size(), 0, 0);
++ bufferLivePacket(thisPacket);
++ lastRawPacket = packetTime;
++ rawBuffer.truncate(0);
++ return;
++ }
++ continue;
++ }
+ if (assembler.assemble(tsStream, srtConn.recvbuf, recvSize, true)){hasPacket = tsStream.hasPacket();}
+ }else if (srtConn){
+ // This should not happen as the SRT socket is read blocking and won't return until there is
+diff --git a/src/input/input_tssrt.h b/src/input/input_tssrt.h
+index 40fa05c1..4f337b48 100644
+--- a/src/input/input_tssrt.h
++++ b/src/input/input_tssrt.h
+@@ -41,6 +41,10 @@ namespace Mist{
+ Socket::SRTConnection srtConn;
+ bool singularFlag;
+ virtual void connStats(Comms::Statistics &statComm);
++
++ Util::ResizeablePointer rawBuffer;
++ size_t rawIdx;
++ uint64_t lastRawPacket;
+ };
+ }// namespace Mist
+
+diff --git a/src/io.cpp b/src/io.cpp
+index 6a5844c4..fea32193 100644
+--- a/src/io.cpp
++++ b/src/io.cpp
+@@ -292,6 +292,13 @@ namespace Mist{
+ tPages.setInt("avail", pageOffset + packDataLen, pageIdx);
+ }
+
++ /// Wraps up the buffering of a shared memory data page
++ /// \param idx The track index of the page to finalize
++ void InOutBase::liveFinalize(size_t idx){
++ if (!livePage.count(idx)){return;}
++ bufferFinalize(idx, livePage[idx]);
++ }
++
+ /// Wraps up the buffering of a shared memory data page
+ /// \param idx The track index of the page to finalize
+ void InOutBase::bufferFinalize(size_t idx, IPC::sharedPage & page){
+diff --git a/src/io.h b/src/io.h
+index d839952a..e6545929 100644
+--- a/src/io.h
++++ b/src/io.h
+@@ -21,6 +21,7 @@ namespace Mist{
+
+ bool bufferStart(size_t idx, uint32_t pageNumber, IPC::sharedPage & page, DTSC::Meta & aMeta);
+ void bufferFinalize(size_t idx, IPC::sharedPage & page);
++ void liveFinalize(size_t idx);
+ bool isCurrentLivePage(size_t idx, uint32_t pageNumber);
+ void bufferRemove(size_t idx, uint32_t pageNumber);
+ void bufferLivePacket(const DTSC::Packet &packet);
+diff --git a/src/output/output_httpts.cpp b/src/output/output_httpts.cpp
+index 6aab2b9b..9daa9dc3 100644
+--- a/src/output/output_httpts.cpp
++++ b/src/output/output_httpts.cpp
+@@ -135,6 +135,7 @@ namespace Mist{
+ capa["codecs"][0u][1u].append("+AC3");
+ capa["codecs"][0u][1u].append("+MP2");
+ capa["codecs"][0u][1u].append("+opus");
++ capa["codecs"][1u][0u].append("rawts");
+ capa["methods"][0u]["handler"] = "http";
+ capa["methods"][0u]["type"] = "html5/video/mpeg";
+ capa["methods"][0u]["hrn"] = "TS HTTP progressive";
+diff --git a/src/output/output_ts.cpp b/src/output/output_ts.cpp
+index d2addc24..561bf9aa 100644
+--- a/src/output/output_ts.cpp
++++ b/src/output/output_ts.cpp
+@@ -179,6 +179,7 @@ namespace Mist{
+ capa["codecs"][0u][1u].append("+AC3");
+ capa["codecs"][0u][1u].append("+MP2");
+ capa["codecs"][0u][1u].append("+opus");
++ capa["codecs"][1u][0u].append("rawts");
+ cfg->addConnectorOptions(8888, capa);
+ config = cfg;
+ capa["push_urls"].append("tsudp://*");
+diff --git a/src/output/output_ts_base.cpp b/src/output/output_ts_base.cpp
+index 17b2a8ce..c4163e7f 100644
+--- a/src/output/output_ts_base.cpp
++++ b/src/output/output_ts_base.cpp
+@@ -75,6 +75,11 @@ namespace Mist{
+ size_t dataLen = 0;
+ thisPacket.getString("data", dataPointer, dataLen); // data
+
++ if (codec == "rawts"){
++ for (size_t i = 0; i+188 <= dataLen; i+=188){sendTS(dataPointer+i, 188);}
++ return;
++ }
++
+ packTime *= 90;
+ std::string bs;
+ // prepare bufferstring
+diff --git a/src/output/output_tsrist.cpp b/src/output/output_tsrist.cpp
+index ee57d8ee..c4e03f80 100644
+--- a/src/output/output_tsrist.cpp
++++ b/src/output/output_tsrist.cpp
+@@ -202,6 +202,7 @@ namespace Mist{
+ capa["codecs"][0u][1u].append("+AC3");
+ capa["codecs"][0u][1u].append("+MP2");
+ capa["codecs"][0u][1u].append("+opus");
++ capa["codecs"][1u][0u].append("rawts");
+
+ capa["optional"]["profile"]["name"] = "RIST profile";
+ capa["optional"]["profile"]["help"] = "RIST profile to use";
+diff --git a/src/output/output_tssrt.cpp b/src/output/output_tssrt.cpp
+index fb07b11e..db07dc91 100644
+--- a/src/output/output_tssrt.cpp
++++ b/src/output/output_tssrt.cpp
+@@ -200,6 +200,7 @@ namespace Mist{
+ capa["codecs"][0u][1u].append("AC3");
+ capa["codecs"][0u][1u].append("MP2");
+ capa["codecs"][0u][1u].append("opus");
++ capa["codecs"][1u][0u].append("rawts");
+ cfg->addConnectorOptions(8889, capa);
+ config = cfg;
+ capa["push_urls"].append("srt://*");
+--
+2.25.1
+
+
+From 6c117b63cfe85a204b48dfddddb355f416b39364 Mon Sep 17 00:00:00 2001
+From: Alex Kordic <alexandarkordic@gmail.com>
+Date: Thu, 4 Aug 2022 14:49:57 +0200
+Subject: [PATCH 11/38] Add s3 protocol to `URIReader`
+
+---
+ lib/timing.cpp | 12 +++++
+ lib/timing.h | 1 +
+ lib/urireader.cpp | 114 +++++++++++++++++++++++++++++++++++++++++++++-
+ 3 files changed, 125 insertions(+), 2 deletions(-)
+
+diff --git a/lib/timing.cpp b/lib/timing.cpp
+index 036f0b29..f339f4fe 100644
+--- a/lib/timing.cpp
++++ b/lib/timing.cpp
+@@ -114,3 +114,15 @@ std::string Util::getUTCString(uint64_t epoch){
+ ptm->tm_mday, ptm->tm_hour, ptm->tm_min, ptm->tm_sec);
+ return std::string(result);
+ }
++
++std::string Util::getDateString(uint64_t epoch){
++ char buffer[80];
++ time_t rawtime = epoch;
++ if (!epoch) {
++ time(&rawtime);
++ }
++ struct tm * timeinfo;
++ timeinfo = localtime(&rawtime);
++ strftime(buffer, sizeof(buffer), "%a, %d %b %Y %H:%M:%S %z", timeinfo);
++ return std::string(buffer);
++}
+diff --git a/lib/timing.h b/lib/timing.h
+index 107ccee5..b2a4d612 100644
+--- a/lib/timing.h
++++ b/lib/timing.h
+@@ -17,4 +17,5 @@ namespace Util{
+ uint64_t getNTP();
+ uint64_t epoch(); ///< Gets the amount of seconds since 01/01/1970.
+ std::string getUTCString(uint64_t epoch = 0);
++ std::string getDateString(uint64_t epoch = 0);
+ }// namespace Util
+diff --git a/lib/urireader.cpp b/lib/urireader.cpp
+index 80f204c8..ad85b052 100644
+--- a/lib/urireader.cpp
++++ b/lib/urireader.cpp
+@@ -3,11 +3,86 @@
+ #include "timing.h"
+ #include "urireader.h"
+ #include "util.h"
++#include "encode.h"
+ #include <sys/mman.h>
+ #include <sys/stat.h>
++#include <cstdlib>
++#include <ctime>
+
+ namespace HTTP{
+
++ // When another protocol needs this, rename struct to HeaderOverride or similar
++ struct HTTPHeadThenGet {
++ bool continueOperation;
++ std::string date, headAuthorization, getAuthorization;
++
++ HTTPHeadThenGet() : continueOperation(false) {}
++
++ void prepareHeadHeaders(HTTP::Downloader& downloader) {
++ if(!continueOperation) return;
++ downloader.setHeader("Date", date);
++ downloader.setHeader("Authorization", headAuthorization);
++ }
++
++ void prepareGetHeaders(HTTP::Downloader& downloader) {
++ if(!continueOperation) return;
++ // .setHeader() overwrites existing header value
++ downloader.setHeader("Date", date);
++ downloader.setHeader("Authorization", getAuthorization);
++ }
++ };
++
++#ifndef NOSSL
++ inline bool s3CalculateSignature(std::string& signature, const std::string method, const std::string date, const std::string& requestPath, const std::string& accessKey, const std::string& secret) {
++ std::string toSign = method + "\n\n\n" + date + "\n" + requestPath;
++ unsigned char signatureBytes[MBEDTLS_MD_MAX_SIZE];
++ const int sha1Size = 20;
++ mbedtls_md_context_t md_ctx = {0};
++ // TODO: When we use MBEDTLS_MD_SHA512 ? Consult documentation/code
++ const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA1);
++ if (!md_info){ FAIL_MSG("error s3 MBEDTLS_MD_SHA1 unavailable"); return false; }
++ int status = mbedtls_md_setup(&md_ctx, md_info, 1);
++ if(status != 0) { FAIL_MSG("error s3 mbedtls_md_setup error %d", status); return false; }
++ status = mbedtls_md_hmac_starts(&md_ctx, (const unsigned char *)secret.c_str(), secret.size());
++ if(status != 0) { FAIL_MSG("error s3 mbedtls_md_hmac_starts error %d", status); return false; }
++ status = mbedtls_md_hmac_update(&md_ctx, (const unsigned char *)toSign.c_str(), toSign.size());
++ if(status != 0) { FAIL_MSG("error s3 mbedtls_md_hmac_update error %d", status); return false; }
++ status = mbedtls_md_hmac_finish(&md_ctx, signatureBytes);
++ if(status != 0) { FAIL_MSG("error s3 mbedtls_md_hmac_finish error %d", status); return false; }
++ std::string base64encoded = Encodings::Base64::encode(std::string((const char*)signatureBytes, sha1Size));
++ signature = "AWS " + accessKey + ":" + base64encoded;
++ return true;
++ }
++ // Input url == s3+https://s3_key:secret@storage.googleapis.com/alexk-dms-upload-test/testvideo.ts
++ // Transform to:
++ // url=https://storage.googleapis.com/alexk-dms-upload-test/testvideo.ts
++ // header Date: ${Util::getDateString(()}
++ // header Authorization: AWS ${url.user}:${signature}
++ inline HTTPHeadThenGet s3TransformToHttp(HTTP::URL& url) {
++ HTTPHeadThenGet result;
++ result.date = Util::getDateString();
++ // remove "s3+" prefix
++ url.protocol = url.protocol.erase(0, 3);
++ // Use user and pass to create signature and remove from HTTP request
++ std::string accessKey(url.user), secret(url.pass);
++ url.user = "";
++ url.pass = "";
++ std::string requestPath = "/" + Encodings::URL::encode(url.path, "/:=@[]#?&");
++ if(url.args.size()) requestPath += "?" + url.args;
++ // Calculate Authorization data
++ if(!s3CalculateSignature(result.headAuthorization, "HEAD", result.date, requestPath, accessKey, secret)) {
++ result.continueOperation = false;
++ return result;
++ }
++ if(!s3CalculateSignature(result.getAuthorization, "GET", result.date, requestPath, accessKey, secret)) {
++ result.continueOperation = false;
++ return result;
++ }
++ result.continueOperation = true;
++ return result;
++ }
++#endif // ifndef NOSSL
++
+ void URIReader::init(){
+ handle = -1;
+ mapped = 0;
+@@ -97,12 +172,45 @@ namespace HTTP{
+ }
+ }
+
++ // prepare for s3 and http
++ HTTPHeadThenGet httpHeaderOverride;
++
++#ifndef NOSSL
++ // In case of s3 URI we prepare HTTP request with AWS authorization and rely on HTTP logic below
++ if (myURI.protocol == "s3+https" || myURI.protocol == "s3+http"){
++ // Check fallback to global credentials in env vars
++ bool haveCredentials = myURI.user.size() && myURI.pass.size();
++ if(!haveCredentials) {
++ // Use environment variables
++ char * envValue = std::getenv("S3_ACCESS_KEY_ID");
++ if(envValue == NULL) {
++ FAIL_MSG("error s3 uri without credentials. Consider setting S3_ACCESS_KEY_ID env var");
++ return false;
++ }
++ myURI.user = envValue;
++ envValue = std::getenv("S3_SECRET_ACCESS_KEY");
++ if(envValue == NULL) {
++ FAIL_MSG("error s3 uri without credentials. Consider setting S3_SECRET_ACCESS_KEY env var");
++ return false;
++ }
++ myURI.pass = envValue;
++ }
++ // Transform s3 url to HTTP request:
++ httpHeaderOverride = s3TransformToHttp(myURI);
++ bool errorInSignatureCalculation = !httpHeaderOverride.continueOperation;
++ if(errorInSignatureCalculation) return false;
++ // Do not return, continue to HTTP case
++ }
++#endif // ifndef NOSSL
++
+ // HTTP, stream or regular download?
+ if (myURI.protocol == "http" || myURI.protocol == "https"){
+- stateType = HTTP::HTTP;
++ stateType = HTTP;
++ downer.clearHeaders();
+
++ // One set of headers specified for HEAD request
++ httpHeaderOverride.prepareHeadHeaders(downer);
+ // Send HEAD request to determine range request is supported, and get total length
+- downer.clearHeaders();
+ if (userAgentOverride.size()){downer.setHeader("User-Agent", userAgentOverride);}
+ if (!downer.head(myURI) || !downer.isOk()){
+ FAIL_MSG("Error getting URI info for '%s': %" PRIu32 " %s", myURI.getUrl().c_str(),
+@@ -120,6 +228,8 @@ namespace HTTP{
+ myURI = downer.lastURL();
+ }
+
++ // Other set of headers specified for GET request
++ httpHeaderOverride.prepareGetHeaders(downer);
+ // streaming mode when size is unknown
+ if (!supportRangeRequest){
+ MEDIUM_MSG("URI get without range request: %s, totalsize: %zu", myURI.getUrl().c_str(), totalSize);
+--
+2.25.1
+
+
+From df4076a06eeb6fbc0fc20d9ace0091eb476420a2 Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Wed, 17 Aug 2022 14:57:29 +0200
+Subject: [PATCH 12/38] Added ResizeablePointer::shift operator to shift data
+ forward in buffer
+
+---
+ lib/util.cpp | 11 +++++++++++
+ lib/util.h | 1 +
+ 2 files changed, 12 insertions(+)
+
+diff --git a/lib/util.cpp b/lib/util.cpp
+index 8dd75727..d4ff380f 100644
+--- a/lib/util.cpp
++++ b/lib/util.cpp
+@@ -191,6 +191,17 @@ namespace Util{
+ maxSize = 0;
+ }
+
++ void ResizeablePointer::shift(size_t byteCount){
++ //Shifting the entire buffer is easy, we do nothing and set size to zero
++ if (byteCount >= currSize){
++ currSize = 0;
++ return;
++ }
++ //Shifting partial needs a memmove and size change
++ memmove(ptr, ((char*)ptr)+byteCount, currSize-byteCount);
++ currSize -= byteCount;
++ }
++
+ bool ResizeablePointer::assign(const void *p, uint32_t l){
+ if (!allocate(l)){return false;}
+ memcpy(ptr, p, l);
+diff --git a/lib/util.h b/lib/util.h
+index 84b0d56c..d28a7f76 100644
+--- a/lib/util.h
++++ b/lib/util.h
+@@ -45,6 +45,7 @@ namespace Util{
+ bool append(const void *p, uint32_t l);
+ bool append(const std::string &str);
+ bool allocate(uint32_t l);
++ void shift(size_t byteCount);
+ uint32_t rsize();
+ void truncate(const size_t newLen);
+ inline operator char *(){return (char *)ptr;}
+--
+2.25.1
+
+
+From 44c28097332b0a137c4674bcfd8d243b593b3cee Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Thu, 18 Aug 2022 02:44:11 +0200
+Subject: [PATCH 13/38] Decreased verbosity of harmless data offset warnings,
+ increased verbosity of harmful ones
+
+---
+ src/input/input.cpp | 16 +++++++++++-----
+ 1 file changed, 11 insertions(+), 5 deletions(-)
+
+diff --git a/src/input/input.cpp b/src/input/input.cpp
+index 942a55f8..0a062392 100644
+--- a/src/input/input.cpp
++++ b/src/input/input.cpp
+@@ -1382,10 +1382,12 @@ namespace Mist{
+ size_t currPos = tPages.getInt("avail", pageIdx);
+ if (currPos){
+ size_t keySize = keys.getSize(keyNum);
+- if (currPos-prevPos != keySize){
+- INFO_MSG("Key %" PRIu32 " was %zu bytes but should've been %zu bytes! (differs %d)", keyNum, currPos-prevPos, keySize, (int)(currPos-prevPos-keySize));
+- }else{
++ if (currPos-prevPos == keySize){
+ VERYHIGH_MSG("Key %" PRIu32 " was %zu bytes", keyNum, currPos-prevPos);
++ }else if (currPos-prevPos > keySize){
++ FAIL_MSG("Key %" PRIu32 " was %zu bytes but should've been %zu bytes! (differs %d)", keyNum, currPos-prevPos, keySize, (int)(currPos-prevPos-keySize));
++ }else{
++ MEDIUM_MSG("Key %" PRIu32 " was %zu bytes but should've been %zu bytes! (differs %d)", keyNum, currPos-prevPos, keySize, (int)(currPos-prevPos-keySize));
+ }
+ ++keyNum;
+ prevPos = currPos;
+@@ -1412,8 +1414,12 @@ namespace Mist{
+ size_t currPos = tPages.getInt("avail", pageIdx);
+ if (currPos){
+ size_t keySize = keys.getSize(keyNum);
+- if (currPos-prevPos != keySize){
+- INFO_MSG("Key %" PRIu32 " was %zu bytes but should've been %zu bytes! (differs %d)", keyNum, currPos-prevPos, keySize, (int)(currPos-prevPos-keySize));
++ if (currPos-prevPos == keySize){
++ VERYHIGH_MSG("Key %" PRIu32 " was %zu bytes", keyNum, currPos-prevPos);
++ }else if (currPos-prevPos > keySize){
++ FAIL_MSG("Key %" PRIu32 " was %zu bytes but should've been %zu bytes! (differs %d)", keyNum, currPos-prevPos, keySize, (int)(currPos-prevPos-keySize));
++ }else{
++ MEDIUM_MSG("Key %" PRIu32 " was %zu bytes but should've been %zu bytes! (differs %d)", keyNum, currPos-prevPos, keySize, (int)(currPos-prevPos-keySize));
+ }
+ ++keyNum;
+ prevPos = currPos;
+--
+2.25.1
+
+
+From 747438746c068f9217f804fab4d58bdf57156a43 Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Thu, 18 Aug 2022 15:45:46 +0200
+Subject: [PATCH 14/38] Change VoD data page logic to use wallclock seconds
+ rather than loop iterations for timeouts
+
+---
+ src/input/input.cpp | 30 ++++++++++++++++++++++--------
+ src/input/input.h | 2 +-
+ 2 files changed, 23 insertions(+), 9 deletions(-)
+
+diff --git a/src/input/input.cpp b/src/input/input.cpp
+index 0a062392..a86fe3cd 100644
+--- a/src/input/input.cpp
++++ b/src/input/input.cpp
+@@ -1022,28 +1022,42 @@ namespace Mist{
+ }
+
+ void Input::removeUnused(){
++ uint64_t cTime = Util::bootSecs();
+ std::set<size_t> validTracks = M.getValidTracks();
++ std::map<size_t, std::set<uint32_t> > checkedPages;
+ for (std::set<size_t>::iterator it = validTracks.begin(); it != validTracks.end(); ++it){
+ Util::RelAccX &tPages = meta.pages(*it);
+ for (size_t i = tPages.getDeleted(); i < tPages.getEndPos(); i++){
+ uint64_t pageNum = tPages.getInt("firstkey", i);
++ checkedPages[*it].insert(pageNum);
+ if (pageCounter[*it].count(pageNum)){
+ // If the page is still being written to, reset the counter rather than potentially unloading it
+ if (isCurrentLivePage(*it, pageNum)){
+- pageCounter[*it][pageNum] = DEFAULT_PAGE_TIMEOUT;
++ pageCounter[*it][pageNum] = cTime;
+ continue;
+ }
+- --pageCounter[*it][pageNum];
+- if (!pageCounter[*it][pageNum]){
++ if (cTime > pageCounter[*it][pageNum] + DEFAULT_PAGE_TIMEOUT){
+ pageCounter[*it].erase(pageNum);
+ bufferRemove(*it, pageNum);
+ }
++ }else{
++ pageCounter[*it][pageNum] = cTime;
+ }
+- else{
+- pageCounter[*it][pageNum] = DEFAULT_PAGE_TIMEOUT;
++ }
++ }
++ //Check pages we buffered but forgot about
++ for (std::map<size_t, std::map<uint32_t, size_t> >::iterator it = pageCounter.begin();
++ it != pageCounter.end(); it++){
++ for (std::map<uint32_t, size_t>::iterator it2 = it->second.begin(); it2 != it->second.end(); it2++){
++ if (!checkedPages.count(it->first) || !checkedPages[it->first].count(it2->first)){
++ INFO_MSG("Deleting forgotten page %zu:%" PRIu32, it->first, it2->first);
++ bufferRemove(it->first, it2->first);
++ it->second.erase(it2);
++ it2 = it->second.begin();
+ }
+ }
+ }
++
+ }
+
+ std::string formatGUID(const std::string &val){
+@@ -1290,8 +1304,8 @@ namespace Mist{
+ }
+ uint32_t pageNumber = tPages.getInt("firstkey", pageIdx);
+ if (isBuffered(idx, pageNumber, meta)){
+- // Mark the page for removal after 15 seconds of no one watching it
+- pageCounter[idx][pageNumber] = DEFAULT_PAGE_TIMEOUT;
++ // Mark the page as still actively requested
++ pageCounter[idx][pageNumber] = Util::bootSecs();
+ DONTEVEN_MSG("Track %zu, key %" PRIu32 " is already buffered in page %" PRIu32
+ ". Cancelling bufferFrame",
+ idx, keyNum, pageNumber);
+@@ -1432,7 +1446,7 @@ namespace Mist{
+ idx, pageNumber, tPages.getInt("firsttime", pageIdx), thisTime, bufferTimer);
+ INFO_MSG(" (%" PRIu32 "/%" PRIu64 " parts, %" PRIu64 " bytes)", packCounter,
+ tPages.getInt("parts", pageIdx), byteCounter);
+- pageCounter[idx][pageNumber] = DEFAULT_PAGE_TIMEOUT;
++ pageCounter[idx][pageNumber] = Util::bootSecs();
+ return true;
+ }
+
+diff --git a/src/input/input.h b/src/input/input.h
+index 28880337..8d7e8891 100644
+--- a/src/input/input.h
++++ b/src/input/input.h
+@@ -87,7 +87,7 @@ namespace Mist{
+
+ IPC::sharedPage streamStatus;
+
+- std::map<size_t, std::map<uint32_t, size_t> > pageCounter;
++ std::map<size_t, std::map<uint32_t, uint64_t> > pageCounter;
+
+ static Input *singleton;
+
+--
+2.25.1
+
+
+From b210b4f5afc98b3cb4ea46e607267e12cf868831 Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Wed, 17 Aug 2022 17:06:25 +0200
+Subject: [PATCH 15/38] Fixed seek-related bugs in URIReader for HTTP sources
+
+---
+ lib/urireader.cpp | 40 ++++++++++++++++++++++------------------
+ 1 file changed, 22 insertions(+), 18 deletions(-)
+
+diff --git a/lib/urireader.cpp b/lib/urireader.cpp
+index ad85b052..d4dd0e31 100644
+--- a/lib/urireader.cpp
++++ b/lib/urireader.cpp
+@@ -255,20 +255,30 @@ namespace HTTP{
+
+ // seek to pos, return true if succeeded.
+ bool URIReader::seek(const uint64_t pos){
+- if (isSeekable()){
+- allData.truncate(0);
+- bufPos = 0;
+- if (stateType == HTTP::File){
+- curPos = pos;
+- return true;
+- }else if (stateType == HTTP::HTTP && supportRangeRequest){
+- INFO_MSG("SEEK: RangeRequest to %" PRIu64, pos);
+- if (!downer.getRangeNonBlocking(myURI.getUrl(), pos, 0)){
+- FAIL_MSG("error loading request");
+- }
+- }
++ //Seeking in a non-seekable source? No-op, always fails.
++ if (!isSeekable()){return false;}
++
++ //Reset internal buffers
++ allData.truncate(0);
++ bufPos = 0;
++
++ //Files always succeed because we use memmap
++ if (stateType == HTTP::File){
++ curPos = pos;
++ return true;
+ }
+
++ //HTTP-based needs to do a range request
++ if (stateType == HTTP::HTTP && supportRangeRequest){
++ downer.getSocket().close();
++ downer.getSocket().Received().clear();
++ if (!downer.getRangeNonBlocking(myURI.getUrl(), pos, 0)){
++ FAIL_MSG("Error making range request");
++ return false;
++ }
++ curPos = pos;
++ return true;
++ }
+ return false;
+ }
+
+@@ -323,19 +333,13 @@ namespace HTTP{
+
+ }else if (stateType == HTTP::HTTP){
+ downer.continueNonBlocking(cb);
+- if (curPos == downer.const_data().size()){
+- Util::sleep(50);
+- }
+- curPos = downer.const_data().size();
+ }else{// streaming mode
+ int s;
+- static int totaal = 0;
+ if ((downer.getSocket() && downer.getSocket().spool())){// || downer.getSocket().Received().size() > 0){
+ s = downer.getSocket().Received().bytes(wantedLen);
+ std::string buf = downer.getSocket().Received().remove(s);
+
+ cb.dataCallback(buf.data(), s);
+- totaal += s;
+ }else{
+ Util::sleep(50);
+ }
+--
+2.25.1
+
+
+From 01a2ff54edb0cb544495bff57d8cc2a61440db2a Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Wed, 17 Aug 2022 14:57:48 +0200
+Subject: [PATCH 16/38] Converted MP4 input to use URIReader
+
+---
+ src/input/input_mp4.cpp | 518 ++++++++++++++++++++++------------------
+ src/input/input_mp4.h | 15 +-
+ 2 files changed, 298 insertions(+), 235 deletions(-)
+
+diff --git a/src/input/input_mp4.cpp b/src/input/input_mp4.cpp
+index 2e1c7923..f85911fb 100644
+--- a/src/input/input_mp4.cpp
++++ b/src/input/input_mp4.cpp
+@@ -159,11 +159,14 @@ namespace Mist{
+ }
+
+ inputMP4::inputMP4(Util::Config *cfg) : Input(cfg){
+- malSize = 4; // initialise data read buffer to 0;
+- data = (char *)malloc(malSize);
+ capa["name"] = "MP4";
+ capa["desc"] = "This input allows streaming of MP4 files as Video on Demand.";
+- capa["source_match"] = "/*.mp4";
++ capa["source_match"].append("/*.mp4");
++ capa["source_match"].append("http://*.mp4");
++ capa["source_match"].append("https://*.mp4");
++ capa["source_match"].append("s3+http://*.mp4");
++ capa["source_match"].append("s3+https://*.mp4");
++ capa["source_match"].append("mp4:*");
+ capa["source_file"] = "$source";
+ capa["priority"] = 9;
+ capa["codecs"][0u][0u].append("HEVC");
+@@ -173,10 +176,9 @@ namespace Mist{
+ capa["codecs"][0u][1u].append("AAC");
+ capa["codecs"][0u][1u].append("AC3");
+ capa["codecs"][0u][1u].append("MP3");
++ readPos = 0;
+ }
+
+- inputMP4::~inputMP4(){free(data);}
+-
+ bool inputMP4::checkArguments(){
+ if (config->getString("input") == "-"){
+ std::cerr << "Input from stdin not yet supported" << std::endl;
+@@ -199,255 +201,292 @@ namespace Mist{
+
+ bool inputMP4::preRun(){
+ // open File
+- inFile = fopen(config->getString("input").c_str(), "r");
++ std::string inUrl = config->getString("input");
++ if (inUrl.size() > 4 && inUrl.substr(0, 4) == "mp4:"){inUrl.erase(0, 4);}
++ inFile.open(inUrl);
+ if (!inFile){return false;}
++ if (!inFile.isSeekable()){
++ FAIL_MSG("MP4 input only supports seekable data sources, for now, and this source is not seekable: %s", config->getString("input").c_str());
++ return false;
++ }
+ return true;
+ }
+
++ void inputMP4::dataCallback(const char *ptr, size_t size){readBuffer.append(ptr, size);}
++
+ bool inputMP4::readHeader(){
+ if (!inFile){
+ INFO_MSG("inFile failed!");
+ return false;
+ }
++ bool hasMoov = false;
++ readBuffer.truncate(0);
++ readPos = 0;
+
+ // first we get the necessary header parts
+ size_t tNumber = 0;
+- while (!feof(inFile)){
+- std::string boxType = MP4::readBoxType(inFile);
+- if (boxType == "erro"){break;}
++ activityCounter = Util::bootSecs();
++ while (inFile && keepRunning()){
++ //Read box header if needed
++ while (readBuffer.size() < 16 && inFile && keepRunning()){inFile.readSome(16, *this);}
++ //Failed? Abort.
++ if (readBuffer.size() < 16){
++ FAIL_MSG("Could not read box header from input!");
++ break;
++ }
++ //Box type is always on bytes 5-8 from the start of a box
++ std::string boxType = std::string(readBuffer+4, 4);
++ uint64_t boxSize = MP4::calcBoxSize(readBuffer);
+ if (boxType == "moov"){
+- MP4::MOOV moovBox;
+- moovBox.read(inFile);
+- // for all box in moov
++ while (readBuffer.size() < boxSize && inFile && keepRunning()){inFile.readSome(boxSize-readBuffer.size(), *this);}
++ if (readBuffer.size() < boxSize){
++ FAIL_MSG("Could not read entire MOOV box into memory");
++ break;
++ }
++ MP4::Box moovBox(readBuffer, false);
+
+- std::deque<MP4::TRAK> trak = moovBox.getChildren<MP4::TRAK>();
++ // for all box in moov
++ std::deque<MP4::TRAK> trak = ((MP4::MOOV*)&moovBox)->getChildren<MP4::TRAK>();
+ for (std::deque<MP4::TRAK>::iterator trakIt = trak.begin(); trakIt != trak.end(); trakIt++){
+ trackHeaders.push_back(mp4TrackHeader());
+ trackHeaders.rbegin()->read(*trakIt);
+ }
+- continue;
++ hasMoov = true;
++ break;
+ }
+- if (!MP4::skipBox(inFile)){// moving on to next box
+- FAIL_MSG("Error in skipping box, exiting");
+- return false;
++ activityCounter = Util::bootSecs();
++ //Skip to next box
++ if (readBuffer.size() > boxSize){
++ readBuffer.shift(boxSize);
++ readPos += boxSize;
++ }else{
++ readBuffer.truncate(0);
++ if (!inFile.seek(readPos + boxSize)){
++ FAIL_MSG("Seek to %" PRIu64 " failed! Aborting load", readPos+boxSize);
++ }
++ readPos = inFile.getPos();
+ }
+ }
+- fseeko(inFile, 0, SEEK_SET);
+
+ // See whether a separate header file exists.
+- if (readExistingHeader()){return true;}
+- HIGH_MSG("Not read existing header");
++ if (readExistingHeader()){
++ bps = 0;
++ std::set<size_t> tracks = M.getValidTracks();
++ for (std::set<size_t>::iterator it = tracks.begin(); it != tracks.end(); it++){bps += M.getBps(*it);}
++ return true;
++ }
++ INFO_MSG("Not read existing header");
+
+ meta.reInit(isSingular() ? streamName : "");
++ if (!hasMoov){
++ FAIL_MSG("No MOOV box found; aborting header creation!");
++ return false;
++ }
+
+ tNumber = 0;
+ // Create header file from MP4 data
+- while (!feof(inFile)){
+- std::string boxType = MP4::readBoxType(inFile);
+- if (boxType == "erro"){break;}
+- if (boxType == "moov"){
+- MP4::MOOV moovBox;
+- moovBox.read(inFile);
++ MP4::Box moovBox(readBuffer, false);
+
+- std::deque<MP4::TRAK> trak = moovBox.getChildren<MP4::TRAK>();
+- HIGH_MSG("Obtained %zu trak Boxes", trak.size());
++ std::deque<MP4::TRAK> trak = ((MP4::MOOV*)&moovBox)->getChildren<MP4::TRAK>();
++ HIGH_MSG("Obtained %zu trak Boxes", trak.size());
+
+- for (std::deque<MP4::TRAK>::iterator trakIt = trak.begin(); trakIt != trak.end(); trakIt++){
+- MP4::MDIA mdiaBox = trakIt->getChild<MP4::MDIA>();
++ for (std::deque<MP4::TRAK>::iterator trakIt = trak.begin(); trakIt != trak.end(); trakIt++){
++ MP4::MDIA mdiaBox = trakIt->getChild<MP4::MDIA>();
+
+- std::string hdlrType = mdiaBox.getChild<MP4::HDLR>().getHandlerType();
+- if (hdlrType != "vide" && hdlrType != "soun" && hdlrType != "sbtl"){
+- INFO_MSG("Unsupported handler: %s", hdlrType.c_str());
+- continue;
+- }
++ std::string hdlrType = mdiaBox.getChild<MP4::HDLR>().getHandlerType();
++ if (hdlrType != "vide" && hdlrType != "soun" && hdlrType != "sbtl"){
++ INFO_MSG("Unsupported handler: %s", hdlrType.c_str());
++ continue;
++ }
+
+- tNumber = meta.addTrack();
++ tNumber = meta.addTrack();
+
+- MP4::TKHD tkhdBox = trakIt->getChild<MP4::TKHD>();
+- if (tkhdBox.getWidth() > 0){
+- meta.setWidth(tNumber, tkhdBox.getWidth());
+- meta.setHeight(tNumber, tkhdBox.getHeight());
+- }
+- meta.setID(tNumber, tkhdBox.getTrackID());
+-
+- MP4::MDHD mdhdBox = mdiaBox.getChild<MP4::MDHD>();
+- uint64_t timescale = mdhdBox.getTimeScale();
+- meta.setLang(tNumber, mdhdBox.getLanguage());
+-
+- MP4::STBL stblBox = mdiaBox.getChild<MP4::MINF>().getChild<MP4::STBL>();
+-
+- MP4::STSD stsdBox = stblBox.getChild<MP4::STSD>();
+- MP4::Box sEntryBox = stsdBox.getEntry(0);
+- std::string sType = sEntryBox.getType();
+- HIGH_MSG("Found track %zu of type %s", tNumber, sType.c_str());
+-
+- if (sType == "avc1" || sType == "h264" || sType == "mp4v"){
+- MP4::VisualSampleEntry &vEntryBox = (MP4::VisualSampleEntry &)sEntryBox;
+- meta.setType(tNumber, "video");
+- meta.setCodec(tNumber, "H264");
+- if (!meta.getWidth(tNumber)){
+- meta.setWidth(tNumber, vEntryBox.getWidth());
+- meta.setHeight(tNumber, vEntryBox.getHeight());
+- }
+- MP4::Box initBox = vEntryBox.getCLAP();
+- if (initBox.isType("avcC")){
+- meta.setInit(tNumber, initBox.payload(), initBox.payloadSize());
+- }
+- initBox = vEntryBox.getPASP();
+- if (initBox.isType("avcC")){
+- meta.setInit(tNumber, initBox.payload(), initBox.payloadSize());
+- }
+- /// this is a hacky way around invalid FLV data (since it gets ignored nearly
+- /// everywhere, but we do need correct data...
+- if (!meta.getWidth(tNumber)){
+- h264::sequenceParameterSet sps;
+- sps.fromDTSCInit(meta.getInit(tNumber));
+- h264::SPSMeta spsChar = sps.getCharacteristics();
+- meta.setWidth(tNumber, spsChar.width);
+- meta.setHeight(tNumber, spsChar.height);
+- }
+- }
+- if (sType == "hev1" || sType == "hvc1"){
+- MP4::VisualSampleEntry &vEntryBox = (MP4::VisualSampleEntry &)sEntryBox;
+- meta.setType(tNumber, "video");
+- meta.setCodec(tNumber, "HEVC");
+- if (!meta.getWidth(tNumber)){
+- meta.setWidth(tNumber, vEntryBox.getWidth());
+- meta.setHeight(tNumber, vEntryBox.getHeight());
+- }
+- MP4::Box initBox = vEntryBox.getCLAP();
+- if (initBox.isType("hvcC")){
+- meta.setInit(tNumber, initBox.payload(), initBox.payloadSize());
+- }
+- initBox = vEntryBox.getPASP();
+- if (initBox.isType("hvcC")){
+- meta.setInit(tNumber, initBox.payload(), initBox.payloadSize());
+- }
+- }
+- if (sType == "mp4a" || sType == "aac " || sType == "ac-3"){
+- MP4::AudioSampleEntry &aEntryBox = (MP4::AudioSampleEntry &)sEntryBox;
+- meta.setType(tNumber, "audio");
+- meta.setChannels(tNumber, aEntryBox.getChannelCount());
+- meta.setRate(tNumber, aEntryBox.getSampleRate());
+-
+- if (sType == "ac-3"){
+- meta.setCodec(tNumber, "AC3");
+- }else{
+- MP4::ESDS esdsBox = (MP4::ESDS &)(aEntryBox.getCodecBox());
+- meta.setCodec(tNumber, esdsBox.getCodec());
+- meta.setInit(tNumber, esdsBox.getInitData());
+- }
+- meta.setSize(tNumber, 16); ///\todo this might be nice to calculate from mp4 file;
+- }
+- if (sType == "tx3g"){// plain text subtitles
+- meta.setType(tNumber, "meta");
+- meta.setCodec(tNumber, "subtitle");
++ MP4::TKHD tkhdBox = trakIt->getChild<MP4::TKHD>();
++ if (tkhdBox.getWidth() > 0){
++ meta.setWidth(tNumber, tkhdBox.getWidth());
++ meta.setHeight(tNumber, tkhdBox.getHeight());
++ }
++ meta.setID(tNumber, tkhdBox.getTrackID());
++
++ MP4::MDHD mdhdBox = mdiaBox.getChild<MP4::MDHD>();
++ uint64_t timescale = mdhdBox.getTimeScale();
++ meta.setLang(tNumber, mdhdBox.getLanguage());
++
++ MP4::STBL stblBox = mdiaBox.getChild<MP4::MINF>().getChild<MP4::STBL>();
++
++ MP4::STSD stsdBox = stblBox.getChild<MP4::STSD>();
++ MP4::Box sEntryBox = stsdBox.getEntry(0);
++ std::string sType = sEntryBox.getType();
++ HIGH_MSG("Found track %zu of type %s", tNumber, sType.c_str());
++
++ if (sType == "avc1" || sType == "h264" || sType == "mp4v"){
++ MP4::VisualSampleEntry &vEntryBox = (MP4::VisualSampleEntry &)sEntryBox;
++ meta.setType(tNumber, "video");
++ meta.setCodec(tNumber, "H264");
++ if (!meta.getWidth(tNumber)){
++ meta.setWidth(tNumber, vEntryBox.getWidth());
++ meta.setHeight(tNumber, vEntryBox.getHeight());
++ }
++ MP4::Box initBox = vEntryBox.getCLAP();
++ if (initBox.isType("avcC")){
++ meta.setInit(tNumber, initBox.payload(), initBox.payloadSize());
++ }
++ initBox = vEntryBox.getPASP();
++ if (initBox.isType("avcC")){
++ meta.setInit(tNumber, initBox.payload(), initBox.payloadSize());
++ }
++ /// this is a hacky way around invalid FLV data (since it gets ignored nearly
++ /// everywhere, but we do need correct data...
++ if (!meta.getWidth(tNumber)){
++ h264::sequenceParameterSet sps;
++ sps.fromDTSCInit(meta.getInit(tNumber));
++ h264::SPSMeta spsChar = sps.getCharacteristics();
++ meta.setWidth(tNumber, spsChar.width);
++ meta.setHeight(tNumber, spsChar.height);
++ }
++ }
++ if (sType == "hev1" || sType == "hvc1"){
++ MP4::VisualSampleEntry &vEntryBox = (MP4::VisualSampleEntry &)sEntryBox;
++ meta.setType(tNumber, "video");
++ meta.setCodec(tNumber, "HEVC");
++ if (!meta.getWidth(tNumber)){
++ meta.setWidth(tNumber, vEntryBox.getWidth());
++ meta.setHeight(tNumber, vEntryBox.getHeight());
++ }
++ MP4::Box initBox = vEntryBox.getCLAP();
++ if (initBox.isType("hvcC")){
++ meta.setInit(tNumber, initBox.payload(), initBox.payloadSize());
++ }
++ initBox = vEntryBox.getPASP();
++ if (initBox.isType("hvcC")){
++ meta.setInit(tNumber, initBox.payload(), initBox.payloadSize());
++ }
++ }
++ if (sType == "mp4a" || sType == "aac " || sType == "ac-3"){
++ MP4::AudioSampleEntry &aEntryBox = (MP4::AudioSampleEntry &)sEntryBox;
++ meta.setType(tNumber, "audio");
++ meta.setChannels(tNumber, aEntryBox.getChannelCount());
++ meta.setRate(tNumber, aEntryBox.getSampleRate());
++
++ if (sType == "ac-3"){
++ meta.setCodec(tNumber, "AC3");
++ }else{
++ MP4::ESDS esdsBox = (MP4::ESDS &)(aEntryBox.getCodecBox());
++ meta.setCodec(tNumber, esdsBox.getCodec());
++ meta.setInit(tNumber, esdsBox.getInitData());
++ }
++ meta.setSize(tNumber, 16); ///\todo this might be nice to calculate from mp4 file;
++ }
++ if (sType == "tx3g"){// plain text subtitles
++ meta.setType(tNumber, "meta");
++ meta.setCodec(tNumber, "subtitle");
++ }
++
++ MP4::STSS stssBox = stblBox.getChild<MP4::STSS>();
++ MP4::STTS sttsBox = stblBox.getChild<MP4::STTS>();
++ MP4::STSZ stszBox = stblBox.getChild<MP4::STSZ>();
++ MP4::STCO stcoBox = stblBox.getChild<MP4::STCO>();
++ MP4::CO64 co64Box = stblBox.getChild<MP4::CO64>();
++ MP4::STSC stscBox = stblBox.getChild<MP4::STSC>();
++ MP4::CTTS cttsBox = stblBox.getChild<MP4::CTTS>(); // optional ctts box
++
++ bool stco64 = co64Box.isType("co64");
++ bool hasCTTS = cttsBox.isType("ctts");
++
++ uint64_t totaldur = 0; ///\todo note: set this to begin time
++ mp4PartBpos BsetPart;
++
++ uint64_t entryNo = 0;
++ uint64_t sampleNo = 0;
++
++ uint64_t stssIndex = 0;
++ uint64_t stcoIndex = 0;
++ uint64_t stscIndex = 0;
++ uint64_t cttsIndex = 0; // current ctts Index we are reading
++ uint64_t cttsEntryRead = 0; // current part of ctts we are reading
++
++ uint64_t stssCount = stssBox.getEntryCount();
++ uint64_t stscCount = stscBox.getEntryCount();
++ uint64_t stszCount = stszBox.getSampleCount();
++ uint64_t stcoCount = (stco64 ? co64Box.getEntryCount() : stcoBox.getEntryCount());
++
++ MP4::STTSEntry sttsEntry = sttsBox.getSTTSEntry(0);
++
++ uint32_t fromSTCOinSTSC = 0;
++ uint64_t tmpOffset = (stco64 ? co64Box.getChunkOffset(0) : stcoBox.getChunkOffset(0));
++
++ uint64_t nextFirstChunk = (stscCount > 1 ? stscBox.getSTSCEntry(1).firstChunk - 1 : stcoCount);
++
++ for (uint64_t stszIndex = 0; stszIndex < stszCount; ++stszIndex){
++ if (stcoIndex >= nextFirstChunk){
++ ++stscIndex;
++ nextFirstChunk =
++ (stscIndex + 1 < stscCount ? stscBox.getSTSCEntry(stscIndex + 1).firstChunk - 1 : stcoCount);
++ }
++ BsetPart.keyframe = (meta.getType(tNumber) == "video" && stssIndex < stssCount &&
++ stszIndex + 1 == stssBox.getSampleNumber(stssIndex));
++ if (BsetPart.keyframe){++stssIndex;}
++ // in bpos set
++ BsetPart.stcoNr = stcoIndex;
++ // bpos = chunkoffset[samplenr] in stco
++ BsetPart.bpos = tmpOffset;
++ ++fromSTCOinSTSC;
++ if (fromSTCOinSTSC < stscBox.getSTSCEntry(stscIndex).samplesPerChunk){// as long as we are still in this chunk
++ tmpOffset += stszBox.getEntrySize(stszIndex);
++ }else{
++ ++stcoIndex;
++ fromSTCOinSTSC = 0;
++ tmpOffset = (stco64 ? co64Box.getChunkOffset(stcoIndex) : stcoBox.getChunkOffset(stcoIndex));
++ }
++ BsetPart.time = (totaldur * 1000) / timescale;
++ totaldur += sttsEntry.sampleDelta;
++ sampleNo++;
++ if (sampleNo >= sttsEntry.sampleCount){
++ ++entryNo;
++ sampleNo = 0;
++ if (entryNo < sttsBox.getEntryCount()){sttsEntry = sttsBox.getSTTSEntry(entryNo);}
++ }
++
++ if (hasCTTS){
++ MP4::CTTSEntry cttsEntry = cttsBox.getCTTSEntry(cttsIndex);
++ cttsEntryRead++;
++ if (cttsEntryRead >= cttsEntry.sampleCount){
++ ++cttsIndex;
++ cttsEntryRead = 0;
+ }
++ BsetPart.timeOffset = (cttsEntry.sampleOffset * 1000) / timescale;
++ }else{
++ BsetPart.timeOffset = 0;
++ }
+
+- MP4::STSS stssBox = stblBox.getChild<MP4::STSS>();
+- MP4::STTS sttsBox = stblBox.getChild<MP4::STTS>();
+- MP4::STSZ stszBox = stblBox.getChild<MP4::STSZ>();
+- MP4::STCO stcoBox = stblBox.getChild<MP4::STCO>();
+- MP4::CO64 co64Box = stblBox.getChild<MP4::CO64>();
+- MP4::STSC stscBox = stblBox.getChild<MP4::STSC>();
+- MP4::CTTS cttsBox = stblBox.getChild<MP4::CTTS>(); // optional ctts box
+-
+- bool stco64 = co64Box.isType("co64");
+- bool hasCTTS = cttsBox.isType("ctts");
+-
+- uint64_t totaldur = 0; ///\todo note: set this to begin time
+- mp4PartBpos BsetPart;
+-
+- uint64_t entryNo = 0;
+- uint64_t sampleNo = 0;
+-
+- uint64_t stssIndex = 0;
+- uint64_t stcoIndex = 0;
+- uint64_t stscIndex = 0;
+- uint64_t cttsIndex = 0; // current ctts Index we are reading
+- uint64_t cttsEntryRead = 0; // current part of ctts we are reading
+-
+- uint64_t stssCount = stssBox.getEntryCount();
+- uint64_t stscCount = stscBox.getEntryCount();
+- uint64_t stszCount = stszBox.getSampleCount();
+- uint64_t stcoCount = (stco64 ? co64Box.getEntryCount() : stcoBox.getEntryCount());
+-
+- MP4::STTSEntry sttsEntry = sttsBox.getSTTSEntry(0);
+-
+- uint32_t fromSTCOinSTSC = 0;
+- uint64_t tmpOffset = (stco64 ? co64Box.getChunkOffset(0) : stcoBox.getChunkOffset(0));
+-
+- uint64_t nextFirstChunk = (stscCount > 1 ? stscBox.getSTSCEntry(1).firstChunk - 1 : stcoCount);
+-
+- for (uint64_t stszIndex = 0; stszIndex < stszCount; ++stszIndex){
+- if (stcoIndex >= nextFirstChunk){
+- ++stscIndex;
+- nextFirstChunk =
+- (stscIndex + 1 < stscCount ? stscBox.getSTSCEntry(stscIndex + 1).firstChunk - 1 : stcoCount);
+- }
+- BsetPart.keyframe = (meta.getType(tNumber) == "video" && stssIndex < stssCount &&
+- stszIndex + 1 == stssBox.getSampleNumber(stssIndex));
+- if (BsetPart.keyframe){++stssIndex;}
+- // in bpos set
+- BsetPart.stcoNr = stcoIndex;
+- // bpos = chunkoffset[samplenr] in stco
+- BsetPart.bpos = tmpOffset;
+- ++fromSTCOinSTSC;
+- if (fromSTCOinSTSC < stscBox.getSTSCEntry(stscIndex).samplesPerChunk){// as long as we are still in this chunk
+- tmpOffset += stszBox.getEntrySize(stszIndex);
+- }else{
+- ++stcoIndex;
+- fromSTCOinSTSC = 0;
+- tmpOffset = (stco64 ? co64Box.getChunkOffset(stcoIndex) : stcoBox.getChunkOffset(stcoIndex));
+- }
+- BsetPart.time = (totaldur * 1000) / timescale;
+- totaldur += sttsEntry.sampleDelta;
+- sampleNo++;
+- if (sampleNo >= sttsEntry.sampleCount){
+- ++entryNo;
+- sampleNo = 0;
+- if (entryNo < sttsBox.getEntryCount()){sttsEntry = sttsBox.getSTTSEntry(entryNo);}
+- }
+-
+- if (hasCTTS){
+- MP4::CTTSEntry cttsEntry = cttsBox.getCTTSEntry(cttsIndex);
+- cttsEntryRead++;
+- if (cttsEntryRead >= cttsEntry.sampleCount){
+- ++cttsIndex;
+- cttsEntryRead = 0;
+- }
+- BsetPart.timeOffset = (cttsEntry.sampleOffset * 1000) / timescale;
+- }else{
+- BsetPart.timeOffset = 0;
+- }
+-
+- if (sType == "tx3g"){
+- if (stszBox.getEntrySize(stszIndex) <= 2 && false){
+- FAIL_MSG("size <=2");
+- }else{
+- long long packSendSize = 0;
+- packSendSize = 24 + (BsetPart.timeOffset ? 17 : 0) + (BsetPart.bpos ? 15 : 0) + 19 +
+- stszBox.getEntrySize(stszIndex) + 11 - 2 + 19;
+- meta.update(BsetPart.time, BsetPart.timeOffset, tNumber,
+- stszBox.getEntrySize(stszIndex) - 2, BsetPart.bpos, true, packSendSize);
+- }
+- }else{
+- meta.update(BsetPart.time, BsetPart.timeOffset, tNumber,
+- stszBox.getEntrySize(stszIndex), BsetPart.bpos, BsetPart.keyframe);
+- }
++ if (sType == "tx3g"){
++ if (stszBox.getEntrySize(stszIndex) <= 2 && false){
++ FAIL_MSG("size <=2");
++ }else{
++ long long packSendSize = 0;
++ packSendSize = 24 + (BsetPart.timeOffset ? 17 : 0) + (BsetPart.bpos ? 15 : 0) + 19 +
++ stszBox.getEntrySize(stszIndex) + 11 - 2 + 19;
++ meta.update(BsetPart.time, BsetPart.timeOffset, tNumber,
++ stszBox.getEntrySize(stszIndex) - 2, BsetPart.bpos, true, packSendSize);
+ }
++ }else{
++ meta.update(BsetPart.time, BsetPart.timeOffset, tNumber,
++ stszBox.getEntrySize(stszIndex), BsetPart.bpos, BsetPart.keyframe);
+ }
+- continue;
+- }
+- if (!MP4::skipBox(inFile)){// moving on to next box
+- FAIL_MSG("Error in Skipping box, exiting");
+- return false;
+ }
+ }
+- clearerr(inFile);
+
+ // outputting dtsh file
+- M.toFile(config->getString("input") + ".dtsh");
++ std::string inUrl = config->getString("input");
++ if (inUrl.size() > 4 && inUrl.substr(0, 4) == "mp4:"){inUrl.erase(0, 4);}
++ if (inUrl != "-" && HTTP::URL(inUrl).isLocalPath()){
++ M.toFile(inUrl + ".dtsh");
++ }else{
++ INFO_MSG("Skipping header write, as the source is not a local file");
++ }
++ bps = 0;
++ std::set<size_t> tracks = M.getValidTracks();
++ for (std::set<size_t>::iterator it = tracks.begin(); it != tracks.end(); it++){bps += M.getBps(*it);}
+ return true;
+ }
+
+@@ -474,23 +513,48 @@ namespace Mist{
+ ++nextKeyNum;
+ }
+ }
+- if (fseeko(inFile, curPart.bpos, SEEK_SET)){
+- FAIL_MSG("seek unsuccessful @bpos %" PRIu64 ": %s", curPart.bpos, strerror(errno));
+- thisPacket.null();
+- return;
++ if (curPart.bpos < readPos || curPart.bpos > readPos + readBuffer.size() + 512*1024 + bps){
++ INFO_MSG("Buffer contains %" PRIu64 "-%" PRIu64 ", but we need %" PRIu64 "; seeking!", readPos, readPos + readBuffer.size(), curPart.bpos);
++ readBuffer.truncate(0);
++ if (!inFile.seek(curPart.bpos)){
++ FAIL_MSG("seek unsuccessful @bpos %" PRIu64 ": %s", curPart.bpos, strerror(errno));
++ thisPacket.null();
++ return;
++ }
++ readPos = inFile.getPos();
++ }else{
++ //If we have more than 5MiB buffered and are more than 5MiB into the buffer, shift the first 4MiB off the buffer.
++ //This prevents infinite growth of the read buffer for large files
++ if (readBuffer.size() >= 5*1024*1024 && curPart.bpos > readPos + 5*1024*1024 + bps){
++ readBuffer.shift(4*1024*1024);
++ readPos += 4*1024*1024;
++ }
+ }
+- if (curPart.size > malSize){
+- data = (char *)realloc(data, curPart.size);
+- malSize = curPart.size;
++
++ while (readPos+readBuffer.size() < curPart.bpos+curPart.size && inFile && keepRunning()){
++ inFile.readSome((curPart.bpos+curPart.size) - (readPos+readBuffer.size()), *this);
+ }
+- if (fread(data, curPart.size, 1, inFile) != 1){
+- FAIL_MSG("read unsuccessful at %ld", ftell(inFile));
+- thisPacket.null();
+- return;
++ if (readPos+readBuffer.size() < curPart.bpos+curPart.size){
++ FAIL_MSG("Read unsuccessful at %" PRIu64 ", seeking to retry...", readPos+readBuffer.size());
++ readBuffer.truncate(0);
++ if (!inFile.seek(curPart.bpos)){
++ FAIL_MSG("seek unsuccessful @bpos %" PRIu64 ": %s", curPart.bpos, strerror(errno));
++ thisPacket.null();
++ return;
++ }
++ readPos = inFile.getPos();
++ while (readPos+readBuffer.size() < curPart.bpos+curPart.size && inFile && keepRunning()){
++ inFile.readSome((curPart.bpos+curPart.size) - (readPos+readBuffer.size()), *this);
++ }
++ if (readPos+readBuffer.size() < curPart.bpos+curPart.size){
++ FAIL_MSG("Read retry unsuccessful at %" PRIu64 ", aborting", readPos+readBuffer.size());
++ thisPacket.null();
++ return;
++ }
+ }
+
+ if (M.getCodec(curPart.trackID) == "subtitle"){
+- unsigned int txtLen = Bit::btohs(data);
++ unsigned int txtLen = Bit::btohs(readBuffer + (curPart.bpos-readPos));
+ if (!txtLen && false){
+ curPart.index++;
+ return getNext(idx);
+@@ -499,14 +563,14 @@ namespace Mist{
+ thisPack.null();
+ thisPack["trackid"] = curPart.trackID;
+ thisPack["bpos"] = curPart.bpos; //(long long)fileSource.tellg();
+- thisPack["data"] = std::string(data + 2, txtLen);
++ thisPack["data"] = std::string(readBuffer + (curPart.bpos-readPos) + 2, txtLen);
+ thisPack["time"] = curPart.time;
+ if (curPart.duration){thisPack["duration"] = curPart.duration;}
+ thisPack["keyframe"] = true;
+ std::string tmpStr = thisPack.toNetPacked();
+ thisPacket.reInit(tmpStr.data(), tmpStr.size());
+ }else{
+- thisPacket.genericFill(curPart.time, curPart.offset, curPart.trackID, data, curPart.size, 0, isKeyframe);
++ thisPacket.genericFill(curPart.time, curPart.offset, curPart.trackID, readBuffer + (curPart.bpos-readPos), curPart.size, 0, isKeyframe);
+ }
+ thisTime = curPart.time;
+ thisIdx = curPart.trackID;
+diff --git a/src/input/input_mp4.h b/src/input/input_mp4.h
+index b5987d3b..4ab9f36e 100644
+--- a/src/input/input_mp4.h
++++ b/src/input/input_mp4.h
+@@ -1,5 +1,6 @@
+ #include "input.h"
+ #include <mist/dtsc.h>
++#include <mist/urireader.h>
+ #include <mist/mp4.h>
+ #include <mist/mp4_generic.h>
+ namespace Mist{
+@@ -70,10 +71,10 @@ namespace Mist{
+ bool stco64;
+ };
+
+- class inputMP4 : public Input{
++ class inputMP4 : public Input, public Util::DataCallback {
+ public:
+ inputMP4(Util::Config *cfg);
+- ~inputMP4();
++ void dataCallback(const char *ptr, size_t size);
+
+ protected:
+ // Private Functions
+@@ -85,7 +86,10 @@ namespace Mist{
+ void seek(uint64_t seekTime, size_t idx = INVALID_TRACK_ID);
+ void handleSeek(uint64_t seekTime, size_t idx);
+
+- FILE *inFile;
++ HTTP::URIReader inFile;
++ Util::ResizeablePointer readBuffer;
++ uint64_t readPos;
++ uint64_t bps;
+
+ mp4TrackHeader &headerData(size_t trackID);
+
+@@ -94,11 +98,6 @@ namespace Mist{
+
+ // remember last seeked keyframe;
+ std::map<size_t, uint32_t> nextKeyframe;
+-
+- // these next two variables keep a buffer for reading from filepointer inFile;
+- uint64_t malSize;
+- char *data; ///\todo rename this variable to a more sensible name, it is a temporary piece of
+- /// memory to read from files
+ };
+ }// namespace Mist
+
+--
+2.25.1
+
+
+From 2740d65a0f5715c42eb6de24cbf610ba6271b85a Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Wed, 17 Aug 2022 10:54:31 +0200
+Subject: [PATCH 17/38] Improved RTP timestamp logging, fixed bug related to
+ firstTime value
+
+---
+ lib/rtp.cpp | 7 ++++---
+ 1 file changed, 4 insertions(+), 3 deletions(-)
+
+diff --git a/lib/rtp.cpp b/lib/rtp.cpp
+index 3ed9b7c6..5a5ed829 100644
+--- a/lib/rtp.cpp
++++ b/lib/rtp.cpp
+@@ -877,7 +877,7 @@ namespace RTP{
+ if (!firstTime){
+ milliSync = Util::bootMS();
+ firstTime = pTime + 1;
+- INFO_MSG("RTP timestamp rollover expected in " PRETTY_PRINT_TIME,
++ INFO_MSG("RTP timestamp rollover for %" PRIu64 " (%s) expected in " PRETTY_PRINT_TIME, trackId, codec.c_str(),
+ PRETTY_ARG_TIME((0xFFFFFFFFul - firstTime) / multiplier / 1000));
+ }else{
+ if (recentWrap){
+@@ -886,14 +886,15 @@ namespace RTP{
+ }else{
+ if (prevTime > pTime && pTime < 0x40000000lu && prevTime > 0x80000000lu){
+ ++wrapArounds;
++ INFO_MSG("RTP timestamp rollover %" PRIu32 " for %" PRIu64 " (%s) happened; next should be in " PRETTY_PRINT_TIME, wrapArounds, trackId, codec.c_str(), PRETTY_ARG_TIME((0xFFFFFFFFul) / multiplier / 1000));
+ recentWrap = true;
+ }
+ }
+ }
+ // When there are B-frames, the firstTime can be higher than the current time
+ // causing msTime to become negative and thus overflow
+- if (firstTime > pTime + 1){
+- WARN_MSG("firstTime was higher than current packet time. Readjusting firsTime...");
++ if (!wrapArounds && firstTime > pTime + 1){
++ WARN_MSG("firstTime was higher than current packet time. Readjusting firstTime...");
+ firstTime = pTime + 1;
+ }
+ prevTime = pkt.getTimeStamp();
+--
+2.25.1
+
+
+From 2870ae1cedd202755bbe9d0e6808086d40656157 Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Wed, 24 Aug 2022 15:02:19 +0200
+Subject: [PATCH 18/38] =?UTF-8?q?SRT=20RAW=20fixup=20=F0=9F=A4=A6?=
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+---
+ src/input/input_tssrt.cpp | 1 -
+ 1 file changed, 1 deletion(-)
+
+diff --git a/src/input/input_tssrt.cpp b/src/input/input_tssrt.cpp
+index da69be29..4219ebbc 100644
+--- a/src/input/input_tssrt.cpp
++++ b/src/input/input_tssrt.cpp
+@@ -211,7 +211,6 @@ namespace Mist{
+ }
+ uint64_t packetTime = Util::bootMS();
+ thisPacket.genericFill(packetTime, 0, 1, rawBuffer, rawBuffer.size(), 0, 0);
+- bufferLivePacket(thisPacket);
+ lastRawPacket = packetTime;
+ rawBuffer.truncate(0);
+ return;
+--
+2.25.1
+
+
+From a9ddc377895f5feb898c162de0f6b0363cf94276 Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Thu, 25 Aug 2022 10:26:38 +0200
+Subject: [PATCH 19/38] =?UTF-8?q?Why,=20Haivision,=20why=3F=20=F0=9F=98=AD?=
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+---
+ lib/socket_srt.cpp | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+diff --git a/lib/socket_srt.cpp b/lib/socket_srt.cpp
+index 3e03bce4..6d1d5695 100644
+--- a/lib/socket_srt.cpp
++++ b/lib/socket_srt.cpp
+@@ -392,6 +392,10 @@ namespace Socket{
+ }
+ }
+ params["payloadsize"] = asString(chunkTransmitSize);
++ //This line forces the transmission type to live if unset.
++ //Live is actually the default, but not explicitly setting the option means
++ //that all other defaults do not get applied either, which is bad.
++ if (!params.count("transtype")){params["transtype"] = "live";}
+ }
+
+ int SRTConnection::preConfigureSocket(){
+--
+2.25.1
+
+
+From 8aceff951d3e320786bf33c9ff558a506df372c6 Mon Sep 17 00:00:00 2001
+From: Marco van Dijk <marco@stronk.rocks>
+Date: Mon, 29 Aug 2022 14:36:00 +0200
+Subject: [PATCH 20/38] Fix connections getting closed on interrupted system
+ call
+
+---
+ lib/socket.cpp | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/lib/socket.cpp b/lib/socket.cpp
+index 865c3be7..6d63af00 100644
+--- a/lib/socket.cpp
++++ b/lib/socket.cpp
+@@ -1032,6 +1032,7 @@ unsigned int Socket::Connection::iwrite(const void *buffer, int len){
+ case MBEDTLS_ERR_SSL_WANT_WRITE: return 0; break;
+ case MBEDTLS_ERR_SSL_WANT_READ: return 0; break;
+ case EWOULDBLOCK: return 0; break;
++ case EINTR: return 0; break;
+ default:
+ Error = true;
+ lastErr = strerror(errno);
+@@ -1071,6 +1072,7 @@ unsigned int Socket::Connection::iwrite(const void *buffer, int len){
+ if (r < 0){
+ switch (errno){
+ case EWOULDBLOCK: return 0; break;
++ case EINTR: return 0; break;
+ default:
+ Error = true;
+ lastErr = strerror(errno);
+--
+2.25.1
+
+
+From 54a46146c2527004a96b78fe8b7341eab4de6f62 Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Thu, 11 Aug 2022 13:17:13 +0200
+Subject: [PATCH 21/38] Fix TS inputs taking more and more memory over time in
+ some cases
+
+---
+ lib/ts_stream.cpp | 12 +++++++++---
+ 1 file changed, 9 insertions(+), 3 deletions(-)
+
+diff --git a/lib/ts_stream.cpp b/lib/ts_stream.cpp
+index 4ab20c59..8da72493 100644
+--- a/lib/ts_stream.cpp
++++ b/lib/ts_stream.cpp
+@@ -190,7 +190,8 @@ namespace TS{
+ uint32_t tid = newPack.getPID();
+ bool unitStart = newPack.getUnitStart();
+ static uint32_t wantPrev = 0;
+- bool wantTrack = ((wantPrev == tid) || (tid == 0 || newPack.isPMT(pmtTracks) || pidToCodec.count(tid)));
++ bool isData = pidToCodec.count(tid);
++ bool wantTrack = ((wantPrev == tid) || (tid == 0 || newPack.isPMT(pmtTracks) || isData));
+ if (!wantTrack){return;}
+ if (psCacheTid != tid || !psCache){
+ psCache = &(pesStreams[tid]);
+@@ -199,7 +200,7 @@ namespace TS{
+ if (unitStart || !psCache->empty()){
+ wantPrev = tid;
+ psCache->push_back(newPack);
+- if (unitStart){
++ if (unitStart && isData){
+ pesPositions[tid].push_back(bytePos);
+ ++(seenUnitStart[tid]);
+ }
+@@ -210,7 +211,7 @@ namespace TS{
+ if (tid == 0){return false;}
+ {
+ tthread::lock_guard<tthread::recursive_mutex> guard(tMutex);
+- return !pmtTracks.count(tid);
++ return pidToCodec.count(tid);
+ }
+ }
+
+@@ -280,6 +281,11 @@ namespace TS{
+ }
+
+ if (!pidToCodec.count(tid)){
++ pesStreams.erase(tid);
++ pesPositions.erase(tid);
++ seenUnitStart.erase(tid);
++ psCacheTid = 0;
++ psCache = 0;
+ return; // skip unknown codecs
+ }
+
+--
+2.25.1
+
+
+From c10d5a7ec1fdc71cc4af2329a9030b422735fd34 Mon Sep 17 00:00:00 2001
+From: Marco van Dijk <marco@stronk.rocks>
+Date: Wed, 16 Mar 2022 13:45:37 +0100
+Subject: [PATCH 22/38] onFail on a WS connection, send the error back using
+ the websocket request handler
+
+---
+ src/output/output_http_internal.cpp | 20 ++++++++++++++++++--
+ src/output/output_http_internal.h | 1 +
+ 2 files changed, 19 insertions(+), 2 deletions(-)
+
+diff --git a/src/output/output_http_internal.cpp b/src/output/output_http_internal.cpp
+index c8ca2e6c..97ca455f 100644
+--- a/src/output/output_http_internal.cpp
++++ b/src/output/output_http_internal.cpp
+@@ -36,6 +36,7 @@ namespace Mist{
+
+ OutHTTP::OutHTTP(Socket::Connection &conn) : HTTPOutput(conn){
+ stayConnected = false;
++ thisError = "";
+ // If this connection is a socket and not already connected to stdio, connect it to stdio.
+ if (myConn.getPureSocket() != -1 && myConn.getSocket() != STDIN_FILENO && myConn.getSocket() != STDOUT_FILENO){
+ std::string host = getConnectedHost();
+@@ -63,6 +64,11 @@ namespace Mist{
+ bool OutHTTP::listenMode(){return !(config->getString("ip").size());}
+
+ void OutHTTP::onFail(const std::string &msg, bool critical){
++ // If we are connected through WS, the websockethandler should return the error message
++ if (stayConnected){
++ thisError = msg;
++ return;
++ }
+ if (responded){
+ HTTPOutput::onFail(msg, critical);
+ return;
+@@ -78,7 +84,6 @@ namespace Mist{
+ return;
+ }
+ if (H.url.size() >= 3 && H.url.substr(H.url.size() - 3) == ".js"){
+- if (websocketHandler()){return;}
+ JSON::Value json_resp;
+ json_resp["error"] = "Could not retrieve stream. Sorry.";
+ json_resp["error_guru"] = msg;
+@@ -1149,12 +1154,23 @@ namespace Mist{
+ if (meta){meta.reloadReplacedPagesIfNeeded();}
+ if (newState != prevState || (newState == STRMSTAT_READY && M.getValidTracks() != prevTracks)){
+ if (newState == STRMSTAT_READY){
++ thisError = "";
+ reconnect();
+ prevTracks = M.getValidTracks();
+ }else{
+ disconnect();
+ }
+- JSON::Value resp = getStatusJSON(reqHost, useragent);
++ JSON::Value resp;
++ // Check if we have an error message set
++ if (thisError == ""){
++ resp = getStatusJSON(reqHost, useragent);
++ }else{
++ resp["error"] = "Could not retrieve stream. Sorry.";
++ resp["error_guru"] = thisError;
++ if (config->getString("nostreamtext") != ""){
++ resp["on_error"] = config->getString("nostreamtext");
++ }
++ }
+ if (currStreamName != streamName){
+ currStreamName = streamName;
+ snprintf(pageName, NAME_BUFFER_SIZE, SHM_STREAM_STATE, streamName.c_str());
+diff --git a/src/output/output_http_internal.h b/src/output/output_http_internal.h
+index 0f939e78..774eb186 100644
+--- a/src/output/output_http_internal.h
++++ b/src/output/output_http_internal.h
+@@ -21,6 +21,7 @@ namespace Mist{
+ private:
+ std::string origStreamName;
+ std::string mistPath;
++ std::string thisError;
+ };
+ }// namespace Mist
+
+--
+2.25.1
+
+
+From f418fed81c0c46fd338e238b558085f408456ed9 Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Thu, 24 Mar 2022 15:20:52 +0100
+Subject: [PATCH 23/38] Fix HLS output not setting "responded" value
+ appropriately
+
+---
+ src/output/output_hls.cpp | 8 ++++++++
+ 1 file changed, 8 insertions(+)
+
+diff --git a/src/output/output_hls.cpp b/src/output/output_hls.cpp
+index 19d083e6..ad810aa7 100644
+--- a/src/output/output_hls.cpp
++++ b/src/output/output_hls.cpp
+@@ -232,6 +232,7 @@ namespace Mist{
+ H.setCORSHeaders();
+ if (H.method == "OPTIONS" || H.method == "HEAD"){
+ H.SendResponse("200", "OK", myConn);
++ responded = true;
+ return;
+ }
+ H.SetBody("<?xml version=\"1.0\"?><!DOCTYPE cross-domain-policy SYSTEM "
+@@ -239,6 +240,7 @@ namespace Mist{
+ "cross-domain-policy.dtd\"><cross-domain-policy><allow-access-from domain=\"*\" "
+ "/><site-control permitted-cross-domain-policies=\"all\"/></cross-domain-policy>");
+ H.SendResponse("200", "OK", myConn);
++ responded = true;
+ return;
+ }// crossdomain.xml
+
+@@ -262,6 +264,7 @@ namespace Mist{
+ }
+ H.SetBody("");
+ H.SendResponse("200", "OK", myConn);
++ responded = true;
+ return;
+ }
+
+@@ -311,6 +314,7 @@ namespace Mist{
+ targetTime = HLS::getPartTargetTime(M, idx, mTrack, startTime, msn, part);
+ if (!targetTime){
+ H.SendResponse("404", "Partial fragment does not exist", myConn);
++ responded = true;
+ return;
+ }
+ startTime += part * HLS::partDurationMaxMs;
+@@ -372,6 +376,7 @@ namespace Mist{
+ "served.\n");
+ myConn.SendNow(H.BuildResponse("404", "Fragment out of range"));
+ WARN_MSG("Fragment @ %" PRIu64 " too old", startTime);
++ responded = true;
+ return;
+ }
+
+@@ -389,10 +394,12 @@ namespace Mist{
+ }
+ if (H.method == "OPTIONS" || H.method == "HEAD"){
+ H.SendResponse("200", "OK", myConn);
++ responded = true;
+ return;
+ }
+
+ H.StartResponse(H, myConn, VLCworkaround || config->getBool("nonchunked"));
++ responded = true;
+ // we assume whole fragments - but timestamps may be altered at will
+ contPAT = fragmentIndex; // PAT continuity counter
+ contPMT = fragmentIndex; // PMT continuity counter
+@@ -419,6 +426,7 @@ namespace Mist{
+ // Strip /hls/<streamname>/ from url
+ std::string url = H.url.substr(H.url.find('/', 5) + 1);
+ sendHlsManifest(url);
++ responded = true;
+ }
+ }
+
+--
+2.25.1
+
+
+From f3c003481d572c140643e9bbfb292942d8e2ed38 Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Mon, 26 Sep 2022 12:44:30 +0200
+Subject: [PATCH 24/38] Made FLV memory-based loader functions use const
+ pointers
+
+---
+ lib/flv_tag.cpp | 6 +++---
+ lib/flv_tag.h | 6 +++---
+ 2 files changed, 6 insertions(+), 6 deletions(-)
+
+diff --git a/lib/flv_tag.cpp b/lib/flv_tag.cpp
+index 9ccb6c41..780c6c5a 100644
+--- a/lib/flv_tag.cpp
++++ b/lib/flv_tag.cpp
+@@ -49,7 +49,7 @@ bool FLV::check_header(char *header){
+
+ /// Checks the first 3 bytes for the string "FLV". Implementing a basic FLV header check,
+ /// returning true if it is, false if not.
+-bool FLV::is_header(char *header){
++bool FLV::is_header(const char *header){
+ if (header[0] != 'F') return false;
+ if (header[1] != 'L') return false;
+ if (header[2] != 'V') return false;
+@@ -624,7 +624,7 @@ bool FLV::Tag::ChunkLoader(const RTMPStream::Chunk &O){
+ /// \param S The size of the data buffer.
+ /// \param P The current position in the data buffer. Will be updated to reflect new position.
+ /// \return True if count bytes are read succesfully, false otherwise.
+-bool FLV::Tag::MemReadUntil(char *buffer, unsigned int count, unsigned int &sofar, char *D,
++bool FLV::Tag::MemReadUntil(char *buffer, unsigned int count, unsigned int &sofar, const char *D,
+ unsigned int S, unsigned int &P){
+ if (sofar >= count){return true;}
+ int r = 0;
+@@ -646,7 +646,7 @@ bool FLV::Tag::MemReadUntil(char *buffer, unsigned int count, unsigned int &sofa
+ /// location of the data buffer. \param S The size of the data buffer. \param P The current position
+ /// in the data buffer. Will be updated to reflect new position. \return True if a whole tag is
+ /// succesfully read, false otherwise.
+-bool FLV::Tag::MemLoader(char *D, unsigned int S, unsigned int &P){
++bool FLV::Tag::MemLoader(const char *D, unsigned int S, unsigned int &P){
+ if (len < 15){len = 15;}
+ if (!checkBufferSize()){return false;}
+ if (done){
+diff --git a/lib/flv_tag.h b/lib/flv_tag.h
+index ca27f472..10f616b9 100644
+--- a/lib/flv_tag.h
++++ b/lib/flv_tag.h
+@@ -22,7 +22,7 @@ namespace FLV{
+
+ // functions
+ bool check_header(char *header); ///< Checks a FLV Header for validness.
+- bool is_header(char *header); ///< Checks the first 3 bytes for the string "FLV".
++ bool is_header(const char *header); ///< Checks the first 3 bytes for the string "FLV".
+
+ /// Helper function that can quickly skip through a file looking for a particular tag type
+ bool seekToTagType(FILE *f, uint8_t type);
+@@ -55,7 +55,7 @@ namespace FLV{
+ bool DTSCMetaInit(const DTSC::Meta &M, std::set<size_t> &selTracks);
+ void toMeta(DTSC::Meta &meta, AMF::Object &amf_storage);
+ void toMeta(DTSC::Meta &meta, AMF::Object &amf_storage, size_t &reTrack, const std::map<std::string, std::string> &targetParams);
+- bool MemLoader(char *D, unsigned int S, unsigned int &P);
++ bool MemLoader(const char *D, unsigned int S, unsigned int &P);
+ bool FileLoader(FILE *f);
+ unsigned int getTrackID();
+ char *getData();
+@@ -68,7 +68,7 @@ namespace FLV{
+ void setLen();
+ bool checkBufferSize();
+ // loader helper functions
+- bool MemReadUntil(char *buffer, unsigned int count, unsigned int &sofar, char *D,
++ bool MemReadUntil(char *buffer, unsigned int count, unsigned int &sofar, const char *D,
+ unsigned int S, unsigned int &P);
+ bool FileReadUntil(char *buffer, unsigned int count, unsigned int &sofar, FILE *f);
+ };
+--
+2.25.1
+
+
+From 3e85da2afd1b3d36290763eebf84dd66ad08636c Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Mon, 19 Sep 2022 17:26:15 +0200
+Subject: [PATCH 25/38] Match libav's RTMP receive window rollover behaviour
+
+---
+ lib/rtmpchunks.cpp | 5 +++++
+ src/output/output_rtmp.cpp | 2 +-
+ 2 files changed, 6 insertions(+), 1 deletion(-)
+
+diff --git a/lib/rtmpchunks.cpp b/lib/rtmpchunks.cpp
+index e7c151f0..5f9d33f0 100644
+--- a/lib/rtmpchunks.cpp
++++ b/lib/rtmpchunks.cpp
+@@ -475,6 +475,11 @@ bool RTMPStream::Chunk::Parse(Socket::Buffer &buffer){
+ }
+ lastrecv[cs_id] = *this;
+ RTMPStream::rec_cnt += i + real_len;
++ if (RTMPStream::rec_cnt >= 0xf0000000){
++ INFO_MSG("Resetting receive window due to impending rollover");
++ RTMPStream::rec_cnt -= 0xf0000000;
++ RTMPStream::rec_window_at = 0;
++ }
+ if (len_left == 0){
+ return true;
+ }else{
+diff --git a/src/output/output_rtmp.cpp b/src/output/output_rtmp.cpp
+index 16a400d0..23194f4a 100644
+--- a/src/output/output_rtmp.cpp
++++ b/src/output/output_rtmp.cpp
+@@ -1599,7 +1599,7 @@ namespace Mist{
+ while (next.Parse(inputBuffer)){
+
+ // send ACK if we received a whole window
+- if ((RTMPStream::rec_cnt - RTMPStream::rec_window_at > RTMPStream::rec_window_size) || Util::bootSecs() > lastAck+15){
++ if ((RTMPStream::rec_cnt - RTMPStream::rec_window_at > RTMPStream::rec_window_size / 4) || Util::bootSecs() > lastAck+15){
+ lastAck = Util::bootSecs();
+ RTMPStream::rec_window_at = RTMPStream::rec_cnt;
+ myConn.SendNow(RTMPStream::SendCTL(3, RTMPStream::rec_cnt)); // send ack (msg 3)
+--
+2.25.1
+
+
+From 074e7570284a094c12b00b88aa5cdf4a8f7fe5e4 Mon Sep 17 00:00:00 2001
+From: Ramkoemar <rbhoera@gmail.com>
+Date: Mon, 18 Oct 2021 14:29:13 +0200
+Subject: [PATCH 26/38] Sessions rework
+
+---
+ CMakeLists.txt | 11 +
+ lib/comms.cpp | 428 +++++---
+ lib/comms.h | 50 +-
+ lib/defines.h | 7 +-
+ src/controller/controller.cpp | 1 -
+ src/controller/controller_api.cpp | 1 +
+ src/controller/controller_statistics.cpp | 1185 ++++++++--------------
+ src/controller/controller_statistics.h | 61 +-
+ src/controller/controller_storage.cpp | 22 +-
+ src/input/input.cpp | 12 +-
+ src/input/input.h | 2 +-
+ src/input/input_rtsp.cpp | 5 +-
+ src/input/input_sdp.cpp | 5 +-
+ src/input/input_ts.cpp | 5 +-
+ src/input/input_tssrt.cpp | 2 +-
+ src/input/input_tssrt.h | 2 +-
+ src/output/output.cpp | 125 +--
+ src/output/output.h | 9 +-
+ src/output/output_cmaf.cpp | 2 +-
+ src/output/output_cmaf.h | 3 +-
+ src/output/output_http.cpp | 28 +-
+ src/output/output_tssrt.cpp | 2 +-
+ src/output/output_tssrt.h | 2 +-
+ src/output/output_webrtc.cpp | 2 +-
+ src/output/output_webrtc.h | 2 +-
+ src/process/process_exec.cpp | 4 +-
+ src/session.cpp | 367 +++++++
+ 27 files changed, 1192 insertions(+), 1153 deletions(-)
+ create mode 100644 src/session.cpp
+
+diff --git a/CMakeLists.txt b/CMakeLists.txt
+index 0e9bd644..6af16808 100644
+--- a/CMakeLists.txt
++++ b/CMakeLists.txt
+@@ -592,6 +592,17 @@ makeOutput(RTSP rtsp)#LTS
+ makeOutput(WAV wav)#LTS
+ makeOutput(SDP sdp http)
+
++add_executable(MistSession
++ ${BINARY_DIR}/mist/.headers
++ src/session.cpp
++)
++install(
++ TARGETS MistSession
++ DESTINATION bin
++)
++target_link_libraries(MistSession mist)
++
++
+ add_executable(MistProcFFMPEG
+ ${BINARY_DIR}/mist/.headers
+ src/process/process_ffmpeg.cpp
+diff --git a/lib/comms.cpp b/lib/comms.cpp
+index 108c686a..85f1e6d7 100644
+--- a/lib/comms.cpp
++++ b/lib/comms.cpp
+@@ -7,6 +7,7 @@
+ #include "timing.h"
+ #include <fcntl.h>
+ #include <string.h>
++#include "config.h"
+
+ namespace Comms{
+ Comms::Comms(){
+@@ -141,22 +142,197 @@ namespace Comms{
+ }
+ }
+
+- Statistics::Statistics() : Comms(){sem.open(SEM_STATISTICS, O_CREAT | O_RDWR, ACCESSPERMS, 1);}
++ Sessions::Sessions() : Connections(){sem.open(SEM_STATISTICS, O_CREAT | O_RDWR, ACCESSPERMS, 1);}
+
+- void Statistics::unload(){
+- if (index != INVALID_RECORD_INDEX){
+- setStatus(COMM_STATUS_DISCONNECT | getStatus());
++ void Sessions::reload(bool _master, bool reIssue){
++ Comms::reload(COMMS_STATISTICS, COMMS_STATISTICS_INITSIZE, _master, reIssue);
++ }
++
++ std::string Sessions::getSessId() const{return sessId.string(index);}
++ std::string Sessions::getSessId(size_t idx) const{return (master ? sessId.string(idx) : 0);}
++ void Sessions::setSessId(std::string _sid){sessId.set(_sid, index);}
++ void Sessions::setSessId(std::string _sid, size_t idx){
++ if (!master){return;}
++ sessId.set(_sid, idx);
++ }
++
++ bool Sessions::sessIdExists(std::string _sid){
++ for (size_t i = 0; i < recordCount(); i++){
++ if (getStatus(i) == COMM_STATUS_INVALID || (getStatus(i) & COMM_STATUS_DISCONNECT)){continue;}
++ if (getSessId(i) == _sid){
++ if (Util::Procs::isRunning(getPid(i))){
++ return true;
++ }
++ }
+ }
+- index = INVALID_RECORD_INDEX;
++ return false;
+ }
+
+- void Statistics::reload(bool _master, bool reIssue){
+- Comms::reload(COMMS_STATISTICS, COMMS_STATISTICS_INITSIZE, _master, reIssue);
++ void Sessions::addFields(){
++ Connections::addFields();
++ dataAccX.addField("sessid", RAX_STRING, 80);
+ }
+
+- void Statistics::addFields(){
++ void Sessions::nullFields(){
++ Connections::nullFields();
++ setSessId("");
++ }
++
++ void Sessions::fieldAccess(){
++ Connections::fieldAccess();
++ sessId = dataAccX.getFieldAccX("sessid");
++ }
++
++ Users::Users() : Comms(){}
++
++ Users::Users(const Users &rhs) : Comms(){
++ if (rhs){
++ reload(rhs.streamName, (size_t)rhs.getTrack());
++ if (*this){
++ setKeyNum(rhs.getKeyNum());
++ setTrack(rhs.getTrack());
++ }
++ }
++ }
++
++ void Users::reload(const std::string &_streamName, bool _master, bool reIssue){
++ streamName = _streamName;
++
++ char semName[NAME_BUFFER_SIZE];
++ snprintf(semName, NAME_BUFFER_SIZE, SEM_USERS, streamName.c_str());
++ sem.open(semName, O_CREAT | O_RDWR, ACCESSPERMS, 1);
++
++ char userPageName[NAME_BUFFER_SIZE];
++ snprintf(userPageName, NAME_BUFFER_SIZE, COMMS_USERS, streamName.c_str());
++
++ Comms::reload(userPageName, COMMS_USERS_INITSIZE, _master, reIssue);
++ }
++
++ void Users::addFields(){
++ Comms::addFields();
++ dataAccX.addField("track", RAX_64UINT);
++ dataAccX.addField("keynum", RAX_64UINT);
++ }
++
++ void Users::nullFields(){
++ Comms::nullFields();
++ setTrack(0);
++ setKeyNum(0);
++ }
++
++ void Users::fieldAccess(){
++ Comms::fieldAccess();
++ track = dataAccX.getFieldAccX("track");
++ keyNum = dataAccX.getFieldAccX("keynum");
++ }
++
++ void Users::reload(const std::string &_streamName, size_t idx, uint8_t initialState){
++ reload(_streamName);
++ if (dataPage){
++ setTrack(idx);
++ setKeyNum(0);
++ setStatus(initialState);
++ }
++ }
++
++ uint32_t Users::getTrack() const{return track.uint(index);}
++ uint32_t Users::getTrack(size_t idx) const{return (master ? track.uint(idx) : 0);}
++ void Users::setTrack(uint32_t _track){track.set(_track, index);}
++ void Users::setTrack(uint32_t _track, size_t idx){
++ if (!master){return;}
++ track.set(_track, idx);
++ }
++
++ size_t Users::getKeyNum() const{return keyNum.uint(index);}
++ size_t Users::getKeyNum(size_t idx) const{return (master ? keyNum.uint(idx) : 0);}
++ void Users::setKeyNum(size_t _keyNum){keyNum.set(_keyNum, index);}
++ void Users::setKeyNum(size_t _keyNum, size_t idx){
++ if (!master){return;}
++ keyNum.set(_keyNum, idx);
++ }
++
++ /// \brief Claims a spot on the connections page for the input/output which calls this function
++ /// Starts the MistSession binary for each session, which handles the statistics
++ /// and the USER_NEW and USER_END triggers
++ /// \param streamName: Name of the stream the input is providing or an output is making available to viewers
++ /// \param ip: IP address of the viewer which wants to access streamName. For inputs this value can be set to any value
++ /// \param sid: Session ID given by the player or randomly generated
++ /// \param protocol: Protocol currently in use for this connection
++ /// \param sessionMode: Determines how a viewer session is defined:
++ // If set to 0, all connections with the same viewer IP and stream name are bundled.
++ // If set to 1, all connections with the same viewer IP and player ID are bundled.
++ // If set to 2, all connections with the same player ID and stream name are bundled.
++ // If set to 3, all connections with the same viewer IP, player ID and stream name are bundled.
++ /// \param _master: If True, we are reading from this page. If False, we are writing (to our entry) on this page
++ /// \param reIssue: If True, claim a new entry on this page
++ void Connections::reload(std::string streamName, std::string ip, std::string sid, std::string protocol, std::string reqUrl, uint64_t sessionMode, bool _master, bool reIssue){
++ if (sessionMode == 0xFFFFFFFFFFFFFFFFull){
++ FAIL_MSG("The session mode was not initialised properly. Assuming default behaviour of bundling by viewer IP, stream name and player id");
++ sessionMode = SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID;
++ }
++ // Generate a unique session ID for each viewer, input or output
++ sessionId = generateSession(streamName, ip, sid, protocol, sessionMode);
++ if (protocol.size() >= 6 && protocol.substr(0, 6) == "INPUT:"){
++ sessionId = "I" + sessionId;
++ }else if (protocol.size() >= 7 && protocol.substr(0, 7) == "OUTPUT:"){
++ sessionId = "O" + sessionId;
++ }
++ char userPageName[NAME_BUFFER_SIZE];
++ snprintf(userPageName, NAME_BUFFER_SIZE, COMMS_SESSIONS, sessionId.c_str());
++ // Check if the page exists, if not, spawn new session process
++ if (!_master){
++ dataPage.init(userPageName, 0, false, false);
++ if (!dataPage){
++ pid_t thisPid;
++ std::deque<std::string> args;
++ args.push_back(Util::getMyPath() + "MistSession");
++ args.push_back(sessionId);
++ args.push_back("--sessionmode");
++ args.push_back(JSON::Value(sessionMode).asString());
++ args.push_back("--streamname");
++ args.push_back(streamName);
++ args.push_back("--ip");
++ args.push_back(ip);
++ args.push_back("--sid");
++ args.push_back(sid);
++ args.push_back("--protocol");
++ args.push_back(protocol);
++ args.push_back("--requrl");
++ args.push_back(reqUrl);
++ int err = fileno(stderr);
++ thisPid = Util::Procs::StartPiped(args, 0, 0, &err);
++ Util::Procs::forget(thisPid);
++ HIGH_MSG("Spawned new session executeable (pid %u) for sessionId '%s', corresponding to host %s and stream %s", thisPid, sessionId.c_str(), ip.c_str(), streamName.c_str());
++ }
++ }
++ // Open SEM_SESSION
++ if(!sem){
++ char semName[NAME_BUFFER_SIZE];
++ snprintf(semName, NAME_BUFFER_SIZE, SEM_SESSION, sessionId.c_str());
++ sem.open(semName, O_RDWR, ACCESSPERMS, 1);
++ }
++ Comms::reload(userPageName, COMMS_SESSIONS_INITSIZE, _master, reIssue);
++ VERYHIGH_MSG("Reloading connection. Claimed record %lu", index);
++ }
++
++ /// \brief Marks the data page as closed, so that we longer write any new data to is
++ void Connections::setExit(){
++ if (!master){return;}
++ dataAccX.setExit();
++ }
++
++ bool Connections::getExit(){
++ return dataAccX.isExit();
++ }
++
++ void Connections::unload(){
++ if (index != INVALID_RECORD_INDEX){
++ setStatus(COMM_STATUS_DISCONNECT | getStatus());
++ }
++ index = INVALID_RECORD_INDEX;
++ }
++ void Connections::addFields(){
+ Comms::addFields();
+- dataAccX.addField("sync", RAX_UINT);
+ dataAccX.addField("now", RAX_64UINT);
+ dataAccX.addField("time", RAX_64UINT);
+ dataAccX.addField("lastsecond", RAX_64UINT);
+@@ -165,15 +341,15 @@ namespace Comms{
+ dataAccX.addField("host", RAX_RAW, 16);
+ dataAccX.addField("stream", RAX_STRING, 100);
+ dataAccX.addField("connector", RAX_STRING, 20);
+- dataAccX.addField("crc", RAX_32UINT);
++ dataAccX.addField("tags", RAX_STRING, 512);
+ dataAccX.addField("pktcount", RAX_64UINT);
+ dataAccX.addField("pktloss", RAX_64UINT);
+ dataAccX.addField("pktretrans", RAX_64UINT);
+ }
+
+- void Statistics::nullFields(){
++ void Connections::nullFields(){
+ Comms::nullFields();
+- setCRC(0);
++ setTags("");
+ setConnector("");
+ setStream("");
+ setHost("");
+@@ -182,15 +358,13 @@ namespace Comms{
+ setLastSecond(0);
+ setTime(0);
+ setNow(0);
+- setSync(0);
+ setPacketCount(0);
+ setPacketLostCount(0);
+ setPacketRetransmitCount(0);
+ }
+
+- void Statistics::fieldAccess(){
++ void Connections::fieldAccess(){
+ Comms::fieldAccess();
+- sync = dataAccX.getFieldAccX("sync");
+ now = dataAccX.getFieldAccX("now");
+ time = dataAccX.getFieldAccX("time");
+ lastSecond = dataAccX.getFieldAccX("lastsecond");
+@@ -199,209 +373,159 @@ namespace Comms{
+ host = dataAccX.getFieldAccX("host");
+ stream = dataAccX.getFieldAccX("stream");
+ connector = dataAccX.getFieldAccX("connector");
+- crc = dataAccX.getFieldAccX("crc");
++ tags = dataAccX.getFieldAccX("tags");
+ pktcount = dataAccX.getFieldAccX("pktcount");
+ pktloss = dataAccX.getFieldAccX("pktloss");
+ pktretrans = dataAccX.getFieldAccX("pktretrans");
+ }
+
+- uint8_t Statistics::getSync() const{return sync.uint(index);}
+- uint8_t Statistics::getSync(size_t idx) const{return (master ? sync.uint(idx) : 0);}
+- void Statistics::setSync(uint8_t _sync){sync.set(_sync, index);}
+- void Statistics::setSync(uint8_t _sync, size_t idx){
+- if (!master){return;}
+- sync.set(_sync, idx);
+- }
+-
+- uint64_t Statistics::getNow() const{return now.uint(index);}
+- uint64_t Statistics::getNow(size_t idx) const{return (master ? now.uint(idx) : 0);}
+- void Statistics::setNow(uint64_t _now){now.set(_now, index);}
+- void Statistics::setNow(uint64_t _now, size_t idx){
++ uint64_t Connections::getNow() const{return now.uint(index);}
++ uint64_t Connections::getNow(size_t idx) const{return (master ? now.uint(idx) : 0);}
++ void Connections::setNow(uint64_t _now){now.set(_now, index);}
++ void Connections::setNow(uint64_t _now, size_t idx){
+ if (!master){return;}
+ now.set(_now, idx);
+ }
+
+- uint64_t Statistics::getTime() const{return time.uint(index);}
+- uint64_t Statistics::getTime(size_t idx) const{return (master ? time.uint(idx) : 0);}
+- void Statistics::setTime(uint64_t _time){time.set(_time, index);}
+- void Statistics::setTime(uint64_t _time, size_t idx){
++ uint64_t Connections::getTime() const{return time.uint(index);}
++ uint64_t Connections::getTime(size_t idx) const{return (master ? time.uint(idx) : 0);}
++ void Connections::setTime(uint64_t _time){time.set(_time, index);}
++ void Connections::setTime(uint64_t _time, size_t idx){
+ if (!master){return;}
+ time.set(_time, idx);
+ }
+
+- uint64_t Statistics::getLastSecond() const{return lastSecond.uint(index);}
+- uint64_t Statistics::getLastSecond(size_t idx) const{
++ uint64_t Connections::getLastSecond() const{return lastSecond.uint(index);}
++ uint64_t Connections::getLastSecond(size_t idx) const{
+ return (master ? lastSecond.uint(idx) : 0);
+ }
+- void Statistics::setLastSecond(uint64_t _lastSecond){lastSecond.set(_lastSecond, index);}
+- void Statistics::setLastSecond(uint64_t _lastSecond, size_t idx){
++ void Connections::setLastSecond(uint64_t _lastSecond){lastSecond.set(_lastSecond, index);}
++ void Connections::setLastSecond(uint64_t _lastSecond, size_t idx){
+ if (!master){return;}
+ lastSecond.set(_lastSecond, idx);
+ }
+
+- uint64_t Statistics::getDown() const{return down.uint(index);}
+- uint64_t Statistics::getDown(size_t idx) const{return (master ? down.uint(idx) : 0);}
+- void Statistics::setDown(uint64_t _down){down.set(_down, index);}
+- void Statistics::setDown(uint64_t _down, size_t idx){
++ uint64_t Connections::getDown() const{return down.uint(index);}
++ uint64_t Connections::getDown(size_t idx) const{return (master ? down.uint(idx) : 0);}
++ void Connections::setDown(uint64_t _down){down.set(_down, index);}
++ void Connections::setDown(uint64_t _down, size_t idx){
+ if (!master){return;}
+ down.set(_down, idx);
+ }
+
+- uint64_t Statistics::getUp() const{return up.uint(index);}
+- uint64_t Statistics::getUp(size_t idx) const{return (master ? up.uint(idx) : 0);}
+- void Statistics::setUp(uint64_t _up){up.set(_up, index);}
+- void Statistics::setUp(uint64_t _up, size_t idx){
++ uint64_t Connections::getUp() const{return up.uint(index);}
++ uint64_t Connections::getUp(size_t idx) const{return (master ? up.uint(idx) : 0);}
++ void Connections::setUp(uint64_t _up){up.set(_up, index);}
++ void Connections::setUp(uint64_t _up, size_t idx){
+ if (!master){return;}
+ up.set(_up, idx);
+ }
+
+- std::string Statistics::getHost() const{return std::string(host.ptr(index), 16);}
+- std::string Statistics::getHost(size_t idx) const{
++ std::string Connections::getHost() const{return std::string(host.ptr(index), 16);}
++ std::string Connections::getHost(size_t idx) const{
+ if (!master){return std::string((size_t)16, (char)'\000');}
+ return std::string(host.ptr(idx), 16);
+ }
+- void Statistics::setHost(std::string _host){host.set(_host, index);}
+- void Statistics::setHost(std::string _host, size_t idx){
++ void Connections::setHost(std::string _host){host.set(_host, index);}
++ void Connections::setHost(std::string _host, size_t idx){
+ if (!master){return;}
+ host.set(_host, idx);
+ }
+
+- std::string Statistics::getStream() const{return stream.string(index);}
+- std::string Statistics::getStream(size_t idx) const{return (master ? stream.string(idx) : "");}
+- void Statistics::setStream(std::string _stream){stream.set(_stream, index);}
+- void Statistics::setStream(std::string _stream, size_t idx){
++ std::string Connections::getStream() const{return stream.string(index);}
++ std::string Connections::getStream(size_t idx) const{return (master ? stream.string(idx) : "");}
++ void Connections::setStream(std::string _stream){stream.set(_stream, index);}
++ void Connections::setStream(std::string _stream, size_t idx){
+ if (!master){return;}
+ stream.set(_stream, idx);
+ }
+
+- std::string Statistics::getConnector() const{return connector.string(index);}
+- std::string Statistics::getConnector(size_t idx) const{
++ std::string Connections::getConnector() const{return connector.string(index);}
++ std::string Connections::getConnector(size_t idx) const{
+ return (master ? connector.string(idx) : "");
+ }
+- void Statistics::setConnector(std::string _connector){connector.set(_connector, index);}
+- void Statistics::setConnector(std::string _connector, size_t idx){
++ void Connections::setConnector(std::string _connector){connector.set(_connector, index);}
++ void Connections::setConnector(std::string _connector, size_t idx){
+ if (!master){return;}
+ connector.set(_connector, idx);
+ }
+
+- uint32_t Statistics::getCRC() const{return crc.uint(index);}
+- uint32_t Statistics::getCRC(size_t idx) const{return (master ? crc.uint(idx) : 0);}
+- void Statistics::setCRC(uint32_t _crc){crc.set(_crc, index);}
+- void Statistics::setCRC(uint32_t _crc, size_t idx){
++ bool Connections::hasConnector(size_t idx, std::string protocol){
++ std::stringstream sstream(connector.string(idx));
++ std::string _conn;
++ while (std::getline(sstream, _conn, ',')){
++ if (_conn == protocol){
++ return true;
++ }
++ }
++ return false;
++ }
++
++ std::string Connections::getTags() const{return tags.string(index);}
++ std::string Connections::getTags(size_t idx) const{return (master ? tags.string(idx) : 0);}
++ void Connections::setTags(std::string _sid){tags.set(_sid, index);}
++ void Connections::setTags(std::string _sid, size_t idx){
+ if (!master){return;}
+- crc.set(_crc, idx);
++ tags.set(_sid, idx);
+ }
+
+- uint64_t Statistics::getPacketCount() const{return pktcount.uint(index);}
+- uint64_t Statistics::getPacketCount(size_t idx) const{
++ uint64_t Connections::getPacketCount() const{return pktcount.uint(index);}
++ uint64_t Connections::getPacketCount(size_t idx) const{
+ return (master ? pktcount.uint(idx) : 0);
+ }
+- void Statistics::setPacketCount(uint64_t _count){pktcount.set(_count, index);}
+- void Statistics::setPacketCount(uint64_t _count, size_t idx){
++ void Connections::setPacketCount(uint64_t _count){pktcount.set(_count, index);}
++ void Connections::setPacketCount(uint64_t _count, size_t idx){
+ if (!master){return;}
+ pktcount.set(_count, idx);
+ }
+
+- uint64_t Statistics::getPacketLostCount() const{return pktloss.uint(index);}
+- uint64_t Statistics::getPacketLostCount(size_t idx) const{
++ uint64_t Connections::getPacketLostCount() const{return pktloss.uint(index);}
++ uint64_t Connections::getPacketLostCount(size_t idx) const{
+ return (master ? pktloss.uint(idx) : 0);
+ }
+- void Statistics::setPacketLostCount(uint64_t _lost){pktloss.set(_lost, index);}
+- void Statistics::setPacketLostCount(uint64_t _lost, size_t idx){
++ void Connections::setPacketLostCount(uint64_t _lost){pktloss.set(_lost, index);}
++ void Connections::setPacketLostCount(uint64_t _lost, size_t idx){
+ if (!master){return;}
+ pktloss.set(_lost, idx);
+ }
+
+- uint64_t Statistics::getPacketRetransmitCount() const{return pktretrans.uint(index);}
+- uint64_t Statistics::getPacketRetransmitCount(size_t idx) const{
++ uint64_t Connections::getPacketRetransmitCount() const{return pktretrans.uint(index);}
++ uint64_t Connections::getPacketRetransmitCount(size_t idx) const{
+ return (master ? pktretrans.uint(idx) : 0);
+ }
+- void Statistics::setPacketRetransmitCount(uint64_t _retrans){pktretrans.set(_retrans, index);}
+- void Statistics::setPacketRetransmitCount(uint64_t _retrans, size_t idx){
++ void Connections::setPacketRetransmitCount(uint64_t _retrans){pktretrans.set(_retrans, index);}
++ void Connections::setPacketRetransmitCount(uint64_t _retrans, size_t idx){
+ if (!master){return;}
+ pktretrans.set(_retrans, idx);
+ }
+
+- std::string Statistics::getSessId() const{return getSessId(index);}
+-
+- std::string Statistics::getSessId(size_t idx) const{
+- char res[140];
+- memset(res, 0, 140);
+- std::string tmp = host.string(idx);
+- memcpy(res, tmp.c_str(), (tmp.size() > 16 ? 16 : tmp.size()));
+- tmp = stream.string(idx);
+- memcpy(res + 16, tmp.c_str(), (tmp.size() > 100 ? 100 : tmp.size()));
+- tmp = connector.string(idx);
+- memcpy(res + 116, tmp.c_str(), (tmp.size() > 20 ? 20 : tmp.size()));
+- Bit::htobl(res + 136, crc.uint(idx));
+- return Secure::md5(res, 140);
+- }
+-
+- Users::Users() : Comms(){}
+-
+- Users::Users(const Users &rhs) : Comms(){
+- if (rhs){
+- reload(rhs.streamName, (size_t)rhs.getTrack());
+- if (*this){
+- setKeyNum(rhs.getKeyNum());
+- setTrack(rhs.getTrack());
+- }
++ /// \brief Generates a session ID which is unique per viewer
++ /// \return generated session ID as string
++ std::string Connections::generateSession(std::string streamName, std::string ip, std::string sid, std::string connector, uint64_t sessionMode){
++ std::string concat;
++ // First bit defines whether to include stream name
++ if (sessionMode > 7){
++ concat += streamName;
++ sessionMode -= 8;
+ }
+- }
+-
+- void Users::reload(const std::string &_streamName, bool _master, bool reIssue){
+- streamName = _streamName;
+-
+- char semName[NAME_BUFFER_SIZE];
+- snprintf(semName, NAME_BUFFER_SIZE, SEM_USERS, streamName.c_str());
+- sem.open(semName, O_CREAT | O_RDWR, ACCESSPERMS, 1);
+-
+- char userPageName[NAME_BUFFER_SIZE];
+- snprintf(userPageName, NAME_BUFFER_SIZE, COMMS_USERS, streamName.c_str());
+-
+- Comms::reload(userPageName, COMMS_USERS_INITSIZE, _master, reIssue);
+- }
+-
+- void Users::addFields(){
+- Comms::addFields();
+- dataAccX.addField("track", RAX_64UINT);
+- dataAccX.addField("keynum", RAX_64UINT);
+- }
+-
+- void Users::nullFields(){
+- Comms::nullFields();
+- setTrack(0);
+- setKeyNum(0);
+- }
+-
+- void Users::fieldAccess(){
+- Comms::fieldAccess();
+- track = dataAccX.getFieldAccX("track");
+- keyNum = dataAccX.getFieldAccX("keynum");
+- }
+-
+- void Users::reload(const std::string &_streamName, size_t idx, uint8_t initialState){
+- reload(_streamName);
+- if (dataPage){
+- setTrack(idx);
+- setKeyNum(0);
+- setStatus(initialState);
++ // Second bit defines whether to include viewer ip
++ if (sessionMode > 3){
++ concat += ip;
++ sessionMode -= 4;
+ }
+- }
+-
+- uint32_t Users::getTrack() const{return track.uint(index);}
+- uint32_t Users::getTrack(size_t idx) const{return (master ? track.uint(idx) : 0);}
+- void Users::setTrack(uint32_t _track){track.set(_track, index);}
+- void Users::setTrack(uint32_t _track, size_t idx){
+- if (!master){return;}
+- track.set(_track, idx);
+- }
+-
+- size_t Users::getKeyNum() const{return keyNum.uint(index);}
+- size_t Users::getKeyNum(size_t idx) const{return (master ? keyNum.uint(idx) : 0);}
+- void Users::setKeyNum(size_t _keyNum){keyNum.set(_keyNum, index);}
+- void Users::setKeyNum(size_t _keyNum, size_t idx){
+- if (!master){return;}
+- keyNum.set(_keyNum, idx);
++ // Third bit defines whether to include player ip
++ if (sessionMode > 1){
++ concat += sid;
++ sessionMode -= 2;
++ }
++ // Fourth bit defines whether to include protocol
++ if (sessionMode == 1){
++ concat += connector;
++ sessionMode = 0;
++ }
++ if (sessionMode > 0){
++ WARN_MSG("Could not resolve session mode of value %lu", sessionMode);
++ }
++ return Secure::sha256(concat.c_str(), concat.length());
+ }
+ }// namespace Comms
+diff --git a/lib/comms.h b/lib/comms.h
+index 9f459422..9a5c0ea9 100644
+--- a/lib/comms.h
++++ b/lib/comms.h
+@@ -64,21 +64,21 @@ namespace Comms{
+ Util::FieldAccX pid;
+ };
+
+- class Statistics : public Comms{
++ class Connections : public Comms{
+ public:
+- Statistics();
+- operator bool() const{return dataPage.mapped && (master || index != INVALID_RECORD_INDEX);}
++ void reload(std::string streamName, std::string ip, std::string sid, std::string protocol, std::string reqUrl, uint64_t sessionMode, bool _master = false, bool reIssue = false);
+ void unload();
+- void reload(bool _master = false, bool reIssue = false);
++ operator bool() const{return dataPage.mapped && (master || index != INVALID_RECORD_INDEX);}
++ std::string generateSession(std::string streamName, std::string ip, std::string sid, std::string connector, uint64_t sessionMode);
++ std::string sessionId;
++
++ void setExit();
++ bool getExit();
++
+ virtual void addFields();
+ virtual void nullFields();
+ virtual void fieldAccess();
+
+- uint8_t getSync() const;
+- uint8_t getSync(size_t idx) const;
+- void setSync(uint8_t _sync);
+- void setSync(uint8_t _sync, size_t idx);
+-
+ uint64_t getNow() const;
+ uint64_t getNow(size_t idx) const;
+ void setNow(uint64_t _now);
+@@ -118,11 +118,12 @@ namespace Comms{
+ std::string getConnector(size_t idx) const;
+ void setConnector(std::string _connector);
+ void setConnector(std::string _connector, size_t idx);
++ bool hasConnector(size_t idx, std::string protocol);
+
+- uint32_t getCRC() const;
+- uint32_t getCRC(size_t idx) const;
+- void setCRC(uint32_t _crc);
+- void setCRC(uint32_t _crc, size_t idx);
++ std::string getTags() const;
++ std::string getTags(size_t idx) const;
++ void setTags(std::string _sid);
++ void setTags(std::string _sid, size_t idx);
+
+ uint64_t getPacketCount() const;
+ uint64_t getPacketCount(size_t idx) const;
+@@ -139,11 +140,7 @@ namespace Comms{
+ void setPacketRetransmitCount(uint64_t _retransmit);
+ void setPacketRetransmitCount(uint64_t _retransmit, size_t idx);
+
+- std::string getSessId() const;
+- std::string getSessId(size_t index) const;
+-
+- private:
+- Util::FieldAccX sync;
++ protected:
+ Util::FieldAccX now;
+ Util::FieldAccX time;
+ Util::FieldAccX lastSecond;
+@@ -152,7 +149,8 @@ namespace Comms{
+ Util::FieldAccX host;
+ Util::FieldAccX stream;
+ Util::FieldAccX connector;
+- Util::FieldAccX crc;
++ Util::FieldAccX sessId;
++ Util::FieldAccX tags;
+ Util::FieldAccX pktcount;
+ Util::FieldAccX pktloss;
+ Util::FieldAccX pktretrans;
+@@ -186,4 +184,18 @@ namespace Comms{
+ Util::FieldAccX track;
+ Util::FieldAccX keyNum;
+ };
++
++ class Sessions : public Connections{
++ public:
++ Sessions();
++ void reload(bool _master = false, bool reIssue = false);
++ std::string getSessId() const;
++ std::string getSessId(size_t idx) const;
++ void setSessId(std::string _sid);
++ void setSessId(std::string _sid, size_t idx);
++ bool sessIdExists(std::string _sid);
++ virtual void addFields();
++ virtual void nullFields();
++ virtual void fieldAccess();
++ };
+ }// namespace Comms
+diff --git a/lib/defines.h b/lib/defines.h
+index 10c4b9b1..203b75db 100644
+--- a/lib/defines.h
++++ b/lib/defines.h
+@@ -196,11 +196,14 @@ static inline void show_stackframe(){}
+ #define TRACK_PAGE_RECORDSIZE 36
+
+ #define COMMS_STATISTICS "MstStat"
+-#define COMMS_STATISTICS_INITSIZE 8 * 1024 * 1024
++#define COMMS_STATISTICS_INITSIZE 16 * 1024 * 1024
+
+ #define COMMS_USERS "MstUser%s" //%s stream name
+ #define COMMS_USERS_INITSIZE 512 * 1024
+
++#define COMMS_SESSIONS "MstSession%s"
++#define COMMS_SESSIONS_INITSIZE 8 * 1024 * 1024
++
+ #define SEM_STATISTICS "/MstStat"
+ #define SEM_USERS "/MstUser%s" //%s stream name
+
+@@ -226,7 +229,9 @@ static inline void show_stackframe(){}
+ #define SEM_LIVE "/MstLIVE%s" //%s stream name
+ #define SEM_INPUT "/MstInpt%s" //%s stream name
+ #define SEM_TRACKLIST "/MstTRKS%s" //%s stream name
++#define SEM_SESSION "MstSess%s"
+ #define SEM_SESSCACHE "/MstSessCacheLock"
++#define SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID 14
+ #define SHM_CAPA "MstCapa"
+ #define SHM_PROTO "MstProt"
+ #define SHM_PROXY "MstProx"
+diff --git a/src/controller/controller.cpp b/src/controller/controller.cpp
+index a0ff2ee8..9e17ca6b 100644
+--- a/src/controller/controller.cpp
++++ b/src/controller/controller.cpp
+@@ -306,7 +306,6 @@ int main_loop(int argc, char **argv){
+ if (Controller::Storage["config"].isMember("accesslog")){
+ Controller::conf.getOption("accesslog", true)[0u] = Controller::Storage["config"]["accesslog"];
+ }
+- Controller::maxConnsPerIP = Controller::conf.getInteger("maxconnsperip");
+ Controller::Storage["config"]["prometheus"] = Controller::conf.getString("prometheus");
+ Controller::Storage["config"]["accesslog"] = Controller::conf.getString("accesslog");
+ Controller::normalizeTrustedProxies(Controller::Storage["config"]["trustedproxy"]);
+diff --git a/src/controller/controller_api.cpp b/src/controller/controller_api.cpp
+index 4d94bb11..02491065 100644
+--- a/src/controller/controller_api.cpp
++++ b/src/controller/controller_api.cpp
+@@ -594,6 +594,7 @@ void Controller::handleAPICommands(JSON::Value &Request, JSON::Value &Response){
+ out["prometheus"] = in["prometheus"];
+ Controller::prometheus = out["prometheus"].asStringRef();
+ }
++ if (in.isMember("sessionMode")){out["sessionMode"] = in["sessionMode"];}
+ if (in.isMember("defaultStream")){out["defaultStream"] = in["defaultStream"];}
+ if (in.isMember("location") && in["location"].isObject()){
+ out["location"]["lat"] = in["location"]["lat"].asDouble();
+diff --git a/src/controller/controller_statistics.cpp b/src/controller/controller_statistics.cpp
+index c5b7f643..6e51f4ee 100644
+--- a/src/controller/controller_statistics.cpp
++++ b/src/controller/controller_statistics.cpp
+@@ -15,6 +15,7 @@
+ #include <mist/url.h>
+ #include <sys/statvfs.h> //for fstatvfs
+ #include <mist/triggers.h>
++#include <signal.h>
+
+ #ifndef KILL_ON_EXIT
+ #define KILL_ON_EXIT false
+@@ -30,7 +31,6 @@
+ #define STAT_CLI_UP 64
+ #define STAT_CLI_BPS_DOWN 128
+ #define STAT_CLI_BPS_UP 256
+-#define STAT_CLI_CRC 512
+ #define STAT_CLI_SESSID 1024
+ #define STAT_CLI_PKTCOUNT 2048
+ #define STAT_CLI_PKTLOST 4096
+@@ -46,59 +46,18 @@
+ #define STAT_TOT_PERCRETRANS 64
+ #define STAT_TOT_ALL 0xFF
+
+-#define COUNTABLE_BYTES 128 * 1024
+-
+-std::map<Controller::sessIndex, Controller::statSession> Controller::sessions; ///< list of sessions that have statistics data available
+-std::map<unsigned long, Controller::sessIndex> Controller::connToSession; ///< Map of socket IDs to session info.
++// Mapping of sessId -> session statistics
++std::map<std::string, Controller::statSession> sessions;
+
+ std::map<std::string, Controller::triggerLog> Controller::triggerStats; ///< Holds prometheus stats for trigger executions
+ bool Controller::killOnExit = KILL_ON_EXIT;
+ tthread::mutex Controller::statsMutex;
+-unsigned int Controller::maxConnsPerIP = 0;
+ uint64_t Controller::statDropoff = 0;
+ static uint64_t cpu_use = 0;
+
+ char noBWCountMatches[1717];
+ uint64_t bwLimit = 128 * 1024 * 1024; // gigabit default limit
+
+-/// Session cache shared memory page
+-IPC::sharedPage *shmSessions = 0;
+-/// Lock for the session cache shared memory page
+-IPC::semaphore *cacheLock = 0;
+-
+-/// Convert bandwidth config into memory format
+-void Controller::updateBandwidthConfig(){
+- size_t offset = 0;
+- bwLimit = 128 * 1024 * 1024; // gigabit default limit
+- memset(noBWCountMatches, 0, 1717);
+- if (Storage.isMember("bandwidth")){
+- if (Storage["bandwidth"].isMember("limit")){bwLimit = Storage["bandwidth"]["limit"].asInt();}
+- if (Storage["bandwidth"].isMember("exceptions")){
+- jsonForEach(Storage["bandwidth"]["exceptions"], j){
+- std::string newbins = Socket::getBinForms(j->asStringRef());
+- if (offset + newbins.size() < 1700){
+- memcpy(noBWCountMatches + offset, newbins.data(), newbins.size());
+- offset += newbins.size();
+- }
+- }
+- }
+- }
+- //Localhost is always excepted from counts
+- {
+- std::string newbins = Socket::getBinForms("::1");
+- if (offset + newbins.size() < 1700){
+- memcpy(noBWCountMatches + offset, newbins.data(), newbins.size());
+- offset += newbins.size();
+- }
+- }
+- {
+- std::string newbins = Socket::getBinForms("127.0.0.1/8");
+- if (offset + newbins.size() < 1700){
+- memcpy(noBWCountMatches + offset, newbins.data(), newbins.size());
+- offset += newbins.size();
+- }
+- }
+-}
+
+ // For server-wide totals. Local to this file only.
+ struct streamTotals{
+@@ -116,11 +75,30 @@ struct streamTotals{
+ uint64_t packLoss;
+ uint64_t packRetrans;
+ };
++
++Comms::Sessions statComm;
++bool statCommActive = false;
++// Global server wide statistics
++static uint64_t servUpBytes = 0;
++static uint64_t servDownBytes = 0;
++static uint64_t servUpOtherBytes = 0;
++static uint64_t servDownOtherBytes = 0;
++static uint64_t servInputs = 0;
++static uint64_t servOutputs = 0;
++static uint64_t servViewers = 0;
++static uint64_t servSeconds = 0;
++static uint64_t servPackSent = 0;
++static uint64_t servPackLoss = 0;
++static uint64_t servPackRetrans = 0;
++// Total time watched for all sessions which are no longer active
++static uint64_t viewSecondsTotal = 0;
++// Mapping of streamName -> summary of stream-wide statistics
+ static std::map<std::string, struct streamTotals> streamStats;
+
+-static void createEmptyStatsIfNeeded(const std::string & strm){
+- if (streamStats.count(strm)){return;}
+- streamTotals & sT = streamStats[strm];
++// If sessId does not exist yet in streamStats, create and init an entry for it
++static void createEmptyStatsIfNeeded(const std::string & sessId){
++ if (streamStats.count(sessId)){return;}
++ streamTotals & sT = streamStats[sessId];
+ sT.upBytes = 0;
+ sT.downBytes = 0;
+ sT.inputs = 0;
+@@ -136,68 +114,38 @@ static void createEmptyStatsIfNeeded(const std::string & strm){
+ sT.packRetrans = 0;
+ }
+
+-
+-static uint64_t servUpBytes = 0;
+-static uint64_t servDownBytes = 0;
+-static uint64_t servUpOtherBytes = 0;
+-static uint64_t servDownOtherBytes = 0;
+-static uint64_t servInputs = 0;
+-static uint64_t servOutputs = 0;
+-static uint64_t servViewers = 0;
+-static uint64_t servSeconds = 0;
+-static uint64_t servPackSent = 0;
+-static uint64_t servPackLoss = 0;
+-static uint64_t servPackRetrans = 0;
+-
+-Controller::sessIndex::sessIndex(){
+- crc = 0;
+-}
+-
+-/// Initializes a sessIndex from a statistics object + index, converting binary format IP addresses
+-/// into strings. This extracts the host, stream name, connector and crc field, ignoring everything
+-/// else.
+-Controller::sessIndex::sessIndex(const Comms::Statistics &statComm, size_t id){
+- Socket::hostBytesToStr(statComm.getHost(id).data(), 16, host);
+- streamName = statComm.getStream(id);
+- connector = statComm.getConnector(id);
+- crc = statComm.getCRC(id);
+- ID = statComm.getSessId(id);
+-}
+-
+-std::string Controller::sessIndex::toStr(){
+- std::stringstream s;
+- s << ID << "(" << host << " " << crc << " " << streamName << " " << connector << ")";
+- return s.str();
+-}
+-
+-bool Controller::sessIndex::operator==(const Controller::sessIndex &b) const{
+- return (host == b.host && crc == b.crc && streamName == b.streamName && connector == b.connector);
+-}
+-
+-bool Controller::sessIndex::operator!=(const Controller::sessIndex &b) const{
+- return !(*this == b);
+-}
+-
+-bool Controller::sessIndex::operator>(const Controller::sessIndex &b) const{
+- return host > b.host ||
+- (host == b.host &&
+- (crc > b.crc || (crc == b.crc && (streamName > b.streamName ||
+- (streamName == b.streamName && connector > b.connector)))));
+-}
+-
+-bool Controller::sessIndex::operator<(const Controller::sessIndex &b) const{
+- return host < b.host ||
+- (host == b.host &&
+- (crc < b.crc || (crc == b.crc && (streamName < b.streamName ||
+- (streamName == b.streamName && connector < b.connector)))));
+-}
+-
+-bool Controller::sessIndex::operator<=(const Controller::sessIndex &b) const{
+- return !(*this > b);
+-}
+-
+-bool Controller::sessIndex::operator>=(const Controller::sessIndex &b) const{
+- return !(*this < b);
++/// Convert bandwidth config into memory format
++void Controller::updateBandwidthConfig(){
++ size_t offset = 0;
++ bwLimit = 128 * 1024 * 1024; // gigabit default limit
++ memset(noBWCountMatches, 0, 1717);
++ if (Storage.isMember("bandwidth")){
++ if (Storage["bandwidth"].isMember("limit")){bwLimit = Storage["bandwidth"]["limit"].asInt();}
++ if (Storage["bandwidth"].isMember("exceptions")){
++ jsonForEach(Storage["bandwidth"]["exceptions"], j){
++ std::string newbins = Socket::getBinForms(j->asStringRef());
++ if (offset + newbins.size() < 1700){
++ memcpy(noBWCountMatches + offset, newbins.data(), newbins.size());
++ offset += newbins.size();
++ }
++ }
++ }
++ }
++ //Localhost is always excepted from counts
++ {
++ std::string newbins = Socket::getBinForms("::1");
++ if (offset + newbins.size() < 1700){
++ memcpy(noBWCountMatches + offset, newbins.data(), newbins.size());
++ offset += newbins.size();
++ }
++ }
++ {
++ std::string newbins = Socket::getBinForms("127.0.0.1/8");
++ if (offset + newbins.size() < 1700){
++ memcpy(noBWCountMatches + offset, newbins.data(), newbins.size());
++ offset += newbins.size();
++ }
++ }
+ }
+
+ /// This function is ran whenever a stream becomes active.
+@@ -211,9 +159,6 @@ void Controller::streamStopped(std::string stream){
+ INFO_MSG("Stream %s became inactive", stream.c_str());
+ }
+
+-Comms::Statistics statComm;
+-bool statCommActive = false;
+-
+ /// Invalidates all current sessions for the given streamname
+ /// Updates the session cache, afterwards.
+ void Controller::sessions_invalidate(const std::string &streamname){
+@@ -221,18 +166,17 @@ void Controller::sessions_invalidate(const std::string &streamname){
+ FAIL_MSG("In shutdown procedure - cannot invalidate sessions.");
+ return;
+ }
+- unsigned int invalidated = 0;
+ unsigned int sessCount = 0;
+- tthread::lock_guard<tthread::mutex> guard(statsMutex);
+- for (std::map<sessIndex, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+- if (it->first.streamName == streamname){
++ // Find all matching streams in statComm
++ for (size_t i = 0; i < statComm.recordCount(); i++){
++ if (statComm.getStatus(i) == COMM_STATUS_INVALID || (statComm.getStatus(i) & COMM_STATUS_DISCONNECT)){continue;}
++ if (statComm.getStream(i) == streamname){
+ sessCount++;
+- invalidated += it->second.invalidate();
++ // Re-trigger USER_NEW trigger for this session
++ kill(statComm.getPid(i), SIGUSR1);
+ }
+ }
+- Controller::writeSessionCache();
+- INFO_MSG("Invalidated %u connections in %u sessions for stream %s", invalidated, sessCount,
+- streamname.c_str());
++ INFO_MSG("Invalidated %u session(s) for stream %s", sessCount, streamname.c_str());
+ }
+
+ /// Shuts down all current sessions for the given streamname
+@@ -256,18 +200,8 @@ void Controller::sessId_shutdown(const std::string &sessId){
+ FAIL_MSG("In controller shutdown procedure - cannot shutdown sessions.");
+ return;
+ }
+- unsigned int murdered = 0;
+- unsigned int sessCount = 0;
+- tthread::lock_guard<tthread::mutex> guard(statsMutex);
+- for (std::map<sessIndex, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+- if (it->first.ID == sessId){
+- sessCount++;
+- murdered += it->second.kill();
+- break;
+- }
+- }
+- Controller::writeSessionCache();
+- INFO_MSG("Shut down %u connections in %u session(s) for ID %s", murdered, sessCount, sessId.c_str());
++ killConnections(sessId);
++ INFO_MSG("Shut down session with session ID %s", sessId.c_str());
+ }
+
+ /// Tags the given session
+@@ -277,8 +211,8 @@ void Controller::sessId_tag(const std::string &sessId, const std::string &tag){
+ return;
+ }
+ tthread::lock_guard<tthread::mutex> guard(statsMutex);
+- for (std::map<sessIndex, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+- if (it->first.ID == sessId){
++ for (std::map<std::string, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
++ if (it->first == sessId){
+ it->second.tags.insert(tag);
+ return;
+ }
+@@ -295,17 +229,15 @@ void Controller::tag_shutdown(const std::string &tag){
+ FAIL_MSG("In controller shutdown procedure - cannot shutdown sessions.");
+ return;
+ }
+- unsigned int murdered = 0;
+ unsigned int sessCount = 0;
+ tthread::lock_guard<tthread::mutex> guard(statsMutex);
+- for (std::map<sessIndex, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
++ for (std::map<std::string, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+ if (it->second.tags.count(tag)){
+ sessCount++;
+- murdered += it->second.kill();
++ killConnections(it->first);
+ }
+ }
+- Controller::writeSessionCache();
+- INFO_MSG("Shut down %u connections in %u session(s) for tag %s", murdered, sessCount, tag.c_str());
++ INFO_MSG("Shut down %u session(s) for tag %s", sessCount, tag.c_str());
+ }
+
+ /// Shuts down all current sessions for the given streamname
+@@ -315,48 +247,23 @@ void Controller::sessions_shutdown(const std::string &streamname, const std::str
+ FAIL_MSG("In controller shutdown procedure - cannot shutdown sessions.");
+ return;
+ }
+- unsigned int murdered = 0;
+ unsigned int sessCount = 0;
+ tthread::lock_guard<tthread::mutex> guard(statsMutex);
+- for (std::map<sessIndex, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+- if ((!streamname.size() || it->first.streamName == streamname) &&
+- (!protocol.size() || it->first.connector == protocol)){
++ // Find all matching streams in statComm and get their sessId
++ for (size_t i = 0; i < statComm.recordCount(); i++){
++ if (statComm.getStatus(i) == COMM_STATUS_INVALID || (statComm.getStatus(i) & COMM_STATUS_DISCONNECT)){continue;}
++ if ((!streamname.size() || statComm.getStream(i) == streamname) &&
++ (!protocol.size() || statComm.hasConnector(i, protocol))){
++ uint32_t pid = statComm.getPid(i);
+ sessCount++;
+- murdered += it->second.kill();
+- }
+- }
+- Controller::writeSessionCache();
+- INFO_MSG("Shut down %u connections in %u sessions for stream %s/%s", murdered, sessCount,
+- streamname.c_str(), protocol.c_str());
+-}
+-
+-/// Writes the session cache to shared memory.
+-/// Assumes the config mutex, stats mutex and session cache semaphore are already locked.
+-/// Does nothing if the session cache could not be initialized on the first try
+-/// Does no error checking after first open attempt (fails silently)!
+-void Controller::writeSessionCache(){
+- uint32_t shmOffset = 0;
+- if (shmSessions && shmSessions->mapped){
+- if (cacheLock){cacheLock->wait(16);}
+- if (sessions.size()){
+- for (std::map<sessIndex, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+- if (it->second.hasData()){
+- // store an entry in the shmSessions page, if it fits
+- if (it->second.sync > 2 && shmOffset + SHM_SESSIONS_ITEM < SHM_SESSIONS_SIZE){
+- *((uint32_t *)(shmSessions->mapped + shmOffset)) = it->first.crc;
+- strncpy(shmSessions->mapped + shmOffset + 4, it->first.streamName.c_str(), 100);
+- strncpy(shmSessions->mapped + shmOffset + 104, it->first.connector.c_str(), 20);
+- strncpy(shmSessions->mapped + shmOffset + 124, it->first.host.c_str(), 40);
+- shmSessions->mapped[shmOffset + 164] = it->second.sync;
+- shmOffset += SHM_SESSIONS_ITEM;
+- }
+- }
++ if (pid > 1){
++ Util::Procs::Stop(pid);
++ INFO_MSG("Killing PID %" PRIu32, pid);
+ }
+ }
+- // set a final shmSessions entry to all zeroes
+- memset(shmSessions->mapped + shmOffset, 0, SHM_SESSIONS_ITEM);
+- if (cacheLock){cacheLock->post(16);}
+ }
++ INFO_MSG("Shut down %u sessions for stream %s/%s", sessCount,
++ streamname.c_str(), protocol.c_str());
+ }
+
+ /// This function runs as a thread and roughly once per second retrieves
+@@ -366,14 +273,6 @@ void Controller::SharedMemStats(void *config){
+ HIGH_MSG("Starting stats thread");
+ statComm.reload(true);
+ statCommActive = true;
+- shmSessions = new IPC::sharedPage(SHM_SESSIONS, SHM_SESSIONS_SIZE, false, false);
+- if (!shmSessions || !shmSessions->mapped){
+- if (shmSessions){delete shmSessions;}
+- shmSessions = new IPC::sharedPage(SHM_SESSIONS, SHM_SESSIONS_SIZE, true);
+- }
+- cacheLock = new IPC::semaphore(SEM_SESSCACHE, O_CREAT | O_RDWR, ACCESSPERMS, 16);
+- cacheLock->unlink();
+- cacheLock->open(SEM_SESSCACHE, O_CREAT | O_RDWR, ACCESSPERMS, 16);
+ std::set<std::string> inactiveStreams;
+ Controller::initState();
+ bool shiftWrites = true;
+@@ -401,7 +300,6 @@ void Controller::SharedMemStats(void *config){
+ }
+ }
+ {
+-
+ tthread::lock_guard<tthread::mutex> guard(Controller::configMutex);
+ tthread::lock_guard<tthread::mutex> guard2(statsMutex);
+ // parse current users
+@@ -429,29 +327,47 @@ void Controller::SharedMemStats(void *config){
+ it->second.packRetrans = 0;
+ }
+ }
+- // wipe old statistics
++ unsigned int tOut = Util::bootSecs() - STATS_DELAY;
++ unsigned int tIn = Util::bootSecs() - STATS_INPUT_DELAY;
++ if (streamStats.size()){
++ for (std::map<std::string, struct streamTotals>::iterator it = streamStats.begin();
++ it != streamStats.end(); ++it){
++ it->second.currViews = 0;
++ it->second.currIns = 0;
++ it->second.currOuts = 0;
++ }
++ }
++ // wipe old statistics and set session type counters
+ if (sessions.size()){
+- std::list<sessIndex> mustWipe;
++ std::list<std::string> mustWipe;
+ uint64_t cutOffPoint = Util::bootSecs() - STAT_CUTOFF;
+- uint64_t disconnectPointIn = Util::bootSecs() - STATS_INPUT_DELAY;
+- uint64_t disconnectPointOut = Util::bootSecs() - STATS_DELAY;
+- for (std::map<sessIndex, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+- uint64_t dPoint = it->second.getSessType() == SESS_INPUT ? disconnectPointIn : disconnectPointOut;
+- if (it->second.sync == 100){
+- // Denied entries are connection-entry-wiped as soon as they become boring
+- it->second.wipeOld(dPoint);
+- }else{
+- // Normal entries are summarized after STAT_CUTOFF seconds
+- it->second.wipeOld(cutOffPoint);
+- }
++ for (std::map<std::string, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+ // This part handles ending sessions, keeping them in cache for now
+- if (it->second.isTracked() && !it->second.isConnected() && it->second.getEnd() < dPoint){
+- it->second.dropSession(it->first);
+- }
+- // This part handles wiping from the session cache
+- if (!it->second.hasData()){
+- it->second.dropSession(it->first); // End the session, just in case it wasn't yet
++ if (it->second.getEnd() < cutOffPoint && it->second.newestDataPoint() < cutOffPoint){
++ viewSecondsTotal += it->second.getConnTime();
+ mustWipe.push_back(it->first);
++ // Don't count this session as a viewer
++ continue;
++ }
++ // Recount input, output and viewer type sessions
++ switch (it->second.getSessType()){
++ case SESS_UNSET: break;
++ case SESS_VIEWER:
++ if (it->second.hasDataFor(tOut) && it->second.isViewerOn(tOut)){
++ streamStats[it->first].currViews++;
++ }
++ servSeconds += it->second.getConnTime();
++ break;
++ case SESS_INPUT:
++ if (it->second.hasDataFor(tIn) && it->second.isViewerOn(tIn)){
++ streamStats[it->first].currIns++;
++ }
++ break;
++ case SESS_OUTPUT:
++ if (it->second.hasDataFor(tOut) && it->second.isViewerOn(tOut)){
++ streamStats[it->first].currOuts++;
++ }
++ break;
+ }
+ }
+ while (mustWipe.size()){
+@@ -513,7 +429,6 @@ void Controller::SharedMemStats(void *config){
+ shiftWrites = true;
+ }
+ /*LTS-START*/
+- Controller::writeSessionCache();
+ Controller::checkServerLimits();
+ /*LTS-END*/
+ }
+@@ -523,7 +438,6 @@ void Controller::SharedMemStats(void *config){
+ HIGH_MSG("Stopping stats thread");
+ if (Util::Config::is_restarting){
+ statComm.setMaster(false);
+- shmSessions->master = false;
+ }else{/*LTS-START*/
+ if (Controller::killOnExit){
+ WARN_MSG("Killing all connected clients to force full shutdown");
+@@ -532,10 +446,6 @@ void Controller::SharedMemStats(void *config){
+ /*LTS-END*/
+ }
+ Controller::deinitState(Util::Config::is_restarting);
+- delete shmSessions;
+- shmSessions = 0;
+- delete cacheLock;
+- cacheLock = 0;
+ }
+
+ /// Gets a complete list of all streams currently in active state, with optional prefix matching
+@@ -559,97 +469,53 @@ std::set<std::string> Controller::getActiveStreams(const std::string &prefix){
+ return ret;
+ }
+
+-/// Forces a re-sync of the session
+-/// Assumes the session cache will be updated separately - may not work correctly if this is forgotten!
+-uint32_t Controller::statSession::invalidate(){
+- uint32_t ret = 0;
+- sync = 1;
+- if (curConns.size() && statCommActive){
+- for (std::map<uint64_t, statStorage>::iterator jt = curConns.begin(); jt != curConns.end(); ++jt){
+- if (statComm.getStatus(jt->first) != COMM_STATUS_INVALID){
+- statComm.setSync(2, jt->first);
+- ret++;
+- }
+- }
+- }
+- return ret;
+-}
+-
+-/// Kills all active connections, sets the session state to denied (sync=100).
++/// Kills all connection of a given session
+ /// Assumes the session cache will be updated separately - may not work correctly if this is forgotten!
+-uint32_t Controller::statSession::kill(){
+- uint32_t ret = 0;
+- sync = 100;
+- if (curConns.size() && statCommActive){
+- for (std::map<uint64_t, statStorage>::iterator jt = curConns.begin(); jt != curConns.end(); ++jt){
+- if (statComm.getStatus(jt->first) != COMM_STATUS_INVALID){
+- statComm.setSync(100, jt->first);
+- uint32_t pid = statComm.getPid(jt->first);
++void Controller::killConnections(std::string sessId){
++ if (statCommActive){
++ // Find a matching stream in statComm with a matching sessID and kill it
++ for (size_t i = 0; i < statComm.recordCount(); i++){
++ if (statComm.getStatus(i) == COMM_STATUS_INVALID || (statComm.getStatus(i) & COMM_STATUS_DISCONNECT)){continue;}
++ if (statComm.getSessId(i) == sessId){
++ uint32_t pid = statComm.getPid(i);
+ if (pid > 1){
+ Util::Procs::Stop(pid);
+ INFO_MSG("Killing PID %" PRIu32, pid);
+ }
+- ret++;
+ }
+ }
+ }
+- return ret;
+ }
+
+ /// Updates the given active connection with new stats data.
+-void Controller::statSession::update(uint64_t index, Comms::Statistics &statComm){
+- std::string myHost;
+- Socket::hostBytesToStr(statComm.getHost(index).data(), 16, myHost);
+- std::string myStream = statComm.getStream(index);
+- std::string myConnector = statComm.getConnector(index);
+- // update the sync byte: 0 = requesting fill, 2 = requesting refill, 1 = needs checking, > 2 =
+- // state known (100=denied, 10=accepted)
+- if (!statComm.getSync(index)){
+- sessIndex tmpidx(statComm, index);
+- // if we have a maximum connection count per IP, enforce it
+- if (maxConnsPerIP && !statComm.getSync(index)){
+- unsigned int currConns = 1;
+- long long shortly = Util::bootSecs();
+- for (std::map<sessIndex, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+-
+- if (&it->second != this && it->first.host == myHost &&
+- (it->second.hasDataFor(shortly - STATS_DELAY) || it->second.hasDataFor(shortly) ||
+- it->second.hasDataFor(shortly - 1) || it->second.hasDataFor(shortly - 2) ||
+- it->second.hasDataFor(shortly - 3) || it->second.hasDataFor(shortly - 4) ||
+- it->second.hasDataFor(shortly - 5)) &&
+- ++currConns > maxConnsPerIP){
+- break;
+- }
+- }
+- if (currConns > maxConnsPerIP){
+- WARN_MSG("Disconnecting session from %s: exceeds max connection count of %u", myHost.c_str(), maxConnsPerIP);
+- statComm.setSync(100, index);
+- }
+- }
+- if (statComm.getSync(index) != 100){
+- // only set the sync if this is the first connection in the list
+- // we also catch the case that there are no connections, which is an error-state
+- if (!sessions[tmpidx].curConns.size() || sessions[tmpidx].curConns.begin()->first == index){
+- MEDIUM_MSG("Requesting sync to %u for %s, %s, %s, %" PRIu32, sync, myStream.c_str(),
+- myConnector.c_str(), myHost.c_str(), statComm.getCRC(index) & 0xFFFFFFFFu);
+- statComm.setSync(sync, index);
+- }
+- // and, always set the sync if it is > 2
+- if (sync > 2){
+- MEDIUM_MSG("Setting sync to %u for %s, %s, %s, %" PRIu32, sync, myStream.c_str(),
+- myConnector.c_str(), myHost.c_str(), statComm.getCRC(index) & 0xFFFFFFFFu);
+- statComm.setSync(sync, index);
+- }
++void Controller::statSession::update(uint64_t index, Comms::Sessions &statComm){
++ if (host == ""){
++ Socket::hostBytesToStr(statComm.getHost(index).data(), 16, host);
++ }
++ if (streamName == ""){
++ streamName = statComm.getStream(index);
++ }
++ if (curConnector == ""){
++ curConnector = statComm.getConnector(index);
++ }
++ if (sessId == ""){
++ sessId = statComm.getSessId(index);
++ }
++ // Export tags to session
++ if (tags.size()){
++ std::stringstream tagStream;
++ for (std::set<std::string>::iterator it = tags.begin(); it != tags.end(); ++it){
++ tagStream << "[" << *it << "]";
+ }
+- }else{
+- if (sync < 2 && statComm.getSync(index) > 2){sync = statComm.getSync(index);}
++ statComm.setTags(tagStream.str(), index);
+ }
++
+ long long prevDown = getDown();
+ long long prevUp = getUp();
+ uint64_t prevPktSent = getPktCount();
+ uint64_t prevPktLost = getPktLost();
+ uint64_t prevPktRetrans = getPktRetransmit();
+- curConns[index].update(statComm, index);
++ curData.update(statComm, index);
+ // store timestamp of first received data, if older
+ if (firstSec > statComm.getNow(index)){firstSec = statComm.getNow(index);}
+ uint64_t secIncr = 0;
+@@ -671,7 +537,7 @@ void Controller::statSession::update(uint64_t index, Comms::Statistics &statComm
+ uint64_t currPktRetrans = getPktRetransmit();
+ if (currUp - prevUp < 0 || currDown - prevDown < 0){
+ INFO_MSG("Negative data usage! %lldu/%lldd (u%lld->%lld) in %s over %s, #%" PRIu64, currUp - prevUp,
+- currDown - prevDown, prevUp, currUp, myStream.c_str(), myConnector.c_str(), index);
++ currDown - prevDown, prevUp, currUp, streamName.c_str(), curConnector.c_str(), index);
+ }else{
+ if (!noBWCount){
+ size_t bwMatchOffset = 0;
+@@ -701,56 +567,39 @@ void Controller::statSession::update(uint64_t index, Comms::Statistics &statComm
+ servPackRetrans += currPktRetrans - prevPktRetrans;
+ }
+ }
+- if (currDown + currUp >= COUNTABLE_BYTES){
+- if (sessionType == SESS_UNSET){
+- if (myConnector.size() >= 5 && myConnector.substr(0, 5) == "INPUT"){
+- ++servInputs;
+- createEmptyStatsIfNeeded(myStream);
+- streamStats[myStream].inputs++;
+- streamStats[myStream].currIns++;
+- sessionType = SESS_INPUT;
+- }else if (myConnector.size() >= 6 && myConnector.substr(0, 6) == "OUTPUT"){
+- ++servOutputs;
+- createEmptyStatsIfNeeded(myStream);
+- streamStats[myStream].outputs++;
+- streamStats[myStream].currOuts++;
+- sessionType = SESS_OUTPUT;
+- }else{
+- ++servViewers;
+- createEmptyStatsIfNeeded(myStream);
+- streamStats[myStream].viewers++;
+- streamStats[myStream].currViews++;
+- sessionType = SESS_VIEWER;
+- }
++ if (sessionType == SESS_UNSET){
++ if (curConnector.size() >= 5 && curConnector.substr(0, 5) == "INPUT"){
++ ++servInputs;
++ createEmptyStatsIfNeeded(streamName);
++ streamStats[streamName].inputs++;
++ streamStats[streamName].currIns++;
++ sessionType = SESS_INPUT;
++ }else if (curConnector.size() >= 6 && curConnector.substr(0, 6) == "OUTPUT"){
++ ++servOutputs;
++ createEmptyStatsIfNeeded(streamName);
++ streamStats[streamName].outputs++;
++ streamStats[streamName].currOuts++;
++ sessionType = SESS_OUTPUT;
++ }else{
++ ++servViewers;
++ createEmptyStatsIfNeeded(streamName);
++ streamStats[streamName].viewers++;
++ streamStats[streamName].currViews++;
++ sessionType = SESS_VIEWER;
+ }
+- // If previous < COUNTABLE_BYTES, we haven't counted any data so far.
+- // We need to count all the data in that case, otherwise we only count the difference.
+- if (noBWCount != 2){ //only count connections that are countable
+- if (prevUp + prevDown < COUNTABLE_BYTES){
+- if (!myStream.size() || myStream[0] == 0){
+- if (streamStats.count(myStream)){streamStats.erase(myStream);}
+- }else{
+- createEmptyStatsIfNeeded(myStream);
+- streamStats[myStream].upBytes += currUp;
+- streamStats[myStream].downBytes += currDown;
+- streamStats[myStream].packSent += currPktSent;
+- streamStats[myStream].packLoss += currPktLost;
+- streamStats[myStream].packRetrans += currPktRetrans;
+- if (sessionType == SESS_VIEWER){streamStats[myStream].viewSeconds += lastSec - firstSec;}
+- }
+- }else{
+- if (!myStream.size() || myStream[0] == 0){
+- if (streamStats.count(myStream)){streamStats.erase(myStream);}
+- }else{
+- createEmptyStatsIfNeeded(myStream);
+- streamStats[myStream].upBytes += currUp - prevUp;
+- streamStats[myStream].downBytes += currDown - prevDown;
+- streamStats[myStream].packSent += currPktSent - prevPktSent;
+- streamStats[myStream].packLoss += currPktLost - prevPktLost;
+- streamStats[myStream].packRetrans += currPktRetrans - prevPktRetrans;
+- if (sessionType == SESS_VIEWER){streamStats[myStream].viewSeconds += secIncr;}
+- }
+- }
++ }
++ // Only count connections that are countable
++ if (noBWCount != 2){
++ if (!streamName.size() || streamName[0] == 0){
++ if (streamStats.count(streamName)){streamStats.erase(streamName);}
++ }else{
++ createEmptyStatsIfNeeded(streamName);
++ streamStats[streamName].upBytes += currUp - prevUp;
++ streamStats[streamName].downBytes += currDown - prevDown;
++ streamStats[streamName].packSent += currPktSent - prevPktSent;
++ streamStats[streamName].packLoss += currPktLost - prevPktLost;
++ streamStats[streamName].packRetrans += currPktRetrans - prevPktRetrans;
++ if (sessionType == SESS_VIEWER){streamStats[streamName].viewSeconds += secIncr;}
+ }
+ }
+ }
+@@ -759,53 +608,19 @@ Controller::sessType Controller::statSession::getSessType(){
+ return sessionType;
+ }
+
+-/// Archives connection log entries older than the given cutOff point.
+-void Controller::statSession::wipeOld(uint64_t cutOff){
+- if (firstSec > cutOff){return;}
+- firstSec = 0xFFFFFFFFFFFFFFFFull;
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- while (it->log.size() && it->log.begin()->first < cutOff){
+- if (it->log.size() == 1){
+- wipedDown += it->log.begin()->second.down;
+- wipedUp += it->log.begin()->second.up;
+- wipedPktCount += it->log.begin()->second.pktCount;
+- wipedPktLost += it->log.begin()->second.pktLost;
+- wipedPktRetransmit += it->log.begin()->second.pktRetransmit;
+- }
+- it->log.erase(it->log.begin());
+- }
+- if (it->log.size()){
+- if (firstSec > it->log.begin()->first){firstSec = it->log.begin()->first;}
+- }
+- }
+- while (oldConns.size() && !oldConns.begin()->log.size()){oldConns.pop_front();}
+- }
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- while (it->second.log.size() > 1 && it->second.log.begin()->first < cutOff){
+- it->second.log.erase(it->second.log.begin());
+- }
+- if (it->second.log.size()){
+- if (firstSec > it->second.log.begin()->first){firstSec = it->second.log.begin()->first;}
+- }
+- }
+- }
+-}
+-
+-void Controller::statSession::dropSession(const Controller::sessIndex &index){
+- if (!tracked || curConns.size()){return;}
++Controller::statSession::~statSession(){
++ if (!tracked){return;}
+ switch (sessionType){
+- case SESS_INPUT:
+- if (streamStats.count(index.streamName) && streamStats[index.streamName].currIns){streamStats[index.streamName].currIns--;}
+- break;
+- case SESS_OUTPUT:
+- if (streamStats.count(index.streamName) && streamStats[index.streamName].currOuts){streamStats[index.streamName].currOuts--;}
+- break;
+- case SESS_VIEWER:
+- if (streamStats.count(index.streamName) && streamStats[index.streamName].currViews){streamStats[index.streamName].currViews--;}
+- break;
+- default: break;
++ case SESS_INPUT:
++ if (streamStats.count(streamName) && streamStats[streamName].currIns){streamStats[streamName].currIns--;}
++ break;
++ case SESS_OUTPUT:
++ if (streamStats.count(streamName) && streamStats[streamName].currOuts){streamStats[streamName].currOuts--;}
++ break;
++ case SESS_VIEWER:
++ if (streamStats.count(streamName) && streamStats[streamName].currViews){streamStats[streamName].currViews--;}
++ break;
++ default: break;
+ }
+ uint64_t duration = lastSec - firstActive;
+ if (duration < 1){duration = 1;}
+@@ -815,13 +630,13 @@ void Controller::statSession::dropSession(const Controller::sessIndex &index){
+ tagStream << "[" << *it << "]";
+ }
+ }
+- Controller::logAccess(index.ID, index.streamName, index.connector, index.host, duration, getUp(),
++ Controller::logAccess(sessId, streamName, curConnector, host, duration, getUp(),
+ getDown(), tagStream.str());
+ if (Controller::accesslog.size()){
+ if (Controller::accesslog == "LOG"){
+ std::stringstream accessStr;
+- accessStr << "Session <" << index.ID << "> " << index.streamName << " (" << index.connector
+- << ") from " << index.host << " ended after " << duration << "s, avg "
++ accessStr << "Session <" << sessId << "> " << streamName << " (" << curConnector
++ << ") from " << host << " ended after " << duration << "s, avg "
+ << getUp() / duration / 1024 << "KB/s up " << getDown() / duration / 1024 << "KB/s down.";
+ if (tags.size()){accessStr << " Tags: " << tagStream.str();}
+ Controller::Log("ACCS", accessStr.str());
+@@ -845,8 +660,8 @@ void Controller::statSession::dropSession(const Controller::sessIndex &index){
+ time(&rawtime);
+ timeinfo = localtime_r(&rawtime, &tmptime);
+ strftime(buffer, 100, "%F %H:%M:%S", timeinfo);
+- accLogFile << buffer << ", " << index.ID << ", " << index.streamName << ", "
+- << index.connector << ", " << index.host << ", " << duration << ", "
++ accLogFile << buffer << ", " << sessId << ", " << streamName << ", "
++ << curConnector << ", " << host << ", " << duration << ", "
+ << getUp() / duration / 1024 << ", " << getDown() / duration / 1024 << ", ";
+ if (tags.size()){accLogFile << tagStream.str();}
+ accLogFile << std::endl;
+@@ -857,77 +672,21 @@ void Controller::statSession::dropSession(const Controller::sessIndex &index){
+ firstActive = 0;
+ firstSec = 0xFFFFFFFFFFFFFFFFull;
+ lastSec = 0;
+- wipedUp = 0;
+- wipedDown = 0;
+- wipedPktCount = 0;
+- wipedPktLost = 0;
+- wipedPktRetransmit = 0;
+- oldConns.clear();
+ sessionType = SESS_UNSET;
+ }
+
+-/// Archives the given connection.
+-void Controller::statSession::finish(uint64_t index){
+- oldConns.push_back(curConns[index]);
+- curConns.erase(index);
+-}
+-
+ /// Constructs an empty session
+ Controller::statSession::statSession(){
+ firstActive = 0;
+ tracked = false;
+ firstSec = 0xFFFFFFFFFFFFFFFFull;
+ lastSec = 0;
+- sync = 1;
+- wipedUp = 0;
+- wipedDown = 0;
+- wipedPktCount = 0;
+- wipedPktLost = 0;
+- wipedPktRetransmit = 0;
+ sessionType = SESS_UNSET;
+ noBWCount = 0;
+-}
+-
+-/// Moves the given connection to the given session
+-void Controller::statSession::switchOverTo(statSession &newSess, uint64_t index){
+- // add to the given session first
+- newSess.curConns[index] = curConns[index];
+- // if this connection has data, update firstSec/lastSec if needed
+- if (curConns[index].log.size()){
+- if (newSess.firstSec > curConns[index].log.begin()->first){
+- newSess.firstSec = curConns[index].log.begin()->first;
+- }
+- if (newSess.lastSec < curConns[index].log.rbegin()->first){
+- newSess.lastSec = curConns[index].log.rbegin()->first;
+- }
+- }
+- // remove from current session
+- curConns.erase(index);
+- // if there was any data, recalculate this session's firstSec and lastSec.
+- if (newSess.curConns[index].log.size()){
+- firstSec = 0xFFFFFFFFFFFFFFFFull;
+- lastSec = 0;
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- if (it->log.size()){
+- if (firstSec > it->log.begin()->first){firstSec = it->log.begin()->first;}
+- if (lastSec < it->log.rbegin()->first){lastSec = it->log.rbegin()->first;}
+- }
+- }
+- }
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- if (it->second.log.size()){
+- if (firstSec > it->second.log.begin()->first){
+- firstSec = it->second.log.begin()->first;
+- }
+- if (lastSec < it->second.log.rbegin()->first){
+- lastSec = it->second.log.rbegin()->first;
+- }
+- }
+- }
+- }
+- }
++ streamName = "";
++ host = "";
++ curConnector = "";
++ sessId = "";
+ }
+
+ /// Returns the first measured timestamp in this session.
+@@ -944,39 +703,34 @@ uint64_t Controller::statSession::getEnd(){
+ bool Controller::statSession::hasDataFor(uint64_t t){
+ if (lastSec < t){return false;}
+ if (firstSec > t){return false;}
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- if (it->hasDataFor(t)){return true;}
+- }
+- }
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- if (it->second.hasDataFor(t)){return true;}
+- }
+- }
+- return false;
+-}
+-
+-/// Returns true if there is any data for this session.
+-bool Controller::statSession::hasData(){
+- if (!firstSec && !lastSec){return false;}
+- if (curConns.size()){return true;}
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- if (it->log.size()){return true;}
+- }
+- }
++ if (curData.hasDataFor(t)){return true;}
+ return false;
+ }
+
+ /// Returns true if this session should count as a viewer on the given timestamp.
+ bool Controller::statSession::isViewerOn(uint64_t t){
+- return getUp(t) + getDown(t) > COUNTABLE_BYTES;
++ return getUp(t) + getDown(t);
++}
++
++std::string Controller::statSession::getStreamName(){
++ return streamName;
++}
++
++std::string Controller::statSession::getHost(){
++ return host;
++}
++
++std::string Controller::statSession::getSessId(){
++ return sessId;
++}
++
++std::string Controller::statSession::getCurrentProtocols(){
++ return curConnector;
+ }
+
+ /// Returns true if this session should be considered connected
+-bool Controller::statSession::isConnected(){
+- return curConns.size();
++uint64_t Controller::statSession::newestDataPoint(){
++ return lastSec;
+ }
+
+ /// Returns true if this session has started (tracked == true) but not yet ended (log entry written)
+@@ -986,188 +740,103 @@ bool Controller::statSession::isTracked(){
+
+ /// Returns the cumulative connected time for this session at timestamp t.
+ uint64_t Controller::statSession::getConnTime(uint64_t t){
+- uint64_t retVal = 0;
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- if (it->hasDataFor(t)){retVal += it->getDataFor(t).time;}
+- }
++ if (curData.hasDataFor(t)){
++ return curData.getDataFor(t).time;
+ }
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- if (it->second.hasDataFor(t)){retVal += it->second.getDataFor(t).time;}
+- }
++ return 0;
++}
++
++/// Returns the cumulative connected time for this session.
++uint64_t Controller::statSession::getConnTime(){
++ if (curData.log.size()){
++ return curData.log.rbegin()->second.time;
+ }
+- return retVal;
++ return 0;
+ }
+
+ /// Returns the last requested media timestamp for this session at timestamp t.
+ uint64_t Controller::statSession::getLastSecond(uint64_t t){
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- if (it->second.hasDataFor(t)){return it->second.getDataFor(t).lastSecond;}
+- }
+- }
+- if (oldConns.size()){
+- for (std::deque<statStorage>::reverse_iterator it = oldConns.rbegin(); it != oldConns.rend(); ++it){
+- if (it->hasDataFor(t)){return it->getDataFor(t).lastSecond;}
+- }
++ if (curData.hasDataFor(t)){
++ return curData.getDataFor(t).lastSecond;
+ }
+ return 0;
+ }
+
+ /// Returns the cumulative downloaded bytes for this session at timestamp t.
+ uint64_t Controller::statSession::getDown(uint64_t t){
+- uint64_t retVal = wipedDown;
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- if (it->hasDataFor(t)){retVal += it->getDataFor(t).down;}
+- }
++ if (curData.hasDataFor(t)){
++ return curData.getDataFor(t).down;
+ }
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- if (it->second.hasDataFor(t)){retVal += it->second.getDataFor(t).down;}
+- }
+- }
+- return retVal;
++ return 0;
+ }
+
+ /// Returns the cumulative uploaded bytes for this session at timestamp t.
+ uint64_t Controller::statSession::getUp(uint64_t t){
+- uint64_t retVal = wipedUp;
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- if (it->hasDataFor(t)){retVal += it->getDataFor(t).up;}
+- }
+- }
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- if (it->second.hasDataFor(t)){retVal += it->second.getDataFor(t).up;}
+- }
++ if (curData.hasDataFor(t)){
++ return curData.getDataFor(t).up;
+ }
+- return retVal;
++ return 0;
+ }
+
+ /// Returns the cumulative downloaded bytes for this session at timestamp t.
+ uint64_t Controller::statSession::getDown(){
+- uint64_t retVal = wipedDown;
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- if (it->log.size()){retVal += it->log.rbegin()->second.down;}
+- }
++ if (curData.log.size()){
++ return curData.log.rbegin()->second.down;
+ }
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- if (it->second.log.size()){retVal += it->second.log.rbegin()->second.down;}
+- }
+- }
+- return retVal;
++ return 0;
+ }
+
+ /// Returns the cumulative uploaded bytes for this session at timestamp t.
+ uint64_t Controller::statSession::getUp(){
+- uint64_t retVal = wipedUp;
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- if (it->log.size()){retVal += it->log.rbegin()->second.up;}
+- }
+- }
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- if (it->second.log.size()){retVal += it->second.log.rbegin()->second.up;}
+- }
++ if (curData.log.size()){
++ return curData.log.rbegin()->second.up;
+ }
+- return retVal;
++ return 0;
+ }
+
+ uint64_t Controller::statSession::getPktCount(uint64_t t){
+- uint64_t retVal = wipedPktCount;
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- if (it->hasDataFor(t)){retVal += it->getDataFor(t).pktCount;}
+- }
++ if (curData.hasDataFor(t)){
++ return curData.getDataFor(t).pktCount;
+ }
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- if (it->second.hasDataFor(t)){retVal += it->second.getDataFor(t).pktCount;}
+- }
+- }
+- return retVal;
++ return 0;
+ }
+
+ /// Returns the cumulative uploaded bytes for this session at timestamp t.
+ uint64_t Controller::statSession::getPktCount(){
+- uint64_t retVal = wipedPktCount;
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- if (it->log.size()){retVal += it->log.rbegin()->second.pktCount;}
+- }
+- }
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- if (it->second.log.size()){retVal += it->second.log.rbegin()->second.pktCount;}
+- }
++ if (curData.log.size()){
++ return curData.log.rbegin()->second.pktCount;
+ }
+- return retVal;
++ return 0;
+ }
++
+ uint64_t Controller::statSession::getPktLost(uint64_t t){
+- uint64_t retVal = wipedPktLost;
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- if (it->hasDataFor(t)){retVal += it->getDataFor(t).pktLost;}
+- }
++ if (curData.hasDataFor(t)){
++ return curData.getDataFor(t).pktLost;
+ }
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- if (it->second.hasDataFor(t)){retVal += it->second.getDataFor(t).pktLost;}
+- }
+- }
+- return retVal;
++ return 0;
+ }
+
+ /// Returns the cumulative uploaded bytes for this session at timestamp t.
+ uint64_t Controller::statSession::getPktLost(){
+- uint64_t retVal = wipedPktLost;
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- if (it->log.size()){retVal += it->log.rbegin()->second.pktLost;}
+- }
+- }
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- if (it->second.log.size()){retVal += it->second.log.rbegin()->second.pktLost;}
+- }
++ if (curData.log.size()){
++ return curData.log.rbegin()->second.pktLost;
+ }
+- return retVal;
++ return 0;
+ }
++
+ uint64_t Controller::statSession::getPktRetransmit(uint64_t t){
+- uint64_t retVal = wipedPktRetransmit;
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- if (it->hasDataFor(t)){retVal += it->getDataFor(t).pktRetransmit;}
+- }
++ if (curData.hasDataFor(t)){
++ return curData.getDataFor(t).pktRetransmit;
+ }
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- if (it->second.hasDataFor(t)){retVal += it->second.getDataFor(t).pktRetransmit;}
+- }
+- }
+- return retVal;
++ return 0;
+ }
+
+ /// Returns the cumulative uploaded bytes for this session at timestamp t.
+ uint64_t Controller::statSession::getPktRetransmit(){
+- uint64_t retVal = wipedPktRetransmit;
+- if (oldConns.size()){
+- for (std::deque<statStorage>::iterator it = oldConns.begin(); it != oldConns.end(); ++it){
+- if (it->log.size()){retVal += it->log.rbegin()->second.pktRetransmit;}
+- }
+- }
+- if (curConns.size()){
+- for (std::map<uint64_t, statStorage>::iterator it = curConns.begin(); it != curConns.end(); ++it){
+- if (it->second.log.size()){retVal += it->second.log.rbegin()->second.pktRetransmit;}
+- }
++ if (curData.log.size()){
++ return curData.log.rbegin()->second.pktRetransmit;
+ }
+- return retVal;
++ return 0;
+ }
+
+ /// Returns the cumulative downloaded bytes per second for this session at timestamp t.
+@@ -1207,6 +876,7 @@ Controller::statLog &Controller::statStorage::getDataFor(unsigned long long t){
+ empty.pktCount = 0;
+ empty.pktLost = 0;
+ empty.pktRetransmit = 0;
++ empty.connectors = "";
+ return empty;
+ }
+ std::map<unsigned long long, statLog>::iterator it = log.upper_bound(t);
+@@ -1216,7 +886,7 @@ Controller::statLog &Controller::statStorage::getDataFor(unsigned long long t){
+
+ /// This function is called by parseStatistics.
+ /// It updates the internally saved statistics data.
+-void Controller::statStorage::update(Comms::Statistics &statComm, size_t index){
++void Controller::statStorage::update(Comms::Sessions &statComm, size_t index){
+ statLog tmp;
+ tmp.time = statComm.getTime(index);
+ tmp.lastSecond = statComm.getLastSecond(index);
+@@ -1225,56 +895,58 @@ void Controller::statStorage::update(Comms::Statistics &statComm, size_t index){
+ tmp.pktCount = statComm.getPacketCount(index);
+ tmp.pktLost = statComm.getPacketLostCount(index);
+ tmp.pktRetransmit = statComm.getPacketRetransmitCount(index);
++ tmp.connectors = statComm.getConnector(index);
+ log[statComm.getNow(index)] = tmp;
+- // wipe data older than approx. STAT_CUTOFF seconds
+- /// \todo Remove least interesting data first.
+- if (log.size() > STAT_CUTOFF){log.erase(log.begin());}
++ // wipe data older than STAT_CUTOFF seconds
++ while (log.size() && log.begin()->first < Util::bootSecs() - STAT_CUTOFF){log.erase(log.begin());}
+ }
+
+ void Controller::statLeadIn(){
+ statDropoff = Util::bootSecs() - 3;
+ }
+-void Controller::statOnActive(size_t id){
+- // calculate the current session index, store as idx.
+- sessIndex idx(statComm, id);
+
++void Controller::statOnActive(size_t id){
+ if (statComm.getNow(id) >= statDropoff){
+- // if the connection was already indexed and it has changed, move it
+- if (connToSession.count(id) && connToSession[id] != idx){
+- if (sessions[connToSession[id]].getSessType() != SESS_UNSET){
+- INFO_MSG("Switching connection %zu from active session %s over to %s", id,
+- connToSession[id].toStr().c_str(), idx.toStr().c_str());
+- }else{
+- INFO_MSG("Switching connection %zu from inactive session %s over to %s", id,
+- connToSession[id].toStr().c_str(), idx.toStr().c_str());
+- }
+- sessions[connToSession[id]].switchOverTo(sessions[idx], id);
+- // Destroy this session without calling dropSession, because it was merged into another. What session? We never made it. Stop asking hard questions. Go, shoo. *sprays water*
+- if (!sessions[connToSession[id]].hasData()){sessions.erase(connToSession[id]);}
+- }
+- if (!connToSession.count(id)){
+- INSANE_MSG("New connection: %zu as %s", id, idx.toStr().c_str());
+- }
+- // store the index for later comparison
+- connToSession[id] = idx;
+ // update the session with the latest data
+- sessions[idx].update(id, statComm);
++ sessions[statComm.getSessId(id)].update(id, statComm);
+ }
+ }
++
+ void Controller::statOnDisconnect(size_t id){
+- sessIndex idx(statComm, id);
+- INSANE_MSG("Ended connection: %zu as %s", id, idx.toStr().c_str());
+- sessions[idx].finish(id);
+- connToSession.erase(id);
++ // Check to see if cleanup is required (when a Session binary fails)
++ const std::string thisSessionId = statComm.getSessId(id);
++ // Try to lock to see if the session crashed during boot
++ IPC::semaphore sessionLock;
++ char semName[NAME_BUFFER_SIZE];
++ snprintf(semName, NAME_BUFFER_SIZE, SEM_SESSION, thisSessionId.c_str());
++ sessionLock.open(semName, O_CREAT | O_RDWR, ACCESSPERMS, 1);
++ if (!sessionLock.tryWaitOneSecond()){
++ // Session likely crashed during boot. Remove the session lock which was created on bootup of the session
++ sessionLock.unlink();
++ }else if (!statComm.sessIdExists(thisSessionId)){
++ // There is no running process managing this session, so check if the data page still exists
++ IPC::sharedPage dataPage;
++ char userPageName[NAME_BUFFER_SIZE];
++ snprintf(userPageName, NAME_BUFFER_SIZE, COMMS_SESSIONS, thisSessionId.c_str());
++ dataPage.init(userPageName, 1, false, false);
++ if(dataPage){
++ // Session likely crashed while it was running
++ dataPage.init(userPageName, 1, true);
++ FAIL_MSG("Session '%s' got canceled unexpectedly. Hoovering up the left overs...", thisSessionId.c_str());
++ }
++ // Finally remove the session lock which was created on bootup of the session
++ sessionLock.unlink();
++ }
+ }
++
+ void Controller::statLeadOut(){}
+
+ /// Returns true if this stream has at least one connected client.
+ bool Controller::hasViewers(std::string streamName){
+ if (sessions.size()){
+ long long currTime = Util::bootSecs();
+- for (std::map<sessIndex, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+- if (it->first.streamName == streamName &&
++ for (std::map<std::string, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
++ if (it->second.getStreamName() == streamName &&
+ (it->second.hasDataFor(currTime) || it->second.hasDataFor(currTime - 1))){
+ return true;
+ }
+@@ -1377,7 +1049,6 @@ void Controller::fillClients(JSON::Value &req, JSON::Value &rep){
+ if (fields & STAT_CLI_UP){rep["fields"].append("up");}
+ if (fields & STAT_CLI_BPS_DOWN){rep["fields"].append("downbps");}
+ if (fields & STAT_CLI_BPS_UP){rep["fields"].append("upbps");}
+- if (fields & STAT_CLI_CRC){rep["fields"].append("crc");}
+ if (fields & STAT_CLI_SESSID){rep["fields"].append("sessid");}
+ if (fields & STAT_CLI_PKTCOUNT){rep["fields"].append("pktcount");}
+ if (fields & STAT_CLI_PKTLOST){rep["fields"].append("pktlost");}
+@@ -1386,26 +1057,25 @@ void Controller::fillClients(JSON::Value &req, JSON::Value &rep){
+ rep["data"].null();
+ // loop over all sessions
+ if (sessions.size()){
+- for (std::map<sessIndex, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
++ for (std::map<std::string, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+ unsigned long long time = reqTime;
+ if (now && reqTime - it->second.getEnd() < 5){time = it->second.getEnd();}
+ // data present and wanted? insert it!
+ if ((it->second.getEnd() >= time && it->second.getStart() <= time) &&
+- (!streams.size() || streams.count(it->first.streamName)) &&
+- (!protos.size() || protos.count(it->first.connector))){
++ (!streams.size() || streams.count(it->second.getStreamName())) &&
++ (!protos.size() || protos.count(it->second.getCurrentProtocols()))){
+ if (it->second.hasDataFor(time)){
+ JSON::Value d;
+- if (fields & STAT_CLI_HOST){d.append(it->first.host);}
+- if (fields & STAT_CLI_STREAM){d.append(it->first.streamName);}
+- if (fields & STAT_CLI_PROTO){d.append(it->first.connector);}
++ if (fields & STAT_CLI_HOST){d.append(it->second.getHost());}
++ if (fields & STAT_CLI_STREAM){d.append(it->second.getStreamName());}
++ if (fields & STAT_CLI_PROTO){d.append(it->second.getCurrentProtocols());}
+ if (fields & STAT_CLI_CONNTIME){d.append(it->second.getConnTime(time));}
+ if (fields & STAT_CLI_POSITION){d.append(it->second.getLastSecond(time));}
+ if (fields & STAT_CLI_DOWN){d.append(it->second.getDown(time));}
+ if (fields & STAT_CLI_UP){d.append(it->second.getUp(time));}
+ if (fields & STAT_CLI_BPS_DOWN){d.append(it->second.getBpsDown(time));}
+ if (fields & STAT_CLI_BPS_UP){d.append(it->second.getBpsUp(time));}
+- if (fields & STAT_CLI_CRC){d.append(it->first.crc);}
+- if (fields & STAT_CLI_SESSID){d.append(it->first.ID);}
++ if (fields & STAT_CLI_SESSID){d.append(it->second.getSessId());}
+ if (fields & STAT_CLI_PKTCOUNT){d.append(it->second.getPktCount(time));}
+ if (fields & STAT_CLI_PKTLOST){d.append(it->second.getPktLost(time));}
+ if (fields & STAT_CLI_PKTRETRANSMIT){d.append(it->second.getPktRetransmit(time));}
+@@ -1463,12 +1133,12 @@ void Controller::fillHasStats(JSON::Value &req, JSON::Value &rep){
+ {
+ tthread::lock_guard<tthread::mutex> guard(statsMutex);
+ if (sessions.size()){
+- for (std::map<sessIndex, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
++ for (std::map<std::string, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+ if (it->second.getSessType() == SESS_INPUT){
+- streams.insert(it->first.streamName);
++ streams.insert(it->second.getStreamName());
+ }else{
+- streams.insert(it->first.streamName);
+- if (it->second.getSessType() == SESS_VIEWER){clients[it->first.streamName]++;}
++ streams.insert(it->second.getStreamName());
++ if (it->second.getSessType() == SESS_VIEWER){clients[it->second.getStreamName()]++;}
+ }
+ }
+ }
+@@ -1742,12 +1412,12 @@ void Controller::fillTotals(JSON::Value &req, JSON::Value &rep){
+ // loop over all sessions
+ /// \todo Make the interval configurable instead of 1 second
+ if (sessions.size()){
+- for (std::map<sessIndex, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
++ for (std::map<std::string, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+ // data present and wanted? insert it!
+ if ((it->second.getEnd() >= (unsigned long long)reqStart ||
+ it->second.getStart() <= (unsigned long long)reqEnd) &&
+- (!streams.size() || streams.count(it->first.streamName)) &&
+- (!protos.size() || protos.count(it->first.connector))){
++ (!streams.size() || streams.count(it->second.getStreamName())) &&
++ (!protos.size() || protos.count(it->second.getCurrentProtocols()))){
+ for (unsigned long long i = reqStart; i <= reqEnd; ++i){
+ if (it->second.hasDataFor(i)){
+ totalsCount[i].add(it->second.getBpsDown(i), it->second.getBpsUp(i), it->second.getSessType(), it->second.getPktCount(), it->second.getPktLost(), it->second.getPktRetransmit());
+@@ -1831,6 +1501,25 @@ void Controller::handlePrometheus(HTTP::Parser &H, Socket::Connection &conn, int
+ H.SetHeader("Server", APPIDENT);
+ H.StartResponse("200", "OK", H, conn, true);
+
++ // Counters of current active viewers, inputs and outputs of the Session stats cache
++ std::map<std::string, uint32_t> outputs;
++ uint32_t totViewers = 0;
++ uint32_t totInputs = 0;
++ uint32_t totOutputs = 0;
++ for (uint64_t idx = 0; idx < statComm.recordCount(); idx++){
++ if (statComm.getStatus(idx) == COMM_STATUS_INVALID || statComm.getStatus(idx) & COMM_STATUS_DISCONNECT){continue;}
++ const std::string thisSessId = statComm.getSessId(idx);
++ // Count active viewers, inputs, outputs and protocols
++ if (thisSessId[0] == 'I'){
++ totInputs++;
++ }else if (thisSessId[0] == 'O'){
++ totOutputs++;
++ outputs[statComm.getConnector(idx)]++;
++ }else{
++ totViewers++;
++ }
++ }
++
+ // Collect core server stats
+ uint64_t mem_total = 0, mem_free = 0, mem_bufcache = 0;
+ uint64_t bw_up_total = 0, bw_down_total = 0;
+@@ -1904,109 +1593,69 @@ void Controller::handlePrometheus(HTTP::Parser &H, Socket::Connection &conn, int
+ response << "# TYPE mist_shm_used gauge\n";
+ response << "mist_shm_used " << (shm_total - shm_free) << "\n\n";
+
+- if (Controller::triggerStats.size()){
+- response << "# HELP mist_trigger_count Total executions for the given trigger\n";
+- response << "# HELP mist_trigger_time Total execution time in millis for the given trigger\n";
+- response << "# HELP mist_trigger_fails Total failed executions for the given trigger\n";
+- for (std::map<std::string, Controller::triggerLog>::iterator it = Controller::triggerStats.begin();
+- it != Controller::triggerStats.end(); it++){
+- response << "mist_trigger_count{trigger=\"" << it->first << "\"}" << it->second.totalCount << "\n";
+- response << "mist_trigger_time{trigger=\"" << it->first << "\"}" << it->second.ms << "\n";
+- response << "mist_trigger_fails{trigger=\"" << it->first << "\"}" << it->second.failCount << "\n";
++ response << "# HELP mist_viewseconds_total Number of seconds any media was received by a viewer.\n";
++ response << "# TYPE mist_viewseconds_total counter\n";
++ response << "mist_viewseconds_total " << servSeconds + viewSecondsTotal << "\n";
++
++ response << "\n# HELP mist_sessions_count Counts of unique sessions by type since server "
++ "start.\n";
++ response << "# TYPE mist_sessions_count counter\n";
++ response << "mist_sessions_count{sessType=\"viewers\"}" << servViewers << "\n";
++ response << "mist_sessions_count{sessType=\"incoming\"}" << servInputs << "\n";
++ response << "mist_sessions_count{sessType=\"outgoing\"}" << servOutputs << "\n\n";
++
++ response << "# HELP mist_bw_total Count of bytes handled since server start, by direction.\n";
++ response << "# TYPE mist_bw_total counter\n";
++ response << "stat_bw_total{direction=\"up\"}" << bw_up_total << "\n";
++ response << "stat_bw_total{direction=\"down\"}" << bw_down_total << "\n\n";
++ response << "mist_bw_total{direction=\"up\"}" << servUpBytes << "\n";
++ response << "mist_bw_total{direction=\"down\"}" << servDownBytes << "\n\n";
++ response << "mist_bw_other{direction=\"up\"}" << servUpOtherBytes << "\n";
++ response << "mist_bw_other{direction=\"down\"}" << servDownOtherBytes << "\n\n";
++ response << "mist_bw_limit " << bwLimit << "\n\n";
++
++ response << "# HELP mist_packets_total Total number of packets sent/received/lost over lossy protocols, server-wide.\n";
++ response << "# TYPE mist_packets_total counter\n";
++ response << "mist_packets_total{pkttype=\"sent\"}" << servPackSent << "\n";
++ response << "mist_packets_total{pkttype=\"lost\"}" << servPackLoss << "\n";
++ response << "mist_packets_total{pkttype=\"retrans\"}" << servPackRetrans << "\n";
++
++ if (outputs.size()){
++ response << "# HELP mist_outputs Number of viewers active right now, server-wide, by output type.\n";
++ response << "# TYPE mist_outputs gauge\n";
++ for (std::map<std::string, uint32_t>::iterator it = outputs.begin(); it != outputs.end(); ++it){
++ response << "mist_outputs{output=\"" << it->first << "\"}" << it->second << "\n";
+ }
+ response << "\n";
+ }
+
+ {// Scope for shortest possible blocking of statsMutex
+ tthread::lock_guard<tthread::mutex> guard(statsMutex);
+- // collect the data first
+- std::map<std::string, uint32_t> outputs;
+- unsigned long totViewers = 0, totInputs = 0, totOutputs = 0;
+- unsigned int tOut = Util::bootSecs() - STATS_DELAY;
+- unsigned int tIn = Util::bootSecs() - STATS_INPUT_DELAY;
+- // check all sessions
+- if (sessions.size()){
+- for (std::map<sessIndex, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+- switch (it->second.getSessType()){
+- case SESS_UNSET: break;
+- case SESS_VIEWER:
+- if (it->second.hasDataFor(tOut) && it->second.isViewerOn(tOut)){
+- outputs[it->first.connector]++;
+- totViewers++;
+- }
+- break;
+- case SESS_INPUT:
+- if (it->second.hasDataFor(tIn) && it->second.isViewerOn(tIn)){totInputs++;}
+- break;
+- case SESS_OUTPUT:
+- if (it->second.hasDataFor(tOut) && it->second.isViewerOn(tOut)){totOutputs++;}
+- break;
+- }
+- }
+- }
+
+- response << "# HELP mist_sessions_total Number of sessions active right now, server-wide, by "
+- "type.\n";
++ response << "# HELP mist_sessions_total Number of sessions active right now, server-wide, by type.\n";
+ response << "# TYPE mist_sessions_total gauge\n";
+ response << "mist_sessions_total{sessType=\"viewers\"}" << totViewers << "\n";
+ response << "mist_sessions_total{sessType=\"incoming\"}" << totInputs << "\n";
+ response << "mist_sessions_total{sessType=\"outgoing\"}" << totOutputs << "\n";
+- response << "mist_sessions_total{sessType=\"cached\"}" << sessions.size() << "\n\n";
+-
+- response << "# HELP mist_viewseconds_total Number of seconds any media was received by a viewer.\n";
+- response << "# TYPE mist_viewseconds_total counter\n";
+- response << "mist_viewseconds_total " << servSeconds << "\n";
++ response << "mist_sessions_total{sessType=\"cached\"}" << sessions.size() << "\n";
+
+- response << "# HELP mist_outputs Number of viewers active right now, server-wide, by output type.\n";
+- response << "# TYPE mist_outputs gauge\n";
+- for (std::map<std::string, uint32_t>::iterator it = outputs.begin(); it != outputs.end(); ++it){
+- response << "mist_outputs{output=\"" << it->first << "\"}" << it->second << "\n";
+- }
+- response << "\n";
+-
+- response << "# HELP mist_sessions_count Counts of unique sessions by type since server "
+- "start.\n";
+- response << "# TYPE mist_sessions_count counter\n";
+- response << "mist_sessions_count{sessType=\"viewers\"}" << servViewers << "\n";
+- response << "mist_sessions_count{sessType=\"incoming\"}" << servInputs << "\n";
+- response << "mist_sessions_count{sessType=\"outgoing\"}" << servOutputs << "\n\n";
+-
+- response << "# HELP mist_bw_total Count of bytes handled since server start, by direction.\n";
+- response << "# TYPE mist_bw_total counter\n";
+- response << "stat_bw_total{direction=\"up\"}" << bw_up_total << "\n";
+- response << "stat_bw_total{direction=\"down\"}" << bw_down_total << "\n\n";
+- response << "mist_bw_total{direction=\"up\"}" << servUpBytes << "\n";
+- response << "mist_bw_total{direction=\"down\"}" << servDownBytes << "\n\n";
+- response << "mist_bw_other{direction=\"up\"}" << servUpOtherBytes << "\n";
+- response << "mist_bw_other{direction=\"down\"}" << servDownOtherBytes << "\n\n";
+- response << "mist_bw_limit " << bwLimit << "\n\n";
+-
+- response << "# HELP mist_packets_total Total number of packets sent/received/lost over lossy protocols, server-wide.\n";
+- response << "# TYPE mist_packets_total counter\n";
+- response << "mist_packets_total{pkttype=\"sent\"}" << servPackSent << "\n";
+- response << "mist_packets_total{pkttype=\"lost\"}" << servPackLoss << "\n";
+- response << "mist_packets_total{pkttype=\"retrans\"}" << servPackRetrans << "\n";
+-
+- response << "\n# HELP mist_viewers Number of sessions by type and stream active right now.\n";
+- response << "# TYPE mist_viewers gauge\n";
+- response << "# HELP mist_viewcount Count of unique viewer sessions since stream start, per "
++ response << "\n# HELP mist_viewcount Count of unique viewer sessions since stream start, per "
+ "stream.\n";
+ response << "# TYPE mist_viewcount counter\n";
+- response << "# HELP mist_bw Count of bytes handled since stream start, by direction.\n";
+- response << "# TYPE mist_bw counter\n";
+ response << "# HELP mist_viewseconds Number of seconds any media was received by a viewer.\n";
+ response << "# TYPE mist_viewseconds counter\n";
++ response << "# HELP mist_bw Count of bytes handled since stream start, by direction.\n";
++ response << "# TYPE mist_bw counter\n";
+ response << "# HELP mist_packets Total number of packets sent/received/lost over lossy protocols.\n";
+ response << "# TYPE mist_packets counter\n";
+- response << "mist_viewseconds_total " << servSeconds << "\n";
+ for (std::map<std::string, struct streamTotals>::iterator it = streamStats.begin();
+- it != streamStats.end(); ++it){
++ it != streamStats.end(); ++it){
+ response << "mist_sessions{stream=\"" << it->first << "\",sessType=\"viewers\"}"
+- << it->second.currViews << "\n";
++ << it->second.currViews << "\n";
+ response << "mist_sessions{stream=\"" << it->first << "\",sessType=\"incoming\"}"
+- << it->second.currIns << "\n";
++ << it->second.currIns << "\n";
+ response << "mist_sessions{stream=\"" << it->first << "\",sessType=\"outgoing\"}"
+- << it->second.currOuts << "\n";
++ << it->second.currOuts << "\n";
+ response << "mist_viewcount{stream=\"" << it->first << "\"}" << it->second.viewers << "\n";
+ response << "mist_viewseconds{stream=\"" << it->first << "\"} " << it->second.viewSeconds << "\n";
+ response << "mist_bw{stream=\"" << it->first << "\",direction=\"up\"}" << it->second.upBytes << "\n";
+@@ -2015,6 +1664,19 @@ void Controller::handlePrometheus(HTTP::Parser &H, Socket::Connection &conn, int
+ response << "mist_packets{stream=\"" << it->first << "\",pkttype=\"lost\"}" << it->second.packLoss << "\n";
+ response << "mist_packets{stream=\"" << it->first << "\",pkttype=\"retrans\"}" << it->second.packRetrans << "\n";
+ }
++
++ if (Controller::triggerStats.size()){
++ response << "\n# HELP mist_trigger_count Total executions for the given trigger\n";
++ response << "# HELP mist_trigger_time Total execution time in millis for the given trigger\n";
++ response << "# HELP mist_trigger_fails Total failed executions for the given trigger\n";
++ for (std::map<std::string, Controller::triggerLog>::iterator it = Controller::triggerStats.begin();
++ it != Controller::triggerStats.end(); it++){
++ response << "mist_trigger_count{trigger=\"" << it->first << "\"}" << it->second.totalCount << "\n";
++ response << "mist_trigger_time{trigger=\"" << it->first << "\"}" << it->second.ms << "\n";
++ response << "mist_trigger_fails{trigger=\"" << it->first << "\"}" << it->second.failCount << "\n";
++ }
++ response << "\n";
++ }
+ }
+ H.Chunkify(response.str(), conn);
+ }
+@@ -2026,58 +1688,33 @@ void Controller::handlePrometheus(HTTP::Parser &H, Socket::Connection &conn, int
+ resp["shm_total"] = shm_total;
+ resp["shm_used"] = (shm_total - shm_free);
+ resp["logs"] = Controller::logCounter;
+- if (Controller::triggerStats.size()){
+- for (std::map<std::string, Controller::triggerLog>::iterator it = Controller::triggerStats.begin();
+- it != Controller::triggerStats.end(); it++){
+- JSON::Value &tVal = resp["triggers"][it->first];
+- tVal["count"] = it->second.totalCount;
+- tVal["ms"] = it->second.ms;
+- tVal["fails"] = it->second.failCount;
+- }
+- }
++ resp["curr"].append(totViewers);
++ resp["curr"].append(totInputs);
++ resp["curr"].append(totOutputs);
++ resp["tot"].append(servViewers);
++ resp["tot"].append(servInputs);
++ resp["tot"].append(servOutputs);
++ resp["st"].append(bw_up_total);
++ resp["st"].append(bw_down_total);
++ resp["bw"].append(servUpBytes);
++ resp["bw"].append(servDownBytes);
++ resp["pkts"].append(servPackSent);
++ resp["pkts"].append(servPackLoss);
++ resp["pkts"].append(servPackRetrans);
++ resp["bwlimit"] = bwLimit;
+ {// Scope for shortest possible blocking of statsMutex
+ tthread::lock_guard<tthread::mutex> guard(statsMutex);
+- // collect the data first
+- std::map<std::string, uint32_t> outputs;
+- uint64_t totViewers = 0, totInputs = 0, totOutputs = 0;
+- uint64_t tOut = Util::bootSecs() - STATS_DELAY;
+- uint64_t tIn = Util::bootSecs() - STATS_INPUT_DELAY;
+- // check all sessions
+- if (sessions.size()){
+- for (std::map<sessIndex, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+- switch (it->second.getSessType()){
+- case SESS_UNSET: break;
+- case SESS_VIEWER:
+- if (it->second.hasDataFor(tOut) && it->second.isViewerOn(tOut)){
+- outputs[it->first.connector]++;
+- totViewers++;
+- }
+- break;
+- case SESS_INPUT:
+- if (it->second.hasDataFor(tIn) && it->second.isViewerOn(tIn)){totInputs++;}
+- break;
+- case SESS_OUTPUT:
+- if (it->second.hasDataFor(tOut) && it->second.isViewerOn(tOut)){totOutputs++;}
+- break;
+- }
++ resp["curr"].append((uint64_t)sessions.size());
++
++ if (Controller::triggerStats.size()){
++ for (std::map<std::string, Controller::triggerLog>::iterator it = Controller::triggerStats.begin();
++ it != Controller::triggerStats.end(); it++){
++ JSON::Value &tVal = resp["triggers"][it->first];
++ tVal["count"] = it->second.totalCount;
++ tVal["ms"] = it->second.ms;
++ tVal["fails"] = it->second.failCount;
+ }
+ }
+-
+- resp["curr"].append(totViewers);
+- resp["curr"].append(totInputs);
+- resp["curr"].append(totOutputs);
+- resp["curr"].append((uint64_t)sessions.size());
+- resp["tot"].append(servViewers);
+- resp["tot"].append(servInputs);
+- resp["tot"].append(servOutputs);
+- resp["st"].append(bw_up_total);
+- resp["st"].append(bw_down_total);
+- resp["bw"].append(servUpBytes);
+- resp["bw"].append(servDownBytes);
+- resp["pkts"].append(servPackSent);
+- resp["pkts"].append(servPackLoss);
+- resp["pkts"].append(servPackRetrans);
+- resp["bwlimit"] = bwLimit;
+ if (Storage["config"].isMember("location") && Storage["config"]["location"].isMember("lat") && Storage["config"]["location"].isMember("lon")){
+ resp["loc"]["lat"] = Storage["config"]["location"]["lat"].asDouble();
+ resp["loc"]["lon"] = Storage["config"]["location"]["lon"].asDouble();
+diff --git a/src/controller/controller_statistics.h b/src/controller/controller_statistics.h
+index 1cc4a82d..f798f811 100644
+--- a/src/controller/controller_statistics.h
++++ b/src/controller/controller_statistics.h
+@@ -18,8 +18,7 @@
+ namespace Controller{
+
+ extern bool killOnExit;
+- extern unsigned int maxConnsPerIP;
+-
++
+ /// This function is ran whenever a stream becomes active.
+ void streamStarted(std::string stream);
+ /// This function is ran whenever a stream becomes inactive.
+@@ -35,34 +34,14 @@ namespace Controller{
+ uint64_t pktCount;
+ uint64_t pktLost;
+ uint64_t pktRetransmit;
++ std::string connectors;
+ };
+
+ enum sessType{SESS_UNSET = 0, SESS_INPUT, SESS_OUTPUT, SESS_VIEWER};
+
+- /// This is a comparison and storage class that keeps sessions apart from each other.
+- /// Whenever two of these objects are not equal, it will create a new session.
+- class sessIndex{
+- public:
+- sessIndex();
+- sessIndex(const Comms::Statistics &statComm, size_t id);
+- std::string ID;
+- std::string host;
+- unsigned int crc;
+- std::string streamName;
+- std::string connector;
+-
+- bool operator==(const sessIndex &o) const;
+- bool operator!=(const sessIndex &o) const;
+- bool operator>(const sessIndex &o) const;
+- bool operator<=(const sessIndex &o) const;
+- bool operator<(const sessIndex &o) const;
+- bool operator>=(const sessIndex &o) const;
+- std::string toStr();
+- };
+-
+ class statStorage{
+ public:
+- void update(Comms::Statistics &statComm, size_t index);
++ void update(Comms::Sessions &statComm, size_t index);
+ bool hasDataFor(unsigned long long);
+ statLog &getDataFor(unsigned long long);
+ std::map<unsigned long long, statLog> log;
+@@ -75,36 +54,33 @@ namespace Controller{
+ uint64_t firstActive;
+ uint64_t firstSec;
+ uint64_t lastSec;
+- uint64_t wipedUp;
+- uint64_t wipedDown;
+- uint64_t wipedPktCount;
+- uint64_t wipedPktLost;
+- uint64_t wipedPktRetransmit;
+- std::deque<statStorage> oldConns;
+ sessType sessionType;
+ bool tracked;
+ uint8_t noBWCount; ///< Set to 2 when not to count for external bandwidth
++ std::string streamName;
++ std::string host;
++ std::string curConnector;
++ std::string sessId;
++
+ public:
+ statSession();
+- uint32_t invalidate();
+- uint32_t kill();
+- char sync;
+- std::map<uint64_t, statStorage> curConns;
++ ~statSession();
++ statStorage curData;
+ std::set<std::string> tags;
+ sessType getSessType();
+- void wipeOld(uint64_t);
+- void finish(uint64_t index);
+- void switchOverTo(statSession &newSess, uint64_t index);
+- void update(uint64_t index, Comms::Statistics &data);
+- void dropSession(const sessIndex &index);
++ void update(uint64_t index, Comms::Sessions &data);
+ uint64_t getStart();
+ uint64_t getEnd();
+ bool isViewerOn(uint64_t time);
+- bool isConnected();
+ bool isTracked();
+ bool hasDataFor(uint64_t time);
+- bool hasData();
++ std::string getStreamName();
++ std::string getHost();
++ std::string getSessId();
++ std::string getCurrentProtocols();
++ uint64_t newestDataPoint();
+ uint64_t getConnTime(uint64_t time);
++ uint64_t getConnTime();
+ uint64_t getLastSecond(uint64_t time);
+ uint64_t getDown(uint64_t time);
+ uint64_t getUp();
+@@ -122,8 +98,6 @@ namespace Controller{
+ uint64_t getBpsUp(uint64_t start, uint64_t end);
+ };
+
+- extern std::map<sessIndex, statSession> sessions;
+- extern std::map<unsigned long, sessIndex> connToSession;
+ extern tthread::mutex statsMutex;
+ extern uint64_t statDropoff;
+
+@@ -155,6 +129,7 @@ namespace Controller{
+ void sessions_shutdown(const std::string &streamname, const std::string &protocol = "");
+ bool hasViewers(std::string streamName);
+ void writeSessionCache(); /*LTS*/
++ void killConnections(std::string sessId);
+
+ #define PROMETHEUS_TEXT 0
+ #define PROMETHEUS_JSON 1
+diff --git a/src/controller/controller_storage.cpp b/src/controller/controller_storage.cpp
+index 58e7b521..bdd52893 100644
+--- a/src/controller/controller_storage.cpp
++++ b/src/controller/controller_storage.cpp
+@@ -91,19 +91,6 @@ namespace Controller{
+ rlxAccs->setString("tags", tags, newEndPos);
+ rlxAccs->setEndPos(newEndPos + 1);
+ }
+- if (Triggers::shouldTrigger("USER_END", strm)){
+- std::stringstream plgen;
+- plgen << sessId << "\n"
+- << strm << "\n"
+- << conn << "\n"
+- << host << "\n"
+- << duration << "\n"
+- << up << "\n"
+- << down << "\n"
+- << tags;
+- std::string payload = plgen.str();
+- Triggers::doTrigger("USER_END", payload, strm);
+- }
+ }
+
+ void normalizeTrustedProxies(JSON::Value &tp){
+@@ -450,7 +437,8 @@ namespace Controller{
+ systemBoot = globAccX.getInt("systemBoot");
+ }
+ if(!globAccX.getFieldAccX("defaultStream")
+- || !globAccX.getFieldAccX("systemBoot")){
++ || !globAccX.getFieldAccX("systemBoot")
++ || !globAccX.getFieldAccX("sessionMode")){
+ globAccX.setReload();
+ globCfg.master = true;
+ globCfg.close();
+@@ -461,12 +449,16 @@ namespace Controller{
+ if (!globAccX.isReady()){
+ globAccX.addField("defaultStream", RAX_128STRING);
+ globAccX.addField("systemBoot", RAX_64UINT);
++ globAccX.addField("sessionMode", RAX_64UINT);
++ if (!Storage["config"]["sessionMode"]){
++ Storage["config"]["sessionMode"] = SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID;
++ }
+ globAccX.setRCount(1);
+ globAccX.setEndPos(1);
+ globAccX.setReady();
+ }
+ globAccX.setString("defaultStream", Storage["config"]["defaultStream"].asStringRef());
+- globAccX.setInt("systemBoot", systemBoot);
++ globAccX.setInt("sessionMode", Storage["config"]["sessionMode"].asInt());
+ globCfg.master = false; // leave the page after closing
+ }
+ }
+diff --git a/src/input/input.cpp b/src/input/input.cpp
+index a86fe3cd..0524f55b 100644
+--- a/src/input/input.cpp
++++ b/src/input/input.cpp
+@@ -794,7 +794,7 @@ namespace Mist{
+ void Input::streamMainLoop(){
+ uint64_t statTimer = 0;
+ uint64_t startTime = Util::bootSecs();
+- Comms::Statistics statComm;
++ Comms::Connections statComm;
+ getNext();
+ if (thisPacket && !userSelect.count(thisIdx)){
+ userSelect[thisIdx].reload(streamName, thisIdx, COMM_STATUS_ACTIVE | COMM_STATUS_SOURCE | COMM_STATUS_DONOTTRACK);
+@@ -820,7 +820,7 @@ namespace Mist{
+
+ if (Util::bootSecs() - statTimer > 1){
+ // Connect to stats for INPUT detection
+- if (!statComm){statComm.reload();}
++ if (!statComm){statComm.reload(streamName, "", JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "", SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID);}
+ if (statComm){
+ if (!statComm){
+ config->is_active = false;
+@@ -829,7 +829,6 @@ namespace Mist{
+ }
+ uint64_t now = Util::bootSecs();
+ statComm.setNow(now);
+- statComm.setCRC(getpid());
+ statComm.setStream(streamName);
+ statComm.setConnector("INPUT:" + capa["name"].asStringRef());
+ statComm.setTime(now - startTime);
+@@ -842,7 +841,7 @@ namespace Mist{
+ }
+ }
+
+- void Input::connStats(Comms::Statistics &statComm){
++ void Input::connStats(Comms::Connections &statComm){
+ statComm.setUp(0);
+ statComm.setDown(streamByteCount());
+ statComm.setHost(getConnectedBinHost());
+@@ -853,7 +852,7 @@ namespace Mist{
+ uint64_t statTimer = 0;
+ uint64_t startTime = Util::bootSecs();
+ size_t idx;
+- Comms::Statistics statComm;
++ Comms::Connections statComm;
+
+
+ DTSC::Meta liveMeta(config->getString("streamname"), false);
+@@ -985,7 +984,7 @@ namespace Mist{
+
+ if (Util::bootSecs() - statTimer > 1){
+ // Connect to stats for INPUT detection
+- if (!statComm){statComm.reload();}
++ if (!statComm){statComm.reload(streamName, "", JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "", SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID);}
+ if (statComm){
+ if (statComm.getStatus() & COMM_STATUS_REQDISCONNECT){
+ config->is_active = false;
+@@ -994,7 +993,6 @@ namespace Mist{
+ }
+ uint64_t now = Util::bootSecs();
+ statComm.setNow(now);
+- statComm.setCRC(getpid());
+ statComm.setStream(streamName);
+ statComm.setConnector("INPUT:" + capa["name"].asStringRef());
+ statComm.setTime(now - startTime);
+diff --git a/src/input/input.h b/src/input/input.h
+index 8d7e8891..ce7686c1 100644
+--- a/src/input/input.h
++++ b/src/input/input.h
+@@ -69,7 +69,7 @@ namespace Mist{
+ virtual void userOnActive(size_t id);
+ virtual void userOnDisconnect(size_t id);
+ virtual void userLeadOut();
+- virtual void connStats(Comms::Statistics & statComm);
++ virtual void connStats(Comms::Connections & statComm);
+ virtual void parseHeader();
+ bool bufferFrame(size_t track, uint32_t keyNum);
+
+diff --git a/src/input/input_rtsp.cpp b/src/input/input_rtsp.cpp
+index fef613bb..ebb812e1 100644
+--- a/src/input/input_rtsp.cpp
++++ b/src/input/input_rtsp.cpp
+@@ -196,7 +196,7 @@ namespace Mist{
+ }
+
+ void InputRTSP::streamMainLoop(){
+- Comms::Statistics statComm;
++ Comms::Connections statComm;
+ uint64_t startTime = Util::epoch();
+ uint64_t lastPing = Util::bootSecs();
+ uint64_t lastSecs = 0;
+@@ -210,7 +210,7 @@ namespace Mist{
+ if (lastSecs != currSecs){
+ lastSecs = currSecs;
+ // Connect to stats for INPUT detection
+- statComm.reload();
++ statComm.reload(streamName, "", JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "", SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID);
+ if (statComm){
+ if (statComm.getStatus() & COMM_STATUS_REQDISCONNECT){
+ config->is_active = false;
+@@ -219,7 +219,6 @@ namespace Mist{
+ }
+ uint64_t now = Util::bootSecs();
+ statComm.setNow(now);
+- statComm.setCRC(getpid());
+ statComm.setStream(streamName);
+ statComm.setConnector("INPUT:" + capa["name"].asStringRef());
+ statComm.setUp(tcpCon.dataUp());
+diff --git a/src/input/input_sdp.cpp b/src/input/input_sdp.cpp
+index 62c02aef..3169a836 100644
+--- a/src/input/input_sdp.cpp
++++ b/src/input/input_sdp.cpp
+@@ -193,7 +193,7 @@ namespace Mist{
+
+ // Updates stats and quits if parsePacket returns false
+ void InputSDP::streamMainLoop(){
+- Comms::Statistics statComm;
++ Comms::Connections statComm;
+ uint64_t startTime = Util::epoch();
+ uint64_t lastSecs = 0;
+ // Get RTP packets from UDP socket and stop if this fails
+@@ -202,7 +202,7 @@ namespace Mist{
+ if (lastSecs != currSecs){
+ lastSecs = currSecs;
+ // Connect to stats for INPUT detection
+- statComm.reload();
++ statComm.reload(streamName, "", JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "", SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID);
+ if (statComm){
+ if (statComm.getStatus() == COMM_STATUS_REQDISCONNECT){
+ config->is_active = false;
+@@ -211,7 +211,6 @@ namespace Mist{
+ }
+ uint64_t now = Util::bootSecs();
+ statComm.setNow(now);
+- statComm.setCRC(getpid());
+ statComm.setStream(streamName);
+ statComm.setConnector("INPUT:" + capa["name"].asStringRef());
+ statComm.setDown(bytesRead);
+diff --git a/src/input/input_ts.cpp b/src/input/input_ts.cpp
+index cfe3f7cd..23311dc0 100644
+--- a/src/input/input_ts.cpp
++++ b/src/input/input_ts.cpp
+@@ -527,7 +527,7 @@ namespace Mist{
+ void inputTS::streamMainLoop(){
+ meta.removeTrack(tmpIdx);
+ INFO_MSG("Removed temptrack %zu", tmpIdx);
+- Comms::Statistics statComm;
++ Comms::Connections statComm;
+ uint64_t downCounter = 0;
+ uint64_t startTime = Util::bootSecs();
+ uint64_t noDataSince = Util::bootSecs();
+@@ -621,7 +621,7 @@ namespace Mist{
+ // Check for and spawn threads here.
+ if (Util::bootSecs() - threadCheckTimer > 1){
+ // Connect to stats for INPUT detection
+- statComm.reload();
++ statComm.reload(streamName, "", JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "", SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID);
+ if (statComm){
+ if (statComm.getStatus() & COMM_STATUS_REQDISCONNECT){
+ config->is_active = false;
+@@ -630,7 +630,6 @@ namespace Mist{
+ }
+ uint64_t now = Util::bootSecs();
+ statComm.setNow(now);
+- statComm.setCRC(getpid());
+ statComm.setStream(streamName);
+ statComm.setConnector("INPUT:" + capa["name"].asStringRef());
+ statComm.setUp(0);
+diff --git a/src/input/input_tssrt.cpp b/src/input/input_tssrt.cpp
+index 4219ebbc..8fef6d7d 100644
+--- a/src/input/input_tssrt.cpp
++++ b/src/input/input_tssrt.cpp
+@@ -282,7 +282,7 @@ namespace Mist{
+
+ void inputTSSRT::setSingular(bool newSingular){singularFlag = newSingular;}
+
+- void inputTSSRT::connStats(Comms::Statistics &statComm){
++ void inputTSSRT::connStats(Comms::Connections &statComm){
+ statComm.setUp(srtConn.dataUp());
+ statComm.setDown(srtConn.dataDown());
+ statComm.setHost(getConnectedBinHost());
+diff --git a/src/input/input_tssrt.h b/src/input/input_tssrt.h
+index 4f337b48..143174cb 100644
+--- a/src/input/input_tssrt.h
++++ b/src/input/input_tssrt.h
+@@ -40,7 +40,7 @@ namespace Mist{
+
+ Socket::SRTConnection srtConn;
+ bool singularFlag;
+- virtual void connStats(Comms::Statistics &statComm);
++ virtual void connStats(Comms::Connections &statComm);
+
+ Util::ResizeablePointer rawBuffer;
+ size_t rawIdx;
+diff --git a/src/output/output.cpp b/src/output/output.cpp
+index 1fa86eee..50d67549 100644
+--- a/src/output/output.cpp
++++ b/src/output/output.cpp
+@@ -7,7 +7,7 @@
+ #include <sys/wait.h>
+ #include <unistd.h>
+
+-#include "output.h"
++#include "output.h"
+ #include <mist/bitfields.h>
+ #include <mist/defines.h>
+ #include <mist/h264.h>
+@@ -92,7 +92,7 @@ namespace Mist{
+ firstTime = 0;
+ firstPacketTime = 0xFFFFFFFFFFFFFFFFull;
+ lastPacketTime = 0;
+- crc = getpid();
++ sid = "";
+ parseData = false;
+ wantRequest = true;
+ sought = false;
+@@ -100,7 +100,7 @@ namespace Mist{
+ isBlocking = false;
+ needsLookAhead = 0;
+ extraKeepAway = 0;
+- lastStats = 0;
++ lastStats = 0xFFFFFFFFFFFFFFFFull;
+ maxSkipAhead = 7500;
+ uaDelay = 10;
+ realTime = 1000;
+@@ -111,6 +111,7 @@ namespace Mist{
+ lastPushUpdate = 0;
+ previousFile = "";
+ currentFile = "";
++ sessionMode = 0xFFFFFFFFFFFFFFFFull;
+
+ lastRecv = Util::bootSecs();
+ if (myConn){
+@@ -211,95 +212,9 @@ namespace Mist{
+ onFail("Not allowed to play (CONN_PLAY)");
+ }
+ }
+- doSync(true);
+ /*LTS-END*/
+ }
+
+- /// If called with force set to true and a USER_NEW trigger enabled, forces a sync immediately.
+- /// Otherwise, does nothing unless the sync byte is set to 2, in which case it forces a sync as well.
+- /// May be called recursively because it calls stats() which calls this function.
+- /// If this happens, the extra calls to the function return instantly.
+- void Output::doSync(bool force){
+- if (!statComm){return;}
+- if (recursingSync){return;}
+- recursingSync = true;
+- if (statComm.getSync() == 2 || force){
+- if (getStatsName() == capa["name"].asStringRef() && Triggers::shouldTrigger("USER_NEW", streamName)){
+- // sync byte 0 = no sync yet, wait for sync from controller...
+- char initialSync = 0;
+- // attempt to load sync status from session cache in shm
+- {
+- IPC::semaphore cacheLock(SEM_SESSCACHE, O_RDWR, ACCESSPERMS, 16);
+- if (cacheLock){cacheLock.wait();}
+- IPC::sharedPage shmSessions(SHM_SESSIONS, SHM_SESSIONS_SIZE, false, false);
+- if (shmSessions.mapped){
+- char shmEmpty[SHM_SESSIONS_ITEM];
+- memset(shmEmpty, 0, SHM_SESSIONS_ITEM);
+- std::string host;
+- Socket::hostBytesToStr(statComm.getHost().data(), 16, host);
+- uint32_t shmOffset = 0;
+- const std::string &cName = capa["name"].asStringRef();
+- while (shmOffset + SHM_SESSIONS_ITEM < SHM_SESSIONS_SIZE){
+- // compare crc
+- if (*(uint32_t*)(shmSessions.mapped + shmOffset) == crc){
+- // compare stream name
+- if (strncmp(shmSessions.mapped + shmOffset + 4, streamName.c_str(), 100) == 0){
+- // compare connector
+- if (strncmp(shmSessions.mapped + shmOffset + 104, cName.c_str(), 20) == 0){
+- // compare host
+- if (strncmp(shmSessions.mapped + shmOffset + 124, host.c_str(), 40) == 0){
+- initialSync = shmSessions.mapped[shmOffset + 164];
+- HIGH_MSG("Instant-sync from session cache to %u", (unsigned int)initialSync);
+- break;
+- }
+- }
+- }
+- }
+- // stop if we reached the end
+- if (memcmp(shmSessions.mapped + shmOffset, shmEmpty, SHM_SESSIONS_ITEM) == 0){
+- break;
+- }
+- shmOffset += SHM_SESSIONS_ITEM;
+- }
+- }
+- if (cacheLock){cacheLock.post();}
+- }
+- unsigned int i = 0;
+- statComm.setSync(initialSync);
+- // wait max 10 seconds for sync
+- while ((!statComm.getSync() || statComm.getSync() == 2) && i++ < 100){
+- Util::wait(100);
+- stats(true);
+- }
+- HIGH_MSG("USER_NEW sync achieved: %u", statComm.getSync());
+- // 1 = check requested (connection is new)
+- if (statComm.getSync() == 1){
+- std::string payload = streamName + "\n" + getConnectedHost() + "\n" +
+- JSON::Value(crc).asString() + "\n" + capa["name"].asStringRef() +
+- "\n" + reqUrl + "\n" + statComm.getSessId();
+- if (!Triggers::doTrigger("USER_NEW", payload, streamName)){
+- onFail("Not allowed to play (USER_NEW)");
+- statComm.setSync(100); // 100 = denied
+- }else{
+- statComm.setSync(10); // 10 = accepted
+- }
+- }
+- // 100 = denied
+- if (statComm.getSync() == 100){onFail("Not allowed to play (USER_NEW cache)");}
+- if (statComm.getSync() == 0){
+- onFail("Not allowed to play (USER_NEW init timeout)", true);
+- }
+- if (statComm.getSync() == 2){
+- onFail("Not allowed to play (USER_NEW re-init timeout)", true);
+- }
+- // anything else = accepted
+- }else{
+- statComm.setSync(10); // auto-accept if no trigger
+- }
+- }
+- recursingSync = false;
+- }
+-
+ std::string Output::getConnectedHost(){return myConn.getHost();}
+
+ std::string Output::getConnectedBinHost(){
+@@ -433,10 +348,10 @@ namespace Mist{
+
+ //Connect to stats reporting, if not connected already
+ if (!statComm){
+- statComm.reload();
++ statComm.reload(streamName, getConnectedHost(), sid, capa["name"].asStringRef(), reqUrl, sessionMode);
+ stats(true);
+ }
+-
++
+ //push inputs do not need to wait for stream to be ready for playback
+ if (isPushing()){return;}
+
+@@ -986,7 +901,7 @@ namespace Mist{
+ INFO_MSG("Will split recording every %lld seconds", atoll(targetParams["split"].c_str()));
+ targetParams["nxt-split"] = JSON::Value((int64_t)(seekPos + endRec)).asString();
+ }
+- // Duration to record in seconds. Overrides recstop.
++ // Duration to record in seconds. Oversides recstop.
+ if (targetParams.count("duration")){
+ long long endRec = atoll(targetParams["duration"].c_str()) * 1000;
+ targetParams["recstop"] = JSON::Value((int64_t)(seekPos + endRec)).asString();
+@@ -1301,6 +1216,7 @@ namespace Mist{
+ /// request URL (if any)
+ /// ~~~~~~~~~~~~~~~
+ int Output::run(){
++ sessionMode = Util::getGlobalConfig("sessionMode").asInt();
+ /*LTS-START*/
+ // Connect to file target, if needed
+ if (isFileTarget()){
+@@ -1507,6 +1423,8 @@ namespace Mist{
+ /*LTS-END*/
+
+ disconnect();
++ stats(true);
++ userSelect.clear();
+ myConn.close();
+ return 0;
+ }
+@@ -1822,7 +1740,7 @@ namespace Mist{
+ // also cancel if it has been less than a second since the last update
+ // unless force is set to true
+ uint64_t now = Util::bootSecs();
+- if (now == lastStats && !force){return;}
++ if (now <= lastStats && !force){return;}
+
+ if (isRecording()){
+ if(lastPushUpdate == 0){
+@@ -1861,13 +1779,17 @@ namespace Mist{
+ }
+ }
+
+- if (!statComm){statComm.reload();}
+- if (!statComm){return;}
++ if (!statComm){statComm.reload(streamName, getConnectedHost(), sid, capa["name"].asStringRef(), reqUrl, sessionMode);}
++ if (!statComm){return;}
++ if (statComm.getExit()){
++ onFail("Shutting down since this session is not allowed to view this stream");
++ return;
++ }
+
+ lastStats = now;
+
+- VERYHIGH_MSG("Writing stats: %s, %s, %u, %" PRIu64 ", %" PRIu64, getConnectedHost().c_str(), streamName.c_str(),
+- crc & 0xFFFFFFFFu, myConn.dataUp(), myConn.dataDown());
++ VERYHIGH_MSG("Writing stats: %s, %s, %s, %" PRIu64 ", %" PRIu64, getConnectedHost().c_str(), streamName.c_str(),
++ sid.c_str(), myConn.dataUp(), myConn.dataDown());
+ /*LTS-START*/
+ if (statComm.getStatus() & COMM_STATUS_REQDISCONNECT){
+ onFail("Shutting down on controller request");
+@@ -1875,9 +1797,6 @@ namespace Mist{
+ }
+ /*LTS-END*/
+ statComm.setNow(now);
+- statComm.setHost(getConnectedBinHost());
+- statComm.setCRC(crc);
+- statComm.setStream(streamName);
+ statComm.setConnector(getStatsName());
+ connStats(now, statComm);
+ statComm.setLastSecond(thisPacket ? thisPacket.getTime() : 0);
+@@ -1887,7 +1806,7 @@ namespace Mist{
+ // Tag the session with the user agent
+ if (newUA && ((now - myConn.connTime()) >= uaDelay || !myConn) && UA.size()){
+ std::string APIcall =
+- "{\"tag_sessid\":{\"" + statComm.getSessId() + "\":" + JSON::string_escape("UA:" + UA) + "}}";
++ "{\"tag_sessid\":{\"" + statComm.sessionId + "\":" + JSON::string_escape("UA:" + UA) + "}}";
+ Socket::UDPConnection uSock;
+ uSock.SetDestination(UDP_API_HOST, UDP_API_PORT);
+ uSock.SendNow(APIcall);
+@@ -1895,8 +1814,6 @@ namespace Mist{
+ }
+ /*LTS-END*/
+
+- doSync();
+-
+ if (isPushing()){
+ for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
+ if (it->second.getStatus() & COMM_STATUS_REQDISCONNECT){
+@@ -1912,7 +1829,7 @@ namespace Mist{
+ }
+ }
+
+- void Output::connStats(uint64_t now, Comms::Statistics &statComm){
++ void Output::connStats(uint64_t now, Comms::Connections &statComm){
+ statComm.setUp(myConn.dataUp());
+ statComm.setDown(myConn.dataDown());
+ statComm.setTime(now - myConn.connTime());
+diff --git a/src/output/output.h b/src/output/output.h
+index 84d17482..173b3840 100644
+--- a/src/output/output.h
++++ b/src/output/output.h
+@@ -86,7 +86,6 @@ namespace Mist{
+ std::string hostLookup(std::string ip);
+ bool onList(std::string ip, std::string list);
+ std::string getCountry(std::string ip);
+- void doSync(bool force = false);
+ /*LTS-END*/
+ std::map<size_t, uint32_t> currentPage;
+ void loadPageForKey(size_t trackId, size_t keyNum);
+@@ -105,6 +104,7 @@ namespace Mist{
+ bool firstData;
+ uint64_t lastPushUpdate;
+ bool newUA;
++
+ protected: // these are to be messed with by child classes
+ virtual bool inlineRestartCapable() const{
+ return false;
+@@ -122,15 +122,16 @@ namespace Mist{
+ virtual std::string getStatsName();
+ virtual bool hasSessionIDs(){return false;}
+
+- virtual void connStats(uint64_t now, Comms::Statistics &statComm);
++ virtual void connStats(uint64_t now, Comms::Connections &statComm);
+
+ std::set<size_t> getSupportedTracks(const std::string &type = "") const;
+
+ inline virtual bool keepGoing(){return config->is_active && myConn;}
+
+- Comms::Statistics statComm;
++ Comms::Connections statComm;
+ bool isBlocking; ///< If true, indicates that myConn is blocking.
+- uint32_t crc; ///< Checksum, if any, for usage in the stats.
++ std::string sid; ///< Random identifier used to split connections into sessions
++ uint64_t sessionMode;
+ uint64_t nextKeyTime();
+
+ // stream delaying variables
+diff --git a/src/output/output_cmaf.cpp b/src/output/output_cmaf.cpp
+index 4582a8b6..c2fbc5bc 100644
+--- a/src/output/output_cmaf.cpp
++++ b/src/output/output_cmaf.cpp
+@@ -101,7 +101,7 @@ namespace Mist{
+ }
+ }
+
+- void OutCMAF::connStats(uint64_t now, Comms::Statistics &statComm){
++ void OutCMAF::connStats(uint64_t now, Comms::Connections &statComm){
+ // For non-push usage, call usual function.
+ if (!isRecording()){
+ Output::connStats(now, statComm);
+diff --git a/src/output/output_cmaf.h b/src/output/output_cmaf.h
+index efc36511..390549cc 100644
+--- a/src/output/output_cmaf.h
++++ b/src/output/output_cmaf.h
+@@ -39,7 +39,7 @@ namespace Mist{
+ bool isReadyForPlay();
+
+ protected:
+- virtual void connStats(uint64_t now, Comms::Statistics &statComm);
++ virtual void connStats(uint64_t now, Comms::Connections &statComm);
+ void onTrackEnd(size_t idx);
+ bool hasSessionIDs(){return !config->getBool("mergesessions");}
+
+@@ -72,6 +72,7 @@ namespace Mist{
+ void startPushOut();
+ void pushNext();
+
++ uint32_t crc;
+ HTTP::URL pushUrl;
+ std::map<size_t, CMAFPushTrack> pushTracks;
+ void setupTrackObject(size_t idx);
+diff --git a/src/output/output_http.cpp b/src/output/output_http.cpp
+index 252f069a..f55b974d 100644
+--- a/src/output/output_http.cpp
++++ b/src/output/output_http.cpp
+@@ -55,7 +55,6 @@ namespace Mist{
+ }
+
+ void HTTPOutput::onFail(const std::string &msg, bool critical){
+- INFO_MSG("Failing '%s': %s", H.url.c_str(), msg.c_str());
+ if (!webSock && !isRecording() && !responded){
+ H.Clean(); // make sure no parts of old requests are left in any buffers
+ H.SetHeader("Server", APPIDENT);
+@@ -238,18 +237,6 @@ namespace Mist{
+ }
+ /*LTS-END*/
+ if (H.hasHeader("User-Agent")){UA = H.GetHeader("User-Agent");}
+- if (hasSessionIDs()){
+- if (H.GetVar("sessId").size()){
+- std::string ua = H.GetVar("sessId");
+- crc = checksum::crc32(0, ua.data(), ua.size());
+- }else{
+- std::string ua = JSON::Value(getpid()).asString();
+- crc = checksum::crc32(0, ua.data(), ua.size());
+- }
+- }else{
+- std::string mixed_ua = UA + H.GetHeader("X-Playback-Session-Id");
+- crc = checksum::crc32(0, mixed_ua.data(), mixed_ua.size());
+- }
+
+ if (H.GetVar("audio") != ""){targetParams["audio"] = H.GetVar("audio");}
+ if (H.GetVar("video") != ""){targetParams["video"] = H.GetVar("video");}
+@@ -281,6 +268,21 @@ namespace Mist{
+ realTime = 0;
+ }
+ }
++ // Get session ID cookie or generate a random one if it wasn't set
++ if (!sid.size()){
++ std::map<std::string, std::string> storage;
++ const std::string koekjes = H.GetHeader("Cookie");
++ HTTP::parseVars(koekjes, storage);
++ if (storage.count("sid")){
++ // Get sid cookie, which is used to divide connections into sessions
++ sid = storage.at("sid");
++ }else{
++ // Else generate one
++ const std::string newSid = UA + JSON::Value(getpid()).asString();
++ sid = JSON::Value(checksum::crc32(0, newSid.data(), newSid.size())).asString();
++ H.SetHeader("sid", sid.c_str());
++ }
++ }
+ // Handle upgrade to websocket if the output supports it
+ std::string upgradeHeader = H.GetHeader("Upgrade");
+ Util::stringToLower(upgradeHeader);
+diff --git a/src/output/output_tssrt.cpp b/src/output/output_tssrt.cpp
+index db07dc91..dc04b247 100644
+--- a/src/output/output_tssrt.cpp
++++ b/src/output/output_tssrt.cpp
+@@ -344,7 +344,7 @@ namespace Mist{
+ }
+ }
+
+- void OutTSSRT::connStats(uint64_t now, Comms::Statistics &statComm){
++ void OutTSSRT::connStats(uint64_t now, Comms::Connections &statComm){
+ if (!srtConn){return;}
+ statComm.setUp(srtConn.dataUp());
+ statComm.setDown(srtConn.dataDown());
+diff --git a/src/output/output_tssrt.h b/src/output/output_tssrt.h
+index 1423af8d..71c9b72f 100644
+--- a/src/output/output_tssrt.h
++++ b/src/output/output_tssrt.h
+@@ -15,7 +15,7 @@ namespace Mist{
+ bool isReadyForPlay(){return true;}
+ virtual void requestHandler();
+ protected:
+- virtual void connStats(uint64_t now, Comms::Statistics &statComm);
++ virtual void connStats(uint64_t now, Comms::Connections &statComm);
+ virtual std::string getConnectedHost(){return srtConn.remotehost;}
+ virtual std::string getConnectedBinHost(){return srtConn.getBinHost();}
+
+diff --git a/src/output/output_webrtc.cpp b/src/output/output_webrtc.cpp
+index b4289b64..058cd737 100644
+--- a/src/output/output_webrtc.cpp
++++ b/src/output/output_webrtc.cpp
+@@ -1015,7 +1015,7 @@ namespace Mist{
+ }
+ }
+
+- void OutWebRTC::connStats(uint64_t now, Comms::Statistics &statComm){
++ void OutWebRTC::connStats(uint64_t now, Comms::Connections &statComm){
+ statComm.setUp(myConn.dataUp());
+ statComm.setDown(myConn.dataDown());
+ statComm.setPacketCount(totalPkts);
+diff --git a/src/output/output_webrtc.h b/src/output/output_webrtc.h
+index 9c3db580..b2b528c5 100644
+--- a/src/output/output_webrtc.h
++++ b/src/output/output_webrtc.h
+@@ -144,7 +144,7 @@ namespace Mist{
+ void onDTSCConverterHasInitData(const size_t trackID, const std::string &initData);
+ void onRTPPacketizerHasRTPPacket(const char *data, size_t nbytes);
+ void onRTPPacketizerHasRTCPPacket(const char *data, uint32_t nbytes);
+- virtual void connStats(uint64_t now, Comms::Statistics &statComm);
++ virtual void connStats(uint64_t now, Comms::Connections &statComm);
+
+ private:
+ uint64_t lastRecv;
+diff --git a/src/process/process_exec.cpp b/src/process/process_exec.cpp
+index 7b775848..34ac53fd 100644
+--- a/src/process/process_exec.cpp
++++ b/src/process/process_exec.cpp
+@@ -72,7 +72,7 @@ namespace Mist{
+ }
+ bool needsLock(){return false;}
+ bool isSingular(){return false;}
+- void connStats(Comms::Statistics &statComm){
++ void connStats(Comms::Connections &statComm){
+ for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
+ if (it->second){it->second.setStatus(COMM_STATUS_DONOTTRACK | it->second.getStatus());}
+ }
+@@ -117,7 +117,7 @@ namespace Mist{
+ realTime = 0;
+ OutEBML::sendHeader();
+ };
+- void connStats(uint64_t now, Comms::Statistics &statComm){
++ void connStats(uint64_t now, Comms::Connections &statComm){
+ for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
+ if (it->second){it->second.setStatus(COMM_STATUS_DONOTTRACK | it->second.getStatus());}
+ }
+diff --git a/src/session.cpp b/src/session.cpp
+new file mode 100644
+index 00000000..3865e0ec
+--- /dev/null
++++ b/src/session.cpp
+@@ -0,0 +1,367 @@
++#include <mist/defines.h>
++#include <mist/stream.h>
++#include <mist/util.h>
++#include <mist/config.h>
++#include <mist/auth.h>
++#include <mist/comms.h>
++#include <mist/triggers.h>
++#include <signal.h>
++#include <stdio.h>
++// Stats of connections which have closed are added to these global counters
++uint64_t globalNow = 0;
++uint64_t globalTime = 0;
++uint64_t globalDown = 0;
++uint64_t globalUp = 0;
++uint64_t globalPktcount = 0;
++uint64_t globalPktloss = 0;
++uint64_t globalPktretrans = 0;
++// Counts the duration a connector has been active
++std::map<std::string, uint64_t> connectorCount;
++std::map<std::string, uint64_t> connectorLastActive;
++// Set to True when a session gets invalidated, so that we know to run a new USER_NEW trigger
++bool forceTrigger = false;
++void handleSignal(int signum){
++ if (signum == SIGUSR1){
++ forceTrigger = true;
++ }
++}
++
++void userOnActive(uint64_t &connections){
++ ++connections;
++}
++
++std::string getEnvWithDefault(const std::string variableName, const std::string defaultValue){
++ const char* value = getenv(variableName.c_str());
++ if (value){
++ unsetenv(variableName.c_str());
++ return value;
++ }else{
++ return defaultValue;
++ }
++}
++
++/// \brief Adds stats of closed connections to global counters
++void userOnDisconnect(Comms::Connections & connections, size_t idx){
++ std::string thisConnector = connections.getConnector(idx);
++ if (thisConnector != ""){
++ connectorCount[thisConnector] += connections.getTime(idx);
++ }
++ globalTime += connections.getTime(idx);
++ globalDown += connections.getDown(idx);
++ globalUp += connections.getUp(idx);
++ globalPktcount += connections.getPacketCount(idx);
++ globalPktloss += connections.getPacketLostCount(idx);
++ globalPktretrans += connections.getPacketRetransmitCount(idx);
++}
++
++int main(int argc, char **argv){
++ Comms::Connections connections;
++ Comms::Sessions sessions;
++ uint64_t lastSeen = Util::bootSecs();
++ uint64_t currentConnections = 0;
++ Util::redirectLogsIfNeeded();
++ signal(SIGUSR1, handleSignal);
++ // Init config and parse arguments
++ Util::Config config = Util::Config("MistSession");
++ JSON::Value option;
++
++ option.null();
++ option["arg_num"] = 1;
++ option["arg"] = "string";
++ option["help"] = "Session identifier of the entire session";
++ option["default"] = "";
++ config.addOption("sessionid", option);
++
++ option.null();
++ option["long"] = "sessionmode";
++ option["short"] = "m";
++ option["arg"] = "integer";
++ option["default"] = 0;
++ config.addOption("sessionmode", option);
++
++ option.null();
++ option["long"] = "streamname";
++ option["short"] = "n";
++ option["arg"] = "string";
++ option["default"] = "";
++ config.addOption("streamname", option);
++
++ option.null();
++ option["long"] = "ip";
++ option["short"] = "i";
++ option["arg"] = "string";
++ option["default"] = "";
++ config.addOption("ip", option);
++
++ option.null();
++ option["long"] = "sid";
++ option["short"] = "s";
++ option["arg"] = "string";
++ option["default"] = "";
++ config.addOption("sid", option);
++
++ option.null();
++ option["long"] = "protocol";
++ option["short"] = "p";
++ option["arg"] = "string";
++ option["default"] = "";
++ config.addOption("protocol", option);
++
++ option.null();
++ option["long"] = "requrl";
++ option["short"] = "r";
++ option["arg"] = "string";
++ option["default"] = "";
++ config.addOption("requrl", option);
++
++ config.activate();
++ if (!(config.parseArgs(argc, argv))){
++ FAIL_MSG("Cannot start a new session due to invalid arguments");
++ return 1;
++ }
++
++ const uint64_t bootTime = Util::getMicros();
++ // Get session ID, session mode and other variables used as payload for the USER_NEW and USER_END triggers
++ const std::string thisStreamName = config.getString("streamname");
++ const std::string thisHost = config.getString("ip");
++ const std::string thisSid = config.getString("sid");
++ const std::string thisProtocol = config.getString("protocol");
++ const std::string thisReqUrl = config.getString("requrl");
++ const std::string thisSessionId = config.getString("sessionid");
++ const uint64_t sessionMode = config.getInteger("sessionmode");
++
++ if (thisSessionId == "" || thisProtocol == "" || thisStreamName == ""){
++ FAIL_MSG("Given the following incomplete arguments: SessionId: '%s', protocol: '%s', stream name: '%s'. Aborting opening a new session",
++ thisSessionId.c_str(), thisProtocol.c_str(), thisStreamName.c_str());
++ return 1;
++ }
++
++ MEDIUM_MSG("Starting a new session for sessionId '%s'", thisSessionId.c_str());
++ if (sessionMode < 1 || sessionMode > 15) {
++ FAIL_MSG("Invalid session mode of value %lu. Should be larger than 0 and smaller than 16", sessionMode);
++ return 1;
++ }
++
++ // Try to lock to ensure we are the only process initialising this session
++ IPC::semaphore sessionLock;
++ char semName[NAME_BUFFER_SIZE];
++ snprintf(semName, NAME_BUFFER_SIZE, SEM_SESSION, thisSessionId.c_str());
++ sessionLock.open(semName, O_CREAT | O_RDWR, ACCESSPERMS, 1);
++ // If the lock fails, the previous Session process must've failed in spectacular fashion
++ // It's the Controller's task to clean everything up. When the lock fails, this cleanup hasn't happened yet
++ if (!sessionLock.tryWaitOneSecond()){
++ FAIL_MSG("Session '%s' already locked", thisSessionId.c_str());
++ return 1;
++ }
++
++ // Check if a page already exists for this session ID. If so, quit
++ {
++ IPC::sharedPage dataPage;
++ char userPageName[NAME_BUFFER_SIZE];
++ snprintf(userPageName, NAME_BUFFER_SIZE, COMMS_SESSIONS, thisSessionId.c_str());
++ dataPage.init(userPageName, 0, false, false);
++ if (dataPage){
++ INFO_MSG("Session '%s' already has a running process", thisSessionId.c_str());
++ sessionLock.post();
++ return 0;
++ }
++ }
++
++ // Claim a spot in shared memory for this session on the global statistics page
++ sessions.reload();
++ if (!sessions){
++ FAIL_MSG("Unable to register entry for session '%s' on the stats page", thisSessionId.c_str());
++ sessionLock.post();
++ return 1;
++ }
++ // Open the shared memory page containing statistics for each individual connection in this session
++ connections.reload(thisStreamName, thisHost, thisSid, thisProtocol, thisReqUrl, sessionMode, true, false);
++ // Initialise global session data
++ sessions.setHost(thisHost);
++ sessions.setSessId(thisSessionId);
++ sessions.setStream(thisStreamName);
++ sessionLock.post();
++
++ // Determine session type, since triggers only get run for viewer type sessions
++ uint64_t thisType = 0;
++ if (thisSessionId[0] == 'I'){
++ INFO_MSG("Started new input session %s in %lu microseconds", thisSessionId.c_str(), Util::getMicros(bootTime));
++ thisType = 1;
++ }
++ else if (thisSessionId[0] == 'O'){
++ INFO_MSG("Started new output session %s in %lu microseconds", thisSessionId.c_str(), Util::getMicros(bootTime));
++ thisType = 2;
++ }
++ else{
++ INFO_MSG("Started new viewer session %s in %lu microseconds", thisSessionId.c_str(), Util::getMicros(bootTime));
++ }
++
++ // Do a USER_NEW trigger if it is defined for this stream
++ if (!thisType && Triggers::shouldTrigger("USER_NEW", thisStreamName)){
++ std::string payload = thisStreamName + "\n" + thisHost + "\n" +
++ thisSid + "\n" + thisProtocol +
++ "\n" + thisReqUrl + "\n" + thisSessionId;
++ if (!Triggers::doTrigger("USER_NEW", payload, thisStreamName)){
++ // Mark all connections of this session as finished, since this viewer is not allowed to view this stream
++ connections.setExit();
++ connections.finishAll();
++ }
++ }
++
++ uint64_t lastSecond = 0;
++ uint64_t now = 0;
++ uint64_t time = 0;
++ uint64_t down = 0;
++ uint64_t up = 0;
++ uint64_t pktcount = 0;
++ uint64_t pktloss = 0;
++ uint64_t pktretrans = 0;
++ std::string connector = "";
++ // Stay active until Mist exits or we no longer have an active connection
++ while (config.is_active && (currentConnections || Util::bootSecs() - lastSeen <= 10)){
++ time = 0;
++ connector = "";
++ down = 0;
++ up = 0;
++ pktcount = 0;
++ pktloss = 0;
++ pktretrans = 0;
++ currentConnections = 0;
++
++ // Count active connections
++ COMM_LOOP(connections, userOnActive(currentConnections), userOnDisconnect(connections, id));
++ // Loop through all connection entries to get a summary of statistics
++ for (uint64_t idx = 0; idx < connections.recordCount(); idx++){
++ if (connections.getStatus(idx) == COMM_STATUS_INVALID || connections.getStatus(idx) & COMM_STATUS_DISCONNECT){continue;}
++ uint64_t thisLastSecond = connections.getLastSecond(idx);
++ std::string thisConnector = connections.getConnector(idx);
++ // Save info on the latest active connection separately
++ if (thisLastSecond > lastSecond){
++ lastSecond = thisLastSecond;
++ now = connections.getNow(idx);
++ }
++ connectorLastActive[thisConnector] = thisLastSecond;
++ // Sum all other variables
++ time += connections.getTime(idx);
++ down += connections.getDown(idx);
++ up += connections.getUp(idx);
++ pktcount += connections.getPacketCount(idx);
++ pktloss += connections.getPacketLostCount(idx);
++ pktretrans += connections.getPacketRetransmitCount(idx);
++ }
++
++ // Convert connector duration to string
++ std::stringstream connectorSummary;
++ bool addDelimiter = false;
++ connectorSummary << "{";
++ for (std::map<std::string, uint64_t>::iterator it = connectorLastActive.begin();
++ it != connectorLastActive.end(); ++it){
++ if (lastSecond - it->second < 10000){
++ connectorSummary << (addDelimiter ? "," : "") << it->first;
++ addDelimiter = true;
++ }
++ }
++ connectorSummary << "}";
++
++ // Write summary to global statistics
++ sessions.setTime(time + globalTime);
++ sessions.setDown(down + globalDown);
++ sessions.setUp(up + globalUp);
++ sessions.setPacketCount(pktcount + globalPktcount);
++ sessions.setPacketLostCount(pktloss + globalPktloss);
++ sessions.setPacketRetransmitCount(pktretrans + globalPktretrans);
++ sessions.setLastSecond(lastSecond);
++ sessions.setConnector(connectorSummary.str());
++ sessions.setNow(now);
++
++ // Retrigger USER_NEW if a re-sync was requested
++ if (!thisType && forceTrigger){
++ forceTrigger = false;
++ if (Triggers::shouldTrigger("USER_NEW", thisStreamName)){
++ INFO_MSG("Triggering USER_NEW for stream %s", thisStreamName.c_str());
++ std::string payload = thisStreamName + "\n" + thisHost + "\n" +
++ thisSid + "\n" + thisProtocol +
++ "\n" + thisReqUrl + "\n" + thisSessionId;
++ if (!Triggers::doTrigger("USER_NEW", payload, thisStreamName)){
++ INFO_MSG("USER_NEW rejected stream %s", thisStreamName.c_str());
++ connections.setExit();
++ connections.finishAll();
++ }else{
++ INFO_MSG("USER_NEW accepted stream %s", thisStreamName.c_str());
++ }
++ }
++ }
++
++ // Invalidate connections if the session is marked as invalid
++ if(connections.getExit()){
++ connections.finishAll();
++ break;
++ }
++ // Remember latest activity so we know when this session ends
++ if (currentConnections){
++ lastSeen = Util::bootSecs();
++ }
++ Util::sleep(1000);
++ }
++
++ // Trigger USER_END
++ if (!thisType && Triggers::shouldTrigger("USER_END", thisStreamName)){
++ lastSecond = 0;
++ time = 0;
++ down = 0;
++ up = 0;
++
++ // Get a final summary of this session
++ for (uint64_t idx = 0; idx < connections.recordCount(); idx++){
++ if (connections.getStatus(idx) == COMM_STATUS_INVALID || connections.getStatus(idx) & COMM_STATUS_DISCONNECT){continue;}
++ uint64_t thisLastSecond = connections.getLastSecond(idx);
++ // Set last second to the latest entry
++ if (thisLastSecond > lastSecond){
++ lastSecond = thisLastSecond;
++ }
++ // Count protocol durations across the entire session
++ std::string thisConnector = connections.getConnector(idx);
++ if (thisConnector != ""){
++ connectorCount[thisConnector] += connections.getTime(idx);
++ }
++ // Sum all other variables
++ time += connections.getTime(idx);
++ down += connections.getDown(idx);
++ up += connections.getUp(idx);
++ }
++
++ // Convert connector duration to string
++ std::stringstream connectorSummary;
++ bool addDelimiter = false;
++ connectorSummary << "{";
++ for (std::map<std::string, uint64_t>::iterator it = connectorCount.begin();
++ it != connectorCount.end(); ++it){
++ connectorSummary << (addDelimiter ? "," : "") << it->first << ":" << it->second;
++ addDelimiter = true;
++ }
++ connectorSummary << "}";
++
++ const uint64_t duration = lastSecond - (bootTime / 1000);
++ std::stringstream summary;
++ summary << thisSessionId << "\n"
++ << thisStreamName << "\n"
++ << connectorSummary.str() << "\n"
++ << thisHost << "\n"
++ << duration << "\n"
++ << up << "\n"
++ << down << "\n"
++ << sessions.getTags();
++ Triggers::doTrigger("USER_END", summary.str(), thisStreamName);
++ }
++
++ if (!thisType && connections.getExit()){
++ WARN_MSG("Session %s has been invalidated since it is not allowed to view stream %s", thisSessionId.c_str(), thisStreamName.c_str());
++ uint64_t sleepStart = Util::bootSecs();
++ // Keep session invalidated for 10 minutes, or until the session stops
++ while (config.is_active && sleepStart - Util::bootSecs() < 600){
++ Util::sleep(1000);
++ }
++ }
++ INFO_MSG("Shutting down session %s", thisSessionId.c_str());
++ return 0;
++}
+--
+2.25.1
+
+
+From 8ac486b815cac711c434422a1b0955486c28bb68 Mon Sep 17 00:00:00 2001
+From: Marco van Dijk <marco@stronk.rocks>
+Date: Wed, 16 Mar 2022 13:46:14 +0100
+Subject: [PATCH 27/38] Completed new sessions system
+
+Co-authored-by: Thulinma <jaron@vietors.com>
+---
+ lib/comms.cpp | 206 ++++++++----
+ lib/comms.h | 36 +-
+ lib/defines.h | 2 +-
+ lib/hls_support.cpp | 12 +-
+ lib/http_parser.cpp | 6 +-
+ lib/http_parser.h | 2 +-
+ lib/socket.cpp | 10 +
+ lib/socket.h | 1 +
+ lib/websocket.cpp | 22 +-
+ lib/websocket.h | 2 +-
+ src/controller/controller.cpp | 18 +
+ src/controller/controller_api.cpp | 11 +-
+ src/controller/controller_statistics.cpp | 375 +++++++++++++--------
+ src/controller/controller_statistics.h | 31 +-
+ src/controller/controller_storage.cpp | 28 +-
+ src/controller/controller_storage.h | 1 +
+ src/input/input.cpp | 21 +-
+ src/input/input_rtsp.cpp | 3 +-
+ src/input/input_sdp.cpp | 3 +-
+ src/input/input_ts.cpp | 3 +-
+ src/input/input_tsrist.cpp | 2 +-
+ src/input/input_tsrist.h | 2 +-
+ src/output/output.cpp | 40 ++-
+ src/output/output.h | 3 +-
+ src/output/output_cmaf.cpp | 27 +-
+ src/output/output_hls.cpp | 28 +-
+ src/output/output_http.cpp | 51 ++-
+ src/output/output_http_internal.cpp | 179 +++++-----
+ src/output/output_http_internal.h | 8 +-
+ src/output/output_sdp.cpp | 12 +
+ src/output/output_sdp.h | 2 +
+ src/output/output_ts.cpp | 12 +
+ src/output/output_ts.h | 2 +
+ src/output/output_tsrist.cpp | 16 +-
+ src/output/output_tsrist.h | 4 +-
+ src/session.cpp | 410 +++++++++++++----------
+ 36 files changed, 981 insertions(+), 610 deletions(-)
+
+diff --git a/lib/comms.cpp b/lib/comms.cpp
+index 85f1e6d7..14faa980 100644
+--- a/lib/comms.cpp
++++ b/lib/comms.cpp
+@@ -3,6 +3,7 @@
+ #include "comms.h"
+ #include "defines.h"
+ #include "encode.h"
++#include "stream.h"
+ #include "procs.h"
+ #include "timing.h"
+ #include <fcntl.h>
+@@ -10,6 +11,34 @@
+ #include "config.h"
+
+ namespace Comms{
++ uint8_t sessionViewerMode = SESS_BUNDLE_DEFAULT_VIEWER;
++ uint8_t sessionInputMode = SESS_BUNDLE_DEFAULT_OTHER;
++ uint8_t sessionOutputMode = SESS_BUNDLE_DEFAULT_OTHER;
++ uint8_t sessionUnspecifiedMode = 0;
++ uint8_t sessionStreamInfoMode = SESS_DEFAULT_STREAM_INFO_MODE;
++ uint8_t tknMode = SESS_TKN_DEFAULT_MODE;
++
++ /// \brief Refreshes the session configuration if the last update was more than 5 seconds ago
++ void sessionConfigCache(){
++ static uint64_t lastUpdate = 0;
++ if (Util::bootSecs() > lastUpdate + 5){
++ VERYHIGH_MSG("Updating session config");
++ JSON::Value tmpVal = Util::getGlobalConfig("sessionViewerMode");
++ if (!tmpVal.isNull()){ sessionViewerMode = tmpVal.asInt(); }
++ tmpVal = Util::getGlobalConfig("sessionInputMode");
++ if (!tmpVal.isNull()){ sessionInputMode = tmpVal.asInt(); }
++ tmpVal = Util::getGlobalConfig("sessionOutputMode");
++ if (!tmpVal.isNull()){ sessionOutputMode = tmpVal.asInt(); }
++ tmpVal = Util::getGlobalConfig("sessionUnspecifiedMode");
++ if (!tmpVal.isNull()){ sessionUnspecifiedMode = tmpVal.asInt(); }
++ tmpVal = Util::getGlobalConfig("sessionStreamInfoMode");
++ if (!tmpVal.isNull()){ sessionStreamInfoMode = tmpVal.asInt(); }
++ tmpVal = Util::getGlobalConfig("tknMode");
++ if (!tmpVal.isNull()){ tknMode = tmpVal.asInt(); }
++ lastUpdate = Util::bootSecs();
++ }
++ }
++
+ Comms::Comms(){
+ index = INVALID_RECORD_INDEX;
+ currentSize = 0;
+@@ -17,7 +46,7 @@ namespace Comms{
+ }
+
+ Comms::~Comms(){
+- if (index != INVALID_RECORD_INDEX){
++ if (index != INVALID_RECORD_INDEX && status){
+ setStatus(COMM_STATUS_DISCONNECT | getStatus());
+ }
+ if (master){
+@@ -123,6 +152,10 @@ namespace Comms{
+ return;
+ }
+ dataAccX = Util::RelAccX(dataPage.mapped);
++ if (dataAccX.isExit()){
++ dataPage.close();
++ return;
++ }
+ fieldAccess();
+ if (index == INVALID_RECORD_INDEX || reIssue){
+ size_t reqCount = dataAccX.getRCount();
+@@ -170,19 +203,30 @@ namespace Comms{
+
+ void Sessions::addFields(){
+ Connections::addFields();
++ dataAccX.addField("tags", RAX_STRING, 512);
+ dataAccX.addField("sessid", RAX_STRING, 80);
+ }
+
+ void Sessions::nullFields(){
+ Connections::nullFields();
+ setSessId("");
++ setTags("");
+ }
+
+ void Sessions::fieldAccess(){
+ Connections::fieldAccess();
++ tags = dataAccX.getFieldAccX("tags");
+ sessId = dataAccX.getFieldAccX("sessid");
+ }
+
++ std::string Sessions::getTags() const{return tags.string(index);}
++ std::string Sessions::getTags(size_t idx) const{return (master ? tags.string(idx) : 0);}
++ void Sessions::setTags(std::string _sid){tags.set(_sid, index);}
++ void Sessions::setTags(std::string _sid, size_t idx){
++ if (!master){return;}
++ tags.set(_sid, idx);
++ }
++
+ Users::Users() : Comms(){}
+
+ Users::Users(const Users &rhs) : Comms(){
+@@ -251,31 +295,60 @@ namespace Comms{
+ keyNum.set(_keyNum, idx);
+ }
+
++
++
++ void Connections::reload(const std::string & sessId, bool _master, bool reIssue){
++ // Open SEM_SESSION
++ if(!sem){
++ char semName[NAME_BUFFER_SIZE];
++ snprintf(semName, NAME_BUFFER_SIZE, SEM_SESSION, sessId.c_str());
++ sem.open(semName, O_RDWR, ACCESSPERMS, 1);
++ if (!sem){return;}
++ }
++ char userPageName[NAME_BUFFER_SIZE];
++ snprintf(userPageName, NAME_BUFFER_SIZE, COMMS_SESSIONS, sessId.c_str());
++ Comms::reload(userPageName, COMMS_SESSIONS_INITSIZE, _master, reIssue);
++ }
++
+ /// \brief Claims a spot on the connections page for the input/output which calls this function
+ /// Starts the MistSession binary for each session, which handles the statistics
+ /// and the USER_NEW and USER_END triggers
+ /// \param streamName: Name of the stream the input is providing or an output is making available to viewers
+ /// \param ip: IP address of the viewer which wants to access streamName. For inputs this value can be set to any value
+- /// \param sid: Session ID given by the player or randomly generated
++ /// \param tkn: Session token given by the player or randomly generated
+ /// \param protocol: Protocol currently in use for this connection
+- /// \param sessionMode: Determines how a viewer session is defined:
+- // If set to 0, all connections with the same viewer IP and stream name are bundled.
+- // If set to 1, all connections with the same viewer IP and player ID are bundled.
+- // If set to 2, all connections with the same player ID and stream name are bundled.
+- // If set to 3, all connections with the same viewer IP, player ID and stream name are bundled.
+ /// \param _master: If True, we are reading from this page. If False, we are writing (to our entry) on this page
+ /// \param reIssue: If True, claim a new entry on this page
+- void Connections::reload(std::string streamName, std::string ip, std::string sid, std::string protocol, std::string reqUrl, uint64_t sessionMode, bool _master, bool reIssue){
+- if (sessionMode == 0xFFFFFFFFFFFFFFFFull){
+- FAIL_MSG("The session mode was not initialised properly. Assuming default behaviour of bundling by viewer IP, stream name and player id");
+- sessionMode = SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID;
+- }
++ void Connections::reload(const std::string & streamName, const std::string & ip, const std::string & tkn, const std::string & protocol, const std::string & reqUrl, bool _master, bool reIssue){
++ initialTkn = tkn;
++ uint8_t sessMode = sessionViewerMode;
+ // Generate a unique session ID for each viewer, input or output
+- sessionId = generateSession(streamName, ip, sid, protocol, sessionMode);
+ if (protocol.size() >= 6 && protocol.substr(0, 6) == "INPUT:"){
+- sessionId = "I" + sessionId;
++ sessMode = sessionInputMode;
++ sessionId = "I" + generateSession(streamName, ip, tkn, protocol, sessMode);
+ }else if (protocol.size() >= 7 && protocol.substr(0, 7) == "OUTPUT:"){
+- sessionId = "O" + sessionId;
++ sessMode = sessionOutputMode;
++ sessionId = "O" + generateSession(streamName, ip, tkn, protocol, sessMode);
++ }else{
++ // If the session only contains the HTTP connector, check sessionStreamInfoMode
++ if (protocol.size() == 4 && protocol == "HTTP"){
++ if (sessionStreamInfoMode == SESS_HTTP_AS_VIEWER){
++ sessionId = generateSession(streamName, ip, tkn, protocol, sessMode);
++ }else if (sessionStreamInfoMode == SESS_HTTP_AS_OUTPUT){
++ sessMode = sessionOutputMode;
++ sessionId = "O" + generateSession(streamName, ip, tkn, protocol, sessMode);
++ }else if (sessionStreamInfoMode == SESS_HTTP_DISABLED){
++ return;
++ }else if (sessionStreamInfoMode == SESS_HTTP_AS_UNSPECIFIED){
++ // Set sessMode to include all variables when determining the session ID
++ sessMode = sessionUnspecifiedMode;
++ sessionId = "U" + generateSession(streamName, ip, tkn, protocol, sessMode);
++ }else{
++ sessionId = generateSession(streamName, ip, tkn, protocol, sessMode);
++ }
++ }else{
++ sessionId = generateSession(streamName, ip, tkn, protocol, sessMode);
++ }
+ }
+ char userPageName[NAME_BUFFER_SIZE];
+ snprintf(userPageName, NAME_BUFFER_SIZE, COMMS_SESSIONS, sessionId.c_str());
+@@ -283,36 +356,59 @@ namespace Comms{
+ if (!_master){
+ dataPage.init(userPageName, 0, false, false);
+ if (!dataPage){
++ std::string host;
++ Socket::hostBytesToStr(ip.data(), 16, host);
+ pid_t thisPid;
+ std::deque<std::string> args;
+ args.push_back(Util::getMyPath() + "MistSession");
+ args.push_back(sessionId);
+- args.push_back("--sessionmode");
+- args.push_back(JSON::Value(sessionMode).asString());
+- args.push_back("--streamname");
+- args.push_back(streamName);
+- args.push_back("--ip");
+- args.push_back(ip);
+- args.push_back("--sid");
+- args.push_back(sid);
+- args.push_back("--protocol");
+- args.push_back(protocol);
+- args.push_back("--requrl");
+- args.push_back(reqUrl);
++
++ // First bit defines whether to include stream name
++ if (sessMode & 0x08){
++ args.push_back("--streamname");
++ args.push_back(streamName);
++ }else{
++ setenv("SESSION_STREAM", streamName.c_str(), 1);
++ }
++ // Second bit defines whether to include viewer ip
++ if (sessMode & 0x04){
++ args.push_back("--ip");
++ args.push_back(host);
++ }else{
++ setenv("SESSION_IP", host.c_str(), 1);
++ }
++ // Third bit defines whether to include tkn
++ if (sessMode & 0x02){
++ args.push_back("--tkn");
++ args.push_back(tkn);
++ }else{
++ setenv("SESSION_TKN", tkn.c_str(), 1);
++ }
++ // Fourth bit defines whether to include protocol
++ if (sessMode & 0x01){
++ args.push_back("--protocol");
++ args.push_back(protocol);
++ }else{
++ setenv("SESSION_PROTOCOL", protocol.c_str(), 1);
++ }
++ setenv("SESSION_REQURL", reqUrl.c_str(), 1);
+ int err = fileno(stderr);
+ thisPid = Util::Procs::StartPiped(args, 0, 0, &err);
+ Util::Procs::forget(thisPid);
+- HIGH_MSG("Spawned new session executeable (pid %u) for sessionId '%s', corresponding to host %s and stream %s", thisPid, sessionId.c_str(), ip.c_str(), streamName.c_str());
++ unsetenv("SESSION_STREAM");
++ unsetenv("SESSION_IP");
++ unsetenv("SESSION_TKN");
++ unsetenv("SESSION_PROTOCOL");
++ unsetenv("SESSION_REQURL");
+ }
+ }
+- // Open SEM_SESSION
+- if(!sem){
+- char semName[NAME_BUFFER_SIZE];
+- snprintf(semName, NAME_BUFFER_SIZE, SEM_SESSION, sessionId.c_str());
+- sem.open(semName, O_RDWR, ACCESSPERMS, 1);
++ reload(sessionId, _master, reIssue);
++ if (index != INVALID_RECORD_INDEX){
++ setConnector(protocol);
++ setHost(ip);
++ setStream(streamName);
++ VERYHIGH_MSG("Reloading connection. Claimed record %lu", index);
+ }
+- Comms::reload(userPageName, COMMS_SESSIONS_INITSIZE, _master, reIssue);
+- VERYHIGH_MSG("Reloading connection. Claimed record %lu", index);
+ }
+
+ /// \brief Marks the data page as closed, so that we longer write any new data to is
+@@ -341,7 +437,6 @@ namespace Comms{
+ dataAccX.addField("host", RAX_RAW, 16);
+ dataAccX.addField("stream", RAX_STRING, 100);
+ dataAccX.addField("connector", RAX_STRING, 20);
+- dataAccX.addField("tags", RAX_STRING, 512);
+ dataAccX.addField("pktcount", RAX_64UINT);
+ dataAccX.addField("pktloss", RAX_64UINT);
+ dataAccX.addField("pktretrans", RAX_64UINT);
+@@ -349,7 +444,6 @@ namespace Comms{
+
+ void Connections::nullFields(){
+ Comms::nullFields();
+- setTags("");
+ setConnector("");
+ setStream("");
+ setHost("");
+@@ -373,7 +467,6 @@ namespace Comms{
+ host = dataAccX.getFieldAccX("host");
+ stream = dataAccX.getFieldAccX("stream");
+ connector = dataAccX.getFieldAccX("connector");
+- tags = dataAccX.getFieldAccX("tags");
+ pktcount = dataAccX.getFieldAccX("pktcount");
+ pktloss = dataAccX.getFieldAccX("pktloss");
+ pktretrans = dataAccX.getFieldAccX("pktretrans");
+@@ -461,14 +554,6 @@ namespace Comms{
+ return false;
+ }
+
+- std::string Connections::getTags() const{return tags.string(index);}
+- std::string Connections::getTags(size_t idx) const{return (master ? tags.string(idx) : 0);}
+- void Connections::setTags(std::string _sid){tags.set(_sid, index);}
+- void Connections::setTags(std::string _sid, size_t idx){
+- if (!master){return;}
+- tags.set(_sid, idx);
+- }
+-
+ uint64_t Connections::getPacketCount() const{return pktcount.uint(index);}
+ uint64_t Connections::getPacketCount(size_t idx) const{
+ return (master ? pktcount.uint(idx) : 0);
+@@ -501,31 +586,32 @@ namespace Comms{
+
+ /// \brief Generates a session ID which is unique per viewer
+ /// \return generated session ID as string
+- std::string Connections::generateSession(std::string streamName, std::string ip, std::string sid, std::string connector, uint64_t sessionMode){
++ std::string Connections::generateSession(const std::string & streamName, const std::string & ip, const std::string & tkn, const std::string & connector, uint64_t sessionMode){
+ std::string concat;
++ std::string debugMsg = "Generating session id based on";
+ // First bit defines whether to include stream name
+- if (sessionMode > 7){
++ if (sessionMode & 0x08){
+ concat += streamName;
+- sessionMode -= 8;
++ debugMsg += " stream name '" + streamName + "'";
+ }
+ // Second bit defines whether to include viewer ip
+- if (sessionMode > 3){
++ if (sessionMode & 0x04){
+ concat += ip;
+- sessionMode -= 4;
++ std::string ipHex;
++ Socket::hostBytesToStr(ip.c_str(), ip.size(), ipHex);
++ debugMsg += " IP '" + ipHex + "'";
+ }
+- // Third bit defines whether to include player ip
+- if (sessionMode > 1){
+- concat += sid;
+- sessionMode -= 2;
++ // Third bit defines whether to include client-side session token
++ if (sessionMode & 0x02){
++ concat += tkn;
++ debugMsg += " session token '" + tkn + "'";
+ }
+ // Fourth bit defines whether to include protocol
+- if (sessionMode == 1){
++ if (sessionMode & 0x01){
+ concat += connector;
+- sessionMode = 0;
+- }
+- if (sessionMode > 0){
+- WARN_MSG("Could not resolve session mode of value %lu", sessionMode);
++ debugMsg += " protocol '" + connector + "'";
+ }
++ VERYHIGH_MSG("%s", debugMsg.c_str());
+ return Secure::sha256(concat.c_str(), concat.length());
+ }
+ }// namespace Comms
+diff --git a/lib/comms.h b/lib/comms.h
+index 9a5c0ea9..ec36dcb0 100644
+--- a/lib/comms.h
++++ b/lib/comms.h
+@@ -9,13 +9,21 @@
+ #define COMM_STATUS_REQDISCONNECT 0x10
+ #define COMM_STATUS_ACTIVE 0x1
+ #define COMM_STATUS_INVALID 0x0
++#define SESS_BUNDLE_DEFAULT_VIEWER 14
++#define SESS_BUNDLE_DEFAULT_OTHER 15
++#define SESS_DEFAULT_STREAM_INFO_MODE 1
++#define SESS_HTTP_AS_VIEWER 1
++#define SESS_HTTP_AS_OUTPUT 2
++#define SESS_HTTP_DISABLED 3
++#define SESS_HTTP_AS_UNSPECIFIED 4
++#define SESS_TKN_DEFAULT_MODE 15
+
+
+ #define COMM_LOOP(comm, onActive, onDisconnect) \
+ {\
+ for (size_t id = 0; id < comm.recordCount(); id++){\
+ if (comm.getStatus(id) == COMM_STATUS_INVALID){continue;}\
+- if (!Util::Procs::isRunning(comm.getPid(id))){\
++ if (!(comm.getStatus(id) & COMM_STATUS_DISCONNECT) && comm.getPid(id) && !Util::Procs::isRunning(comm.getPid(id))){\
+ comm.setStatus(COMM_STATUS_DISCONNECT | comm.getStatus(id), id);\
+ }\
+ onActive;\
+@@ -27,6 +35,14 @@
+ }
+
+ namespace Comms{
++ extern uint8_t sessionViewerMode;
++ extern uint8_t sessionInputMode;
++ extern uint8_t sessionOutputMode;
++ extern uint8_t sessionUnspecifiedMode;
++ extern uint8_t sessionStreamInfoMode;
++ extern uint8_t tknMode;
++ void sessionConfigCache();
++
+ class Comms{
+ public:
+ Comms();
+@@ -66,11 +82,13 @@ namespace Comms{
+
+ class Connections : public Comms{
+ public:
+- void reload(std::string streamName, std::string ip, std::string sid, std::string protocol, std::string reqUrl, uint64_t sessionMode, bool _master = false, bool reIssue = false);
++ void reload(const std::string & streamName, const std::string & ip, const std::string & tkn, const std::string & protocol, const std::string & reqUrl, bool _master = false, bool reIssue = false);
++ void reload(const std::string & sessId, bool _master = false, bool reIssue = false);
+ void unload();
+ operator bool() const{return dataPage.mapped && (master || index != INVALID_RECORD_INDEX);}
+- std::string generateSession(std::string streamName, std::string ip, std::string sid, std::string connector, uint64_t sessionMode);
++ std::string generateSession(const std::string & streamName, const std::string & ip, const std::string & tkn, const std::string & connector, uint64_t sessionMode);
+ std::string sessionId;
++ std::string initialTkn;
+
+ void setExit();
+ bool getExit();
+@@ -79,6 +97,8 @@ namespace Comms{
+ virtual void nullFields();
+ virtual void fieldAccess();
+
++ const std::string & getTkn() const{return initialTkn;}
++
+ uint64_t getNow() const;
+ uint64_t getNow(size_t idx) const;
+ void setNow(uint64_t _now);
+@@ -120,11 +140,6 @@ namespace Comms{
+ void setConnector(std::string _connector, size_t idx);
+ bool hasConnector(size_t idx, std::string protocol);
+
+- std::string getTags() const;
+- std::string getTags(size_t idx) const;
+- void setTags(std::string _sid);
+- void setTags(std::string _sid, size_t idx);
+-
+ uint64_t getPacketCount() const;
+ uint64_t getPacketCount(size_t idx) const;
+ void setPacketCount(uint64_t _count);
+@@ -197,5 +212,10 @@ namespace Comms{
+ virtual void addFields();
+ virtual void nullFields();
+ virtual void fieldAccess();
++
++ std::string getTags() const;
++ std::string getTags(size_t idx) const;
++ void setTags(std::string _sid);
++ void setTags(std::string _sid, size_t idx);
+ };
+ }// namespace Comms
+diff --git a/lib/defines.h b/lib/defines.h
+index 203b75db..d08d1d04 100644
+--- a/lib/defines.h
++++ b/lib/defines.h
+@@ -231,7 +231,7 @@ static inline void show_stackframe(){}
+ #define SEM_TRACKLIST "/MstTRKS%s" //%s stream name
+ #define SEM_SESSION "MstSess%s"
+ #define SEM_SESSCACHE "/MstSessCacheLock"
+-#define SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID 14
++#define SESS_TIMEOUT 600 // Session timeout in seconds
+ #define SHM_CAPA "MstCapa"
+ #define SHM_PROTO "MstProt"
+ #define SHM_PROXY "MstProx"
+diff --git a/lib/hls_support.cpp b/lib/hls_support.cpp
+index c311a7ac..130e691d 100644
+--- a/lib/hls_support.cpp
++++ b/lib/hls_support.cpp
+@@ -274,7 +274,7 @@ namespace HLS{
+ if (trackData.mediaFormat == ".ts"){return;}
+
+ result << "#EXT-X-MAP:URI=\"" << trackData.urlPrefix << "init" << trackData.mediaFormat;
+- if (trackData.sessionId.size()){result << "?sessId=" << trackData.sessionId;}
++ if (trackData.sessionId.size()){result << "?tkn=" << trackData.sessionId;}
+ result << "\"\r\n";
+ }
+
+@@ -327,7 +327,7 @@ namespace HLS{
+ result << "?msn=" << fragData.currentFrag;
+ result << "&mTrack=" << trackData.timingTrackId;
+ result << "&dur=" << fragData.duration;
+- if (trackData.sessionId.size()){result << "&sessId=" << trackData.sessionId;}
++ if (trackData.sessionId.size()){result << "&tkn=" << trackData.sessionId;}
+ result << "\r\n";
+ }
+
+@@ -341,7 +341,7 @@ namespace HLS{
+ result << "?msn=" << fragData.currentFrag;
+ result << "&mTrack=" << trackData.timingTrackId;
+ result << "&dur=" << duration;
+- if (trackData.sessionId.size()){result << "&sessId=" << trackData.sessionId;}
++ if (trackData.sessionId.size()){result << "&tkn=" << trackData.sessionId;}
+ result << "\"";
+
+ // NOTE: INDEPENDENT tags, specified ONLY for VIDEO tracks, indicate the first partial fragment
+@@ -448,7 +448,7 @@ namespace HLS{
+ result << "?msn=" << fragData.currentFrag - 1;
+ result << "&mTrack=" << trackData.timingTrackId;
+ result << "&dur=" << partDurationMaxMs;
+- if (trackData.sessionId.size()){result << "&sessId=" << trackData.sessionId;}
++ if (trackData.sessionId.size()){result << "&tkn=" << trackData.sessionId;}
+ result << "\"\r\n";
+ }
+
+@@ -509,7 +509,7 @@ namespace HLS{
+ result << ",NAME=\"" << name << "\",URI=\"" << trackId << "/index.m3u8";
+ result << "?mTrack=" << masterData.mainTrack;
+ result << "&iMsn=" << iFrag;
+- if (masterData.hasSessId){result << "&sessId=" << masterData.sessId;}
++ if (masterData.sessId.size()){result << "&tkn=" << masterData.sessId;}
+ if (masterData.noLLHLS){result << "&llhls=0";}
+ result << "\"\r\n";
+ }
+@@ -529,7 +529,7 @@ namespace HLS{
+ result << "/index.m3u8";
+ result << "?mTrack=" << masterData.mainTrack;
+ result << "&iMsn=" << iFrag;
+- if (masterData.hasSessId){result << "&sessId=" << masterData.sessId;}
++ if (masterData.sessId.size()){result << "&tkn=" << masterData.sessId;}
+ if (masterData.noLLHLS){result << "&llhls=0";}
+ result << "\r\n";
+ }
+diff --git a/lib/http_parser.cpp b/lib/http_parser.cpp
+index c08e3d56..113fdb51 100644
+--- a/lib/http_parser.cpp
++++ b/lib/http_parser.cpp
+@@ -742,13 +742,13 @@ bool HTTP::Parser::parse(std::string &HTTPbuffer, Util::DataCallback &cb){
+
+ /// HTTP variable parser to std::map<std::string, std::string> structure.
+ /// Reads variables from data, decodes and stores them to storage.
+-void HTTP::parseVars(const std::string &data, std::map<std::string, std::string> &storage){
++void HTTP::parseVars(const std::string &data, std::map<std::string, std::string> &storage, const std::string & separator){
+ std::string varname;
+ std::string varval;
+ // position where a part starts (e.g. after &)
+ size_t pos = 0;
+ while (pos < data.length()){
+- size_t nextpos = data.find('&', pos);
++ size_t nextpos = data.find(separator, pos);
+ if (nextpos == std::string::npos){nextpos = data.length();}
+ size_t eq_pos = data.find('=', pos);
+ if (eq_pos < nextpos){
+@@ -769,7 +769,7 @@ void HTTP::parseVars(const std::string &data, std::map<std::string, std::string>
+ break;
+ }
+ // erase &
+- pos = nextpos + 1;
++ pos = nextpos + separator.size();
+ }
+ }
+
+diff --git a/lib/http_parser.h b/lib/http_parser.h
+index 9c843f20..a5528df3 100644
+--- a/lib/http_parser.h
++++ b/lib/http_parser.h
+@@ -14,7 +14,7 @@ namespace HTTP{
+
+ /// HTTP variable parser to std::map<std::string, std::string> structure.
+ /// Reads variables from data, decodes and stores them to storage.
+- void parseVars(const std::string &data, std::map<std::string, std::string> &storage);
++ void parseVars(const std::string &data, std::map<std::string, std::string> &storage, const std::string & separator = "&");
+
+ /// Simple class for reading and writing HTTP 1.0 and 1.1.
+ class Parser : public Util::DataCallback{
+diff --git a/lib/socket.cpp b/lib/socket.cpp
+index 6d63af00..47b2b6bd 100644
+--- a/lib/socket.cpp
++++ b/lib/socket.cpp
+@@ -166,6 +166,8 @@ bool Socket::isBinAddress(const std::string &binAddr, std::string addr){
+ /// Converts the given address with optional subnet to binary IPv6 form.
+ /// Returns 16 bytes of address, followed by 1 byte of subnet bits, zero or more times.
+ std::string Socket::getBinForms(std::string addr){
++ // Check for empty address
++ if (!addr.size()){return std::string(17, (char)0);}
+ // Check if we need to do prefix matching
+ uint8_t prefixLen = 128;
+ if (addr.find('/') != std::string::npos){
+@@ -1796,6 +1798,14 @@ void Socket::UDPConnection::GetDestination(std::string &destIp, uint32_t &port){
+ FAIL_MSG("Could not get destination for UDP socket");
+ }// Socket::UDPConnection GetDestination
+
++/// Gets the properties of the receiving end of this UDP socket.
++/// This will be the receiving end for all SendNow calls.
++std::string Socket::UDPConnection::getBinDestination(){
++ std::string binList = getIPv6BinAddr(*(sockaddr_in6*)destAddr);
++ if (binList.size() < 16){ return std::string("\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", 16); }
++ return binList.substr(0, 16);
++}// Socket::UDPConnection GetDestination
++
+ /// Returns the port number of the receiving end of this socket.
+ /// Returns 0 on error.
+ uint32_t Socket::UDPConnection::getDestPort() const{
+diff --git a/lib/socket.h b/lib/socket.h
+index a85fbf39..b369ac84 100644
+--- a/lib/socket.h
++++ b/lib/socket.h
+@@ -215,6 +215,7 @@ namespace Socket{
+ void setBlocking(bool blocking);
+ void SetDestination(std::string hostname, uint32_t port);
+ void GetDestination(std::string &hostname, uint32_t &port);
++ std::string getBinDestination();
+ const void * getDestAddr(){return destAddr;}
+ size_t getDestAddrLen(){return destAddr_size;}
+ std::string getBoundAddress();
+diff --git a/lib/websocket.cpp b/lib/websocket.cpp
+index 05703b55..c82b4cba 100644
+--- a/lib/websocket.cpp
++++ b/lib/websocket.cpp
+@@ -73,31 +73,31 @@ namespace HTTP{
+ }
+
+ /// Takes an incoming HTTP::Parser request for a Websocket, and turns it into one.
+- Websocket::Websocket(Socket::Connection &c, HTTP::Parser &h) : C(c){
++ Websocket::Websocket(Socket::Connection &c, const HTTP::Parser &req, HTTP::Parser &resp) : C(c){
+ frameType = 0;
+ maskOut = false;
+- std::string connHeader = h.GetHeader("Connection");
++ std::string connHeader = req.GetHeader("Connection");
+ Util::stringToLower(connHeader);
+ if (connHeader.find("upgrade") == std::string::npos){
+ FAIL_MSG("Could not negotiate websocket, connection header incorrect (%s).", connHeader.c_str());
+ C.close();
+ return;
+ }
+- std::string upgradeHeader = h.GetHeader("Upgrade");
++ std::string upgradeHeader = req.GetHeader("Upgrade");
+ Util::stringToLower(upgradeHeader);
+ if (upgradeHeader != "websocket"){
+ FAIL_MSG("Could not negotiate websocket, upgrade header incorrect (%s).", upgradeHeader.c_str());
+ C.close();
+ return;
+ }
+- if (h.GetHeader("Sec-WebSocket-Version") != "13"){
++ if (req.GetHeader("Sec-WebSocket-Version") != "13"){
+ FAIL_MSG("Could not negotiate websocket, version incorrect (%s).",
+- h.GetHeader("Sec-WebSocket-Version").c_str());
++ req.GetHeader("Sec-WebSocket-Version").c_str());
+ C.close();
+ return;
+ }
+ #ifdef SSL
+- std::string client_key = h.GetHeader("Sec-WebSocket-Key");
++ std::string client_key = req.GetHeader("Sec-WebSocket-Key");
+ if (!client_key.size()){
+ FAIL_MSG("Could not negotiate websocket, missing key!");
+ C.close();
+@@ -105,15 +105,13 @@ namespace HTTP{
+ }
+ #endif
+
+- h.Clean();
+- h.setCORSHeaders();
+- h.SetHeader("Upgrade", "websocket");
+- h.SetHeader("Connection", "Upgrade");
++ resp.SetHeader("Upgrade", "websocket");
++ resp.SetHeader("Connection", "Upgrade");
+ #ifdef SSL
+- h.SetHeader("Sec-WebSocket-Accept", calculateKeyAccept(client_key));
++ resp.SetHeader("Sec-WebSocket-Accept", calculateKeyAccept(client_key));
+ #endif
+ // H.SetHeader("Sec-WebSocket-Protocol", "json");
+- h.SendResponse("101", "Websocket away!", C);
++ resp.SendResponse("101", "Websocket away!", C);
+ }
+
+ /// Loops calling readFrame until the connection is closed, sleeping in between reads if needed.
+diff --git a/lib/websocket.h b/lib/websocket.h
+index 819f18c9..07861a19 100644
+--- a/lib/websocket.h
++++ b/lib/websocket.h
+@@ -7,7 +7,7 @@
+ namespace HTTP{
+ class Websocket{
+ public:
+- Websocket(Socket::Connection &c, HTTP::Parser &h);
++ Websocket(Socket::Connection &c, const HTTP::Parser &req, HTTP::Parser &resp);
+ Websocket(Socket::Connection &c, const HTTP::URL & url, std::map<std::string, std::string> * headers = 0);
+ Websocket(Socket::Connection &c, bool client);
+ operator bool() const;
+diff --git a/src/controller/controller.cpp b/src/controller/controller.cpp
+index 9e17ca6b..972c7ed7 100644
+--- a/src/controller/controller.cpp
++++ b/src/controller/controller.cpp
+@@ -309,6 +309,24 @@ int main_loop(int argc, char **argv){
+ Controller::Storage["config"]["prometheus"] = Controller::conf.getString("prometheus");
+ Controller::Storage["config"]["accesslog"] = Controller::conf.getString("accesslog");
+ Controller::normalizeTrustedProxies(Controller::Storage["config"]["trustedproxy"]);
++ if (!Controller::Storage["config"]["sessionViewerMode"]){
++ Controller::Storage["config"]["sessionViewerMode"] = SESS_BUNDLE_DEFAULT_VIEWER;
++ }
++ if (!Controller::Storage["config"]["sessionInputMode"]){
++ Controller::Storage["config"]["sessionInputMode"] = SESS_BUNDLE_DEFAULT_OTHER;
++ }
++ if (!Controller::Storage["config"]["sessionOutputMode"]){
++ Controller::Storage["config"]["sessionOutputMode"] = SESS_BUNDLE_DEFAULT_OTHER;
++ }
++ if (!Controller::Storage["config"]["sessionUnspecifiedMode"]){
++ Controller::Storage["config"]["sessionUnspecifiedMode"] = 0;
++ }
++ if (!Controller::Storage["config"]["sessionStreamInfoMode"]){
++ Controller::Storage["config"]["sessionStreamInfoMode"] = SESS_DEFAULT_STREAM_INFO_MODE;
++ }
++ if (!Controller::Storage["config"].isMember("tknMode")){
++ Controller::Storage["config"]["tknMode"] = SESS_TKN_DEFAULT_MODE;
++ }
+ Controller::prometheus = Controller::Storage["config"]["prometheus"].asStringRef();
+ Controller::accesslog = Controller::Storage["config"]["accesslog"].asStringRef();
+ Controller::writeConfig();
+diff --git a/src/controller/controller_api.cpp b/src/controller/controller_api.cpp
+index 02491065..4e935aba 100644
+--- a/src/controller/controller_api.cpp
++++ b/src/controller/controller_api.cpp
+@@ -188,7 +188,9 @@ void Controller::handleWebSocket(HTTP::Parser &H, Socket::Connection &C){
+ std::string logs = H.GetVar("logs");
+ std::string accs = H.GetVar("accs");
+ bool doStreams = H.GetVar("streams").size();
+- HTTP::Websocket W(C, H);
++ HTTP::Parser req = H;
++ H.Clean();
++ HTTP::Websocket W(C, req, H);
+ if (!W){return;}
+
+ IPC::sharedPage shmLogs(SHM_STATE_LOGS, 1024 * 1024);
+@@ -594,7 +596,12 @@ void Controller::handleAPICommands(JSON::Value &Request, JSON::Value &Response){
+ out["prometheus"] = in["prometheus"];
+ Controller::prometheus = out["prometheus"].asStringRef();
+ }
+- if (in.isMember("sessionMode")){out["sessionMode"] = in["sessionMode"];}
++ if (in.isMember("sessionViewerMode")){out["sessionViewerMode"] = in["sessionViewerMode"];}
++ if (in.isMember("sessionInputMode")){out["sessionInputMode"] = in["sessionInputMode"];}
++ if (in.isMember("sessionOutputMode")){out["sessionOutputMode"] = in["sessionOutputMode"];}
++ if (in.isMember("sessionUnspecifiedMode")){out["sessionUnspecifiedMode"] = in["sessionUnspecifiedMode"];}
++ if (in.isMember("sessionStreamInfoMode")){out["sessionStreamInfoMode"] = in["sessionStreamInfoMode"];}
++ if (in.isMember("tknMode")){out["tknMode"] = in["tknMode"];}
+ if (in.isMember("defaultStream")){out["defaultStream"] = in["defaultStream"];}
+ if (in.isMember("location") && in["location"].isObject()){
+ out["location"]["lat"] = in["location"]["lat"].asDouble();
+diff --git a/src/controller/controller_statistics.cpp b/src/controller/controller_statistics.cpp
+index 6e51f4ee..ffb89897 100644
+--- a/src/controller/controller_statistics.cpp
++++ b/src/controller/controller_statistics.cpp
+@@ -58,6 +58,11 @@ static uint64_t cpu_use = 0;
+ char noBWCountMatches[1717];
+ uint64_t bwLimit = 128 * 1024 * 1024; // gigabit default limit
+
++const char nullAddress[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
++static Controller::statLog emptyLogEntry = {0, 0, 0, 0, 0, 0 ,0 ,0, "", nullAddress, ""};
++bool notEmpty(const Controller::statLog & dta){
++ return dta.time || dta.firstActive || dta.lastSecond || dta.down || dta.up || dta.streamName.size() || dta.connectors.size();
++}
+
+ // For server-wide totals. Local to this file only.
+ struct streamTotals{
+@@ -66,9 +71,11 @@ struct streamTotals{
+ uint64_t inputs;
+ uint64_t outputs;
+ uint64_t viewers;
++ uint64_t unspecified;
+ uint64_t currIns;
+ uint64_t currOuts;
+ uint64_t currViews;
++ uint64_t currUnspecified;
+ uint8_t status;
+ uint64_t viewSeconds;
+ uint64_t packSent;
+@@ -84,6 +91,7 @@ static uint64_t servDownBytes = 0;
+ static uint64_t servUpOtherBytes = 0;
+ static uint64_t servDownOtherBytes = 0;
+ static uint64_t servInputs = 0;
++static uint64_t servUnspecified = 0;
+ static uint64_t servOutputs = 0;
+ static uint64_t servViewers = 0;
+ static uint64_t servSeconds = 0;
+@@ -95,17 +103,19 @@ static uint64_t viewSecondsTotal = 0;
+ // Mapping of streamName -> summary of stream-wide statistics
+ static std::map<std::string, struct streamTotals> streamStats;
+
+-// If sessId does not exist yet in streamStats, create and init an entry for it
+-static void createEmptyStatsIfNeeded(const std::string & sessId){
+- if (streamStats.count(sessId)){return;}
+- streamTotals & sT = streamStats[sessId];
++// If streamName does not exist yet in streamStats, create and init an entry for it
++static void createEmptyStatsIfNeeded(const std::string & streamName){
++ if (streamStats.count(streamName)){return;}
++ streamTotals & sT = streamStats[streamName];
+ sT.upBytes = 0;
+ sT.downBytes = 0;
+ sT.inputs = 0;
+ sT.outputs = 0;
+ sT.viewers = 0;
++ sT.unspecified = 0;
+ sT.currIns = 0;
+ sT.currOuts = 0;
++ sT.currUnspecified = 0;
+ sT.currViews = 0;
+ sT.status = 0;
+ sT.viewSeconds = 0;
+@@ -335,15 +345,23 @@ void Controller::SharedMemStats(void *config){
+ it->second.currViews = 0;
+ it->second.currIns = 0;
+ it->second.currOuts = 0;
++ it->second.currUnspecified = 0;
+ }
+ }
+ // wipe old statistics and set session type counters
+ if (sessions.size()){
+ std::list<std::string> mustWipe;
+- uint64_t cutOffPoint = Util::bootSecs() - STAT_CUTOFF;
++ // Ensure cutOffPoint is either time of boot or 10 minutes ago, whichever is closer.
++ // Prevents wrapping around to high values close to system boot time.
++ uint64_t cutOffPoint = Util::bootSecs();
++ if (cutOffPoint > STAT_CUTOFF){
++ cutOffPoint -= STAT_CUTOFF;
++ }else{
++ cutOffPoint = 0;
++ }
+ for (std::map<std::string, statSession>::iterator it = sessions.begin(); it != sessions.end(); it++){
+ // This part handles ending sessions, keeping them in cache for now
+- if (it->second.getEnd() < cutOffPoint && it->second.newestDataPoint() < cutOffPoint){
++ if (it->second.getEnd() < cutOffPoint){
+ viewSecondsTotal += it->second.getConnTime();
+ mustWipe.push_back(it->first);
+ // Don't count this session as a viewer
+@@ -353,19 +371,24 @@ void Controller::SharedMemStats(void *config){
+ switch (it->second.getSessType()){
+ case SESS_UNSET: break;
+ case SESS_VIEWER:
+- if (it->second.hasDataFor(tOut) && it->second.isViewerOn(tOut)){
+- streamStats[it->first].currViews++;
++ if (it->second.hasDataFor(tOut)){
++ streamStats[it->second.getStreamName()].currViews++;
+ }
+ servSeconds += it->second.getConnTime();
+ break;
+ case SESS_INPUT:
+- if (it->second.hasDataFor(tIn) && it->second.isViewerOn(tIn)){
+- streamStats[it->first].currIns++;
++ if (it->second.hasDataFor(tIn)){
++ streamStats[it->second.getStreamName()].currIns++;
+ }
+ break;
+ case SESS_OUTPUT:
+- if (it->second.hasDataFor(tOut) && it->second.isViewerOn(tOut)){
+- streamStats[it->first].currOuts++;
++ if (it->second.hasDataFor(tOut)){
++ streamStats[it->second.getStreamName()].currOuts++;
++ }
++ break;
++ case SESS_UNSPECIFIED:
++ if (it->second.hasDataFor(tOut)){
++ streamStats[it->second.getStreamName()].currUnspecified++;
+ }
+ break;
+ }
+@@ -406,6 +429,7 @@ void Controller::SharedMemStats(void *config){
+ strmStats->setInt("viewers", it->second.currViews, strmPos);
+ strmStats->setInt("inputs", it->second.currIns, strmPos);
+ strmStats->setInt("outputs", it->second.currOuts, strmPos);
++ strmStats->setInt("unspecified", it->second.currUnspecified, strmPos);
+ ++strmPos;
+ }
+ }
+@@ -489,47 +513,46 @@ void Controller::killConnections(std::string sessId){
+
+ /// Updates the given active connection with new stats data.
+ void Controller::statSession::update(uint64_t index, Comms::Sessions &statComm){
+- if (host == ""){
+- Socket::hostBytesToStr(statComm.getHost(index).data(), 16, host);
+- }
+- if (streamName == ""){
+- streamName = statComm.getStream(index);
+- }
+- if (curConnector == ""){
+- curConnector = statComm.getConnector(index);
+- }
+ if (sessId == ""){
+ sessId = statComm.getSessId(index);
+ }
+- // Export tags to session
+- if (tags.size()){
+- std::stringstream tagStream;
+- for (std::set<std::string>::iterator it = tags.begin(); it != tags.end(); ++it){
+- tagStream << "[" << *it << "]";
++
++ if (sessionType == SESS_UNSET){
++ if (sessId[0] == 'I'){
++ sessionType = SESS_INPUT;
++ }else if (sessId[0] == 'O'){
++ sessionType = SESS_OUTPUT;
++ }else if (sessId[0] == 'U'){
++ sessionType = SESS_UNSPECIFIED;
++ }else{
++ sessionType = SESS_VIEWER;
+ }
+- statComm.setTags(tagStream.str(), index);
+ }
+
++ uint64_t prevNow = curData.log.size() ? curData.log.rbegin()->first : 0;
++ // only parse last received data, if newer
++ if (prevNow > statComm.getNow(index)){return;};
+ long long prevDown = getDown();
+ long long prevUp = getUp();
+ uint64_t prevPktSent = getPktCount();
+ uint64_t prevPktLost = getPktLost();
+ uint64_t prevPktRetrans = getPktRetransmit();
++ uint64_t prevFirstActive = getFirstActive();
++
+ curData.update(statComm, index);
+- // store timestamp of first received data, if older
+- if (firstSec > statComm.getNow(index)){firstSec = statComm.getNow(index);}
+- uint64_t secIncr = 0;
+- // store timestamp of last received data, if newer
+- if (statComm.getNow(index) > lastSec){
+- lastSec = statComm.getNow(index);
+- if (!tracked){
+- tracked = true;
+- firstActive = firstSec;
+- }else{
+- secIncr = (statComm.getNow(index) - lastSec);
++ const std::string& streamName = getStreamName();
++ // Export tags to session
++ if (tags.size()){
++ std::stringstream tagStream;
++ for (std::set<std::string>::iterator it = tags.begin(); it != tags.end(); ++it){
++ tagStream << "[" << *it << "]";
+ }
+- lastSec = statComm.getNow(index);
++ statComm.setTags(tagStream.str(), index);
++ } else {
++ statComm.setTags("", index);
+ }
++
++ uint64_t secIncr = prevFirstActive ? (statComm.getNow(index) - prevNow) : 0;
+ long long currDown = getDown();
+ long long currUp = getUp();
+ uint64_t currPktSent = getPktCount();
+@@ -537,7 +560,7 @@ void Controller::statSession::update(uint64_t index, Comms::Sessions &statComm){
+ uint64_t currPktRetrans = getPktRetransmit();
+ if (currUp - prevUp < 0 || currDown - prevDown < 0){
+ INFO_MSG("Negative data usage! %lldu/%lldd (u%lld->%lld) in %s over %s, #%" PRIu64, currUp - prevUp,
+- currDown - prevDown, prevUp, currUp, streamName.c_str(), curConnector.c_str(), index);
++ currDown - prevDown, prevUp, currUp, streamName.c_str(), curData.log.rbegin()->second.connectors.c_str(), index);
+ }else{
+ if (!noBWCount){
+ size_t bwMatchOffset = 0;
+@@ -567,40 +590,38 @@ void Controller::statSession::update(uint64_t index, Comms::Sessions &statComm){
+ servPackRetrans += currPktRetrans - prevPktRetrans;
+ }
+ }
+- if (sessionType == SESS_UNSET){
+- if (curConnector.size() >= 5 && curConnector.substr(0, 5) == "INPUT"){
+- ++servInputs;
+- createEmptyStatsIfNeeded(streamName);
+- streamStats[streamName].inputs++;
+- streamStats[streamName].currIns++;
+- sessionType = SESS_INPUT;
+- }else if (curConnector.size() >= 6 && curConnector.substr(0, 6) == "OUTPUT"){
+- ++servOutputs;
+- createEmptyStatsIfNeeded(streamName);
+- streamStats[streamName].outputs++;
+- streamStats[streamName].currOuts++;
+- sessionType = SESS_OUTPUT;
+- }else{
+- ++servViewers;
+- createEmptyStatsIfNeeded(streamName);
+- streamStats[streamName].viewers++;
+- streamStats[streamName].currViews++;
+- sessionType = SESS_VIEWER;
++ if (!prevFirstActive && streamName.size()){
++ createEmptyStatsIfNeeded(streamName);
++ switch(sessionType){
++ case SESS_INPUT:
++ ++servInputs;
++ streamStats[streamName].inputs++;
++ break;
++ case SESS_OUTPUT:
++ ++servOutputs;
++ streamStats[streamName].outputs++;
++ break;
++ case SESS_VIEWER:
++ ++servViewers;
++ streamStats[streamName].viewers++;
++ break;
++ case SESS_UNSPECIFIED:
++ ++servUnspecified;
++ streamStats[streamName].unspecified++;
++ break;
++ case SESS_UNSET:
++ break;
+ }
+ }
+ // Only count connections that are countable
+ if (noBWCount != 2){
+- if (!streamName.size() || streamName[0] == 0){
+- if (streamStats.count(streamName)){streamStats.erase(streamName);}
+- }else{
+- createEmptyStatsIfNeeded(streamName);
+- streamStats[streamName].upBytes += currUp - prevUp;
+- streamStats[streamName].downBytes += currDown - prevDown;
+- streamStats[streamName].packSent += currPktSent - prevPktSent;
+- streamStats[streamName].packLoss += currPktLost - prevPktLost;
+- streamStats[streamName].packRetrans += currPktRetrans - prevPktRetrans;
+- if (sessionType == SESS_VIEWER){streamStats[streamName].viewSeconds += secIncr;}
+- }
++ createEmptyStatsIfNeeded(streamName);
++ streamStats[streamName].upBytes += currUp - prevUp;
++ streamStats[streamName].downBytes += currDown - prevDown;
++ streamStats[streamName].packSent += currPktSent - prevPktSent;
++ streamStats[streamName].packLoss += currPktLost - prevPktLost;
++ streamStats[streamName].packRetrans += currPktRetrans - prevPktRetrans;
++ if (sessionType == SESS_VIEWER){streamStats[streamName].viewSeconds += secIncr;}
+ }
+ }
+
+@@ -608,21 +629,10 @@ Controller::sessType Controller::statSession::getSessType(){
+ return sessionType;
+ }
+
+-Controller::statSession::~statSession(){
+- if (!tracked){return;}
+- switch (sessionType){
+- case SESS_INPUT:
+- if (streamStats.count(streamName) && streamStats[streamName].currIns){streamStats[streamName].currIns--;}
+- break;
+- case SESS_OUTPUT:
+- if (streamStats.count(streamName) && streamStats[streamName].currOuts){streamStats[streamName].currOuts--;}
+- break;
+- case SESS_VIEWER:
+- if (streamStats.count(streamName) && streamStats[streamName].currViews){streamStats[streamName].currViews--;}
+- break;
+- default: break;
+- }
+- uint64_t duration = lastSec - firstActive;
++/// Ends the currently active session by inserting a null datapoint one second after the last datapoint
++void Controller::statSession::finish(){
++ if (!getFirstActive()){return;}
++ uint64_t duration = getEnd() - getFirstActive();
+ if (duration < 1){duration = 1;}
+ std::stringstream tagStream;
+ if (tags.size()){
+@@ -630,6 +640,9 @@ Controller::statSession::~statSession(){
+ tagStream << "[" << *it << "]";
+ }
+ }
++ const std::string& streamName = getStreamName();
++ const std::string& curConnector = getConnectors();
++ const std::string& host = getStrHost();
+ Controller::logAccess(sessId, streamName, curConnector, host, duration, getUp(),
+ getDown(), tagStream.str());
+ if (Controller::accesslog.size()){
+@@ -668,74 +681,99 @@ Controller::statSession::~statSession(){
+ }
+ }
+ }
+- tracked = false;
+- firstActive = 0;
+- firstSec = 0xFFFFFFFFFFFFFFFFull;
+- lastSec = 0;
+- sessionType = SESS_UNSET;
++ tags.clear();
++ // Insert null datapoint
++ curData.log[curData.log.rbegin()->first + 1] = emptyLogEntry;
+ }
+
+ /// Constructs an empty session
+ Controller::statSession::statSession(){
+- firstActive = 0;
+- tracked = false;
+- firstSec = 0xFFFFFFFFFFFFFFFFull;
+- lastSec = 0;
+ sessionType = SESS_UNSET;
+ noBWCount = 0;
+- streamName = "";
+- host = "";
+- curConnector = "";
+ sessId = "";
+ }
+
+ /// Returns the first measured timestamp in this session.
+ uint64_t Controller::statSession::getStart(){
+- return firstSec;
++ if (!curData.log.size()){return 0;}
++ return curData.log.begin()->first;
+ }
+
+ /// Returns the last measured timestamp in this session.
+ uint64_t Controller::statSession::getEnd(){
+- return lastSec;
++ if (!curData.log.size()){return 0;}
++ return curData.log.rbegin()->first;
+ }
+
+ /// Returns true if there is data for this session at timestamp t.
+ bool Controller::statSession::hasDataFor(uint64_t t){
+- if (lastSec < t){return false;}
+- if (firstSec > t){return false;}
+ if (curData.hasDataFor(t)){return true;}
+ return false;
+ }
+
+-/// Returns true if this session should count as a viewer on the given timestamp.
+-bool Controller::statSession::isViewerOn(uint64_t t){
+- return getUp(t) + getDown(t);
++const std::string& Controller::statSession::getSessId(){
++ return sessId;
++}
++
++uint64_t Controller::statSession::getFirstActive(){
++ if (curData.log.size()){
++ return curData.log.rbegin()->second.firstActive;
++ }
++ return 0;
++}
++
++const std::string& Controller::statSession::getStreamName(uint64_t t){
++ if (curData.hasDataFor(t)){
++ return curData.getDataFor(t).streamName;
++ }
++ return emptyLogEntry.streamName;
+ }
+
+-std::string Controller::statSession::getStreamName(){
+- return streamName;
++const std::string& Controller::statSession::getStreamName(){
++ if (curData.log.size()){
++ return curData.log.rbegin()->second.streamName;
++ }
++ return emptyLogEntry.streamName;
+ }
+
+-std::string Controller::statSession::getHost(){
++std::string Controller::statSession::getStrHost(uint64_t t){
++ std::string host;
++ Socket::hostBytesToStr(getHost(t).data(), 16, host);
+ return host;
+ }
+
+-std::string Controller::statSession::getSessId(){
+- return sessId;
++std::string Controller::statSession::getStrHost(){
++ std::string host;
++ Socket::hostBytesToStr(getHost().data(), 16, host);
++ return host;
++}
++
++const std::string& Controller::statSession::getHost(uint64_t t){
++ if (curData.hasDataFor(t)){
++ return curData.getDataFor(t).host;
++ }
++ return emptyLogEntry.host;
+ }
+
+-std::string Controller::statSession::getCurrentProtocols(){
+- return curConnector;
++const std::string& Controller::statSession::getHost(){
++ if (curData.log.size()){
++ return curData.log.rbegin()->second.host;
++ }
++ return emptyLogEntry.host;
+ }
+
+-/// Returns true if this session should be considered connected
+-uint64_t Controller::statSession::newestDataPoint(){
+- return lastSec;
++const std::string& Controller::statSession::getConnectors(uint64_t t){
++ if (curData.hasDataFor(t)){
++ return curData.getDataFor(t).connectors;
++ }
++ return emptyLogEntry.connectors;
+ }
+
+-/// Returns true if this session has started (tracked == true) but not yet ended (log entry written)
+-bool Controller::statSession::isTracked(){
+- return tracked;
++const std::string& Controller::statSession::getConnectors(){
++ if (curData.log.size()){
++ return curData.log.rbegin()->second.connectors;
++ }
++ return emptyLogEntry.connectors;
+ }
+
+ /// Returns the cumulative connected time for this session at timestamp t.
+@@ -842,7 +880,7 @@ uint64_t Controller::statSession::getPktRetransmit(){
+ /// Returns the cumulative downloaded bytes per second for this session at timestamp t.
+ uint64_t Controller::statSession::getBpsDown(uint64_t t){
+ uint64_t aTime = t - 5;
+- if (aTime < firstSec){aTime = firstSec;}
++ if (aTime < curData.log.begin()->first){aTime = curData.log.begin()->first;}
+ if (t <= aTime){return 0;}
+ uint64_t valA = getDown(aTime);
+ uint64_t valB = getDown(t);
+@@ -852,7 +890,7 @@ uint64_t Controller::statSession::getBpsDown(uint64_t t){
+ /// Returns the cumulative uploaded bytes per second for this session at timestamp t.
+ uint64_t Controller::statSession::getBpsUp(uint64_t t){
+ uint64_t aTime = t - 5;
+- if (aTime < firstSec){aTime = firstSec;}
++ if (aTime < curData.log.begin()->first){aTime = curData.log.begin()->first;}
+ if (t <= aTime){return 0;}
+ uint64_t valA = getUp(aTime);
+ uint64_t valB = getUp(t);
+@@ -867,17 +905,8 @@ bool Controller::statStorage::hasDataFor(unsigned long long t){
+
+ /// Returns a reference to the most current data available at timestamp t.
+ Controller::statLog &Controller::statStorage::getDataFor(unsigned long long t){
+- static statLog empty;
+ if (!log.size()){
+- empty.time = 0;
+- empty.lastSecond = 0;
+- empty.down = 0;
+- empty.up = 0;
+- empty.pktCount = 0;
+- empty.pktLost = 0;
+- empty.pktRetransmit = 0;
+- empty.connectors = "";
+- return empty;
++ return emptyLogEntry;
+ }
+ std::map<unsigned long long, statLog>::iterator it = log.upper_bound(t);
+ if (it != log.begin()){it--;}
+@@ -889,6 +918,11 @@ Controller::statLog &Controller::statStorage::getDataFor(unsigned long long t){
+ void Controller::statStorage::update(Comms::Sessions &statComm, size_t index){
+ statLog tmp;
+ tmp.time = statComm.getTime(index);
++ if (!log.size() || !log.rbegin()->second.firstActive){
++ tmp.firstActive = statComm.getNow(index);
++ } else{
++ tmp.firstActive = log.rbegin()->second.firstActive;
++ }
+ tmp.lastSecond = statComm.getLastSecond(index);
+ tmp.down = statComm.getDown(index);
+ tmp.up = statComm.getUp(index);
+@@ -896,9 +930,19 @@ void Controller::statStorage::update(Comms::Sessions &statComm, size_t index){
+ tmp.pktLost = statComm.getPacketLostCount(index);
+ tmp.pktRetransmit = statComm.getPacketRetransmitCount(index);
+ tmp.connectors = statComm.getConnector(index);
++ tmp.streamName = statComm.getStream(index);
++ tmp.host = statComm.getHost(index);
+ log[statComm.getNow(index)] = tmp;
+ // wipe data older than STAT_CUTOFF seconds
+- while (log.size() && log.begin()->first < Util::bootSecs() - STAT_CUTOFF){log.erase(log.begin());}
++ // Ensure cutOffPoint is either time of boot or 10 minutes ago, whichever is closer.
++ // Prevents wrapping around to high values close to system boot time.
++ uint64_t cutOffPoint = Util::bootSecs();
++ if (cutOffPoint > STAT_CUTOFF){
++ cutOffPoint -= STAT_CUTOFF;
++ }else{
++ cutOffPoint = 0;
++ }
++ while (log.size() && log.begin()->first < cutOffPoint){log.erase(log.begin());}
+ }
+
+ void Controller::statLeadIn(){
+@@ -915,6 +959,7 @@ void Controller::statOnActive(size_t id){
+ void Controller::statOnDisconnect(size_t id){
+ // Check to see if cleanup is required (when a Session binary fails)
+ const std::string thisSessionId = statComm.getSessId(id);
++ sessions[thisSessionId].finish();
+ // Try to lock to see if the session crashed during boot
+ IPC::semaphore sessionLock;
+ char semName[NAME_BUFFER_SIZE];
+@@ -932,7 +977,7 @@ void Controller::statOnDisconnect(size_t id){
+ if(dataPage){
+ // Session likely crashed while it was running
+ dataPage.init(userPageName, 1, true);
+- FAIL_MSG("Session '%s' got canceled unexpectedly. Hoovering up the left overs...", thisSessionId.c_str());
++ FAIL_MSG("Session '%s' got cancelled unexpectedly. Cleaning up the leftovers...", thisSessionId.c_str());
+ }
+ // Finally remove the session lock which was created on bootup of the session
+ sessionLock.unlink();
+@@ -999,13 +1044,23 @@ void Controller::fillClients(JSON::Value &req, JSON::Value &rep){
+ if (req.isMember("time")){reqTime = req["time"].asInt();}
+ // to make sure no nasty timing business takes place, we store the case "now" as a bool.
+ bool now = (reqTime == 0);
+- //if greater than current bootsecs, assume unix time and subtract epoch from it
+- if (reqTime > (int64_t)epoch - STAT_CUTOFF){reqTime -= (epoch-bSecs);}
++ //if in the last 600 seconds of unix time (or higher), assume unix time and subtract epoch from it
++ if (reqTime > (int64_t)epoch - STAT_CUTOFF){reqTime -= Controller::systemBoot/1000;}
+ // add the current time, if negative or zero.
+ if (reqTime < 0){reqTime += bSecs;}
+- if (reqTime == 0){reqTime = bSecs - STAT_CUTOFF;}
++ if (reqTime == 0){
++ // Ensure cutOffPoint is either time of boot or 10 minutes ago, whichever is closer.
++ // Prevents wrapping around to high values close to system boot time.
++ uint64_t cutOffPoint = bSecs;
++ if (cutOffPoint > STAT_CUTOFF){
++ cutOffPoint -= STAT_CUTOFF;
++ }else{
++ cutOffPoint = 0;
++ }
++ reqTime = cutOffPoint;
++ }
+ // at this point, we have the absolute timestamp in bootsecs.
+- rep["time"] = reqTime + (epoch-bSecs); // fill the absolute timestamp
++ rep["time"] = reqTime + (Controller::systemBoot/1000); // fill the absolute timestamp
+
+ unsigned int fields = 0;
+ // next, figure out the fields wanted
+@@ -1062,13 +1117,14 @@ void Controller::fillClients(JSON::Value &req, JSON::Value &rep){
+ if (now && reqTime - it->second.getEnd() < 5){time = it->second.getEnd();}
+ // data present and wanted? insert it!
+ if ((it->second.getEnd() >= time && it->second.getStart() <= time) &&
+- (!streams.size() || streams.count(it->second.getStreamName())) &&
+- (!protos.size() || protos.count(it->second.getCurrentProtocols()))){
+- if (it->second.hasDataFor(time)){
++ (!streams.size() || streams.count(it->second.getStreamName(time))) &&
++ (!protos.size() || protos.count(it->second.getConnectors(time)))){
++ const statLog & dta = it->second.curData.getDataFor(time);
++ if (notEmpty(dta)){
+ JSON::Value d;
+- if (fields & STAT_CLI_HOST){d.append(it->second.getHost());}
+- if (fields & STAT_CLI_STREAM){d.append(it->second.getStreamName());}
+- if (fields & STAT_CLI_PROTO){d.append(it->second.getCurrentProtocols());}
++ if (fields & STAT_CLI_HOST){d.append(it->second.getStrHost(time));}
++ if (fields & STAT_CLI_STREAM){d.append(it->second.getStreamName(time));}
++ if (fields & STAT_CLI_PROTO){d.append(it->second.getConnectors(time));}
+ if (fields & STAT_CLI_CONNTIME){d.append(it->second.getConnTime(time));}
+ if (fields & STAT_CLI_POSITION){d.append(it->second.getLastSecond(time));}
+ if (fields & STAT_CLI_DOWN){d.append(it->second.getDown(time));}
+@@ -1252,6 +1308,8 @@ void Controller::fillActive(JSON::Value &req, JSON::Value &rep){
+ F = it->second.currIns;
+ }else if (j->asStringRef() == "outputs"){
+ F = it->second.currOuts;
++ }else if (j->asStringRef() == "unspecified"){
++ F = it->second.currUnspecified;
+ }else if (j->asStringRef() == "views"){
+ F = it->second.viewers;
+ }else if (j->asStringRef() == "viewseconds"){
+@@ -1323,6 +1381,7 @@ public:
+ clients = 0;
+ inputs = 0;
+ outputs = 0;
++ unspecified = 0;
+ downbps = 0;
+ upbps = 0;
+ pktCount = 0;
+@@ -1334,6 +1393,7 @@ public:
+ case Controller::SESS_VIEWER: clients++; break;
+ case Controller::SESS_INPUT: inputs++; break;
+ case Controller::SESS_OUTPUT: outputs++; break;
++ case Controller::SESS_UNSPECIFIED: unspecified++; break;
+ default: break;
+ }
+ downbps += down;
+@@ -1345,6 +1405,7 @@ public:
+ uint64_t clients;
+ uint64_t inputs;
+ uint64_t outputs;
++ uint64_t unspecified;
+ uint64_t downbps;
+ uint64_t upbps;
+ uint64_t pktCount;
+@@ -1363,11 +1424,21 @@ void Controller::fillTotals(JSON::Value &req, JSON::Value &rep){
+ if (req.isMember("start")){reqStart = req["start"].asInt();}
+ if (req.isMember("end")){reqEnd = req["end"].asInt();}
+ //if the reqStart or reqEnd is greater than current bootsecs, assume unix time and subtract epoch from it
+- if (reqStart > (int64_t)epoch - STAT_CUTOFF){reqStart -= (epoch-bSecs);}
+- if (reqEnd > (int64_t)epoch - STAT_CUTOFF){reqEnd -= (epoch-bSecs);}
++ if (reqStart > (int64_t)epoch - STAT_CUTOFF){reqStart -= Controller::systemBoot/1000;}
++ if (reqEnd > (int64_t)epoch - STAT_CUTOFF){reqEnd -= Controller::systemBoot/1000;}
+ // add the current time, if negative or zero.
+ if (reqStart < 0){reqStart += bSecs;}
+- if (reqStart == 0){reqStart = bSecs - STAT_CUTOFF;}
++ if (reqStart == 0){
++ // Ensure cutOffPoint is either time of boot or 10 minutes ago, whichever is closer.
++ // Prevents wrapping around to high values close to system boot time.
++ uint64_t cutOffPoint = bSecs;
++ if (cutOffPoint > STAT_CUTOFF){
++ cutOffPoint -= STAT_CUTOFF;
++ }else{
++ cutOffPoint = 0;
++ }
++ reqStart = cutOffPoint;
++ }
+ if (reqEnd <= 0){reqEnd += bSecs;}
+ // at this point, reqStart and reqEnd are the absolute timestamp in bootsecs.
+ if (reqEnd < reqStart){reqEnd = reqStart;}
+@@ -1417,7 +1488,7 @@ void Controller::fillTotals(JSON::Value &req, JSON::Value &rep){
+ if ((it->second.getEnd() >= (unsigned long long)reqStart ||
+ it->second.getStart() <= (unsigned long long)reqEnd) &&
+ (!streams.size() || streams.count(it->second.getStreamName())) &&
+- (!protos.size() || protos.count(it->second.getCurrentProtocols()))){
++ (!protos.size() || protos.count(it->second.getConnectors()))){
+ for (unsigned long long i = reqStart; i <= reqEnd; ++i){
+ if (it->second.hasDataFor(i)){
+ totalsCount[i].add(it->second.getBpsDown(i), it->second.getBpsUp(i), it->second.getSessType(), it->second.getPktCount(), it->second.getPktLost(), it->second.getPktRetransmit());
+@@ -1436,8 +1507,8 @@ void Controller::fillTotals(JSON::Value &req, JSON::Value &rep){
+ return;
+ }
+ // yay! We have data!
+- rep["start"] = totalsCount.begin()->first + (epoch-bSecs);
+- rep["end"] = totalsCount.rbegin()->first + (epoch-bSecs);
++ rep["start"] = totalsCount.begin()->first + (Controller::systemBoot/1000);
++ rep["end"] = totalsCount.rbegin()->first + (Controller::systemBoot/1000);
+ rep["data"].null();
+ rep["interval"].null();
+ uint64_t prevT = 0;
+@@ -1506,12 +1577,15 @@ void Controller::handlePrometheus(HTTP::Parser &H, Socket::Connection &conn, int
+ uint32_t totViewers = 0;
+ uint32_t totInputs = 0;
+ uint32_t totOutputs = 0;
++ uint32_t totUnspecified = 0;
+ for (uint64_t idx = 0; idx < statComm.recordCount(); idx++){
+ if (statComm.getStatus(idx) == COMM_STATUS_INVALID || statComm.getStatus(idx) & COMM_STATUS_DISCONNECT){continue;}
+ const std::string thisSessId = statComm.getSessId(idx);
+ // Count active viewers, inputs, outputs and protocols
+ if (thisSessId[0] == 'I'){
+ totInputs++;
++ }else if (thisSessId[0] == 'U'){
++ totUnspecified++;
+ }else if (thisSessId[0] == 'O'){
+ totOutputs++;
+ outputs[statComm.getConnector(idx)]++;
+@@ -1602,6 +1676,7 @@ void Controller::handlePrometheus(HTTP::Parser &H, Socket::Connection &conn, int
+ response << "# TYPE mist_sessions_count counter\n";
+ response << "mist_sessions_count{sessType=\"viewers\"}" << servViewers << "\n";
+ response << "mist_sessions_count{sessType=\"incoming\"}" << servInputs << "\n";
++ response << "mist_sessions_count{sessType=\"unspecified\"}" << servUnspecified << "\n";
+ response << "mist_sessions_count{sessType=\"outgoing\"}" << servOutputs << "\n\n";
+
+ response << "# HELP mist_bw_total Count of bytes handled since server start, by direction.\n";
+@@ -1637,6 +1712,7 @@ void Controller::handlePrometheus(HTTP::Parser &H, Socket::Connection &conn, int
+ response << "mist_sessions_total{sessType=\"viewers\"}" << totViewers << "\n";
+ response << "mist_sessions_total{sessType=\"incoming\"}" << totInputs << "\n";
+ response << "mist_sessions_total{sessType=\"outgoing\"}" << totOutputs << "\n";
++ response << "mist_sessions_total{sessType=\"unspecified\"}" << totUnspecified << "\n";
+ response << "mist_sessions_total{sessType=\"cached\"}" << sessions.size() << "\n";
+
+ response << "\n# HELP mist_viewcount Count of unique viewer sessions since stream start, per "
+@@ -1656,6 +1732,8 @@ void Controller::handlePrometheus(HTTP::Parser &H, Socket::Connection &conn, int
+ << it->second.currIns << "\n";
+ response << "mist_sessions{stream=\"" << it->first << "\",sessType=\"outgoing\"}"
+ << it->second.currOuts << "\n";
++ response << "mist_sessions{stream=\"" << it->first << "\",sessType=\"unspecified\"}"
++ << it->second.currUnspecified << "\n";
+ response << "mist_viewcount{stream=\"" << it->first << "\"}" << it->second.viewers << "\n";
+ response << "mist_viewseconds{stream=\"" << it->first << "\"} " << it->second.viewSeconds << "\n";
+ response << "mist_bw{stream=\"" << it->first << "\",direction=\"up\"}" << it->second.upBytes << "\n";
+@@ -1691,9 +1769,11 @@ void Controller::handlePrometheus(HTTP::Parser &H, Socket::Connection &conn, int
+ resp["curr"].append(totViewers);
+ resp["curr"].append(totInputs);
+ resp["curr"].append(totOutputs);
++ resp["curr"].append(totUnspecified);
+ resp["tot"].append(servViewers);
+ resp["tot"].append(servInputs);
+ resp["tot"].append(servOutputs);
++ resp["tot"].append(servUnspecified);
+ resp["st"].append(bw_up_total);
+ resp["st"].append(bw_down_total);
+ resp["bw"].append(servUpBytes);
+@@ -1735,6 +1815,7 @@ void Controller::handlePrometheus(HTTP::Parser &H, Socket::Connection &conn, int
+ resp["streams"][it->first]["curr"].append(it->second.currViews);
+ resp["streams"][it->first]["curr"].append(it->second.currIns);
+ resp["streams"][it->first]["curr"].append(it->second.currOuts);
++ resp["streams"][it->first]["curr"].append(it->second.currUnspecified);
+ resp["streams"][it->first]["pkts"].append(it->second.packSent);
+ resp["streams"][it->first]["pkts"].append(it->second.packLoss);
+ resp["streams"][it->first]["pkts"].append(it->second.packRetrans);
+diff --git a/src/controller/controller_statistics.h b/src/controller/controller_statistics.h
+index f798f811..69605942 100644
+--- a/src/controller/controller_statistics.h
++++ b/src/controller/controller_statistics.h
+@@ -28,16 +28,19 @@ namespace Controller{
+
+ struct statLog{
+ uint64_t time;
++ uint64_t firstActive;
+ uint64_t lastSecond;
+ uint64_t down;
+ uint64_t up;
+ uint64_t pktCount;
+ uint64_t pktLost;
+ uint64_t pktRetransmit;
++ std::string streamName;
++ std::string host;
+ std::string connectors;
+ };
+
+- enum sessType{SESS_UNSET = 0, SESS_INPUT, SESS_OUTPUT, SESS_VIEWER};
++ enum sessType{SESS_UNSET = 0, SESS_INPUT, SESS_OUTPUT, SESS_VIEWER, SESS_UNSPECIFIED};
+
+ class statStorage{
+ public:
+@@ -51,34 +54,30 @@ namespace Controller{
+ /// Allows for moving of connections to another session.
+ class statSession{
+ private:
+- uint64_t firstActive;
+- uint64_t firstSec;
+- uint64_t lastSec;
+ sessType sessionType;
+- bool tracked;
+ uint8_t noBWCount; ///< Set to 2 when not to count for external bandwidth
+- std::string streamName;
+- std::string host;
+- std::string curConnector;
+ std::string sessId;
+
+ public:
+ statSession();
+- ~statSession();
++ void finish();
+ statStorage curData;
+ std::set<std::string> tags;
+ sessType getSessType();
+ void update(uint64_t index, Comms::Sessions &data);
+ uint64_t getStart();
+ uint64_t getEnd();
+- bool isViewerOn(uint64_t time);
+- bool isTracked();
+ bool hasDataFor(uint64_t time);
+- std::string getStreamName();
+- std::string getHost();
+- std::string getSessId();
+- std::string getCurrentProtocols();
+- uint64_t newestDataPoint();
++ const std::string& getSessId();
++ const std::string& getStreamName(uint64_t t);
++ const std::string& getStreamName();
++ std::string getStrHost(uint64_t t);
++ std::string getStrHost();
++ const std::string& getHost(uint64_t t);
++ const std::string& getHost();
++ const std::string& getConnectors(uint64_t t);
++ const std::string& getConnectors();
++ uint64_t getFirstActive();
+ uint64_t getConnTime(uint64_t time);
+ uint64_t getConnTime();
+ uint64_t getLastSecond(uint64_t time);
+diff --git a/src/controller/controller_storage.cpp b/src/controller/controller_storage.cpp
+index bdd52893..532ee4fe 100644
+--- a/src/controller/controller_storage.cpp
++++ b/src/controller/controller_storage.cpp
+@@ -196,6 +196,7 @@ namespace Controller{
+ rlxStrm->addField("viewers", RAX_64UINT);
+ rlxStrm->addField("inputs", RAX_64UINT);
+ rlxStrm->addField("outputs", RAX_64UINT);
++ rlxStrm->addField("unspecified", RAX_64UINT);
+ rlxStrm->setReady();
+ }
+ rlxStrm->setRCount((1024 * 1024 - rlxStrm->getOffset()) / rlxStrm->getRSize());
+@@ -433,12 +434,17 @@ namespace Controller{
+
+ // if fields missing, recreate the page
+ if (globAccX.isReady()){
+- if(globAccX.getFieldAccX("systemBoot")){
++ if(globAccX.getFieldAccX("systemBoot") && globAccX.getInt("systemBoot")){
+ systemBoot = globAccX.getInt("systemBoot");
+ }
+ if(!globAccX.getFieldAccX("defaultStream")
+ || !globAccX.getFieldAccX("systemBoot")
+- || !globAccX.getFieldAccX("sessionMode")){
++ || !globAccX.getFieldAccX("sessionViewerMode")
++ || !globAccX.getFieldAccX("sessionInputMode")
++ || !globAccX.getFieldAccX("sessionOutputMode")
++ || !globAccX.getFieldAccX("sessionUnspecifiedMode")
++ || !globAccX.getFieldAccX("sessionStreamInfoMode")
++ || !globAccX.getFieldAccX("tknMode")){
+ globAccX.setReload();
+ globCfg.master = true;
+ globCfg.close();
+@@ -449,16 +455,24 @@ namespace Controller{
+ if (!globAccX.isReady()){
+ globAccX.addField("defaultStream", RAX_128STRING);
+ globAccX.addField("systemBoot", RAX_64UINT);
+- globAccX.addField("sessionMode", RAX_64UINT);
+- if (!Storage["config"]["sessionMode"]){
+- Storage["config"]["sessionMode"] = SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID;
+- }
++ globAccX.addField("sessionViewerMode", RAX_64UINT);
++ globAccX.addField("sessionInputMode", RAX_64UINT);
++ globAccX.addField("sessionOutputMode", RAX_64UINT);
++ globAccX.addField("sessionUnspecifiedMode", RAX_64UINT);
++ globAccX.addField("sessionStreamInfoMode", RAX_64UINT);
++ globAccX.addField("tknMode", RAX_64UINT);
+ globAccX.setRCount(1);
+ globAccX.setEndPos(1);
+ globAccX.setReady();
+ }
+ globAccX.setString("defaultStream", Storage["config"]["defaultStream"].asStringRef());
+- globAccX.setInt("sessionMode", Storage["config"]["sessionMode"].asInt());
++ globAccX.setInt("sessionViewerMode", Storage["config"]["sessionViewerMode"].asInt());
++ globAccX.setInt("sessionInputMode", Storage["config"]["sessionInputMode"].asInt());
++ globAccX.setInt("sessionOutputMode", Storage["config"]["sessionOutputMode"].asInt());
++ globAccX.setInt("sessionUnspecifiedMode", Storage["config"]["sessionUnspecifiedMode"].asInt());
++ globAccX.setInt("sessionStreamInfoMode", Storage["config"]["sessionStreamInfoMode"].asInt());
++ globAccX.setInt("tknMode", Storage["config"]["tknMode"].asInt());
++ globAccX.setInt("systemBoot", systemBoot);
+ globCfg.master = false; // leave the page after closing
+ }
+ }
+diff --git a/src/controller/controller_storage.h b/src/controller/controller_storage.h
+index 6339cfd0..974d989b 100644
+--- a/src/controller/controller_storage.h
++++ b/src/controller/controller_storage.h
+@@ -16,6 +16,7 @@ namespace Controller{
+ extern bool isTerminal; ///< True if connected to a terminal and not a log file.
+ extern bool isColorized; ///< True if we colorize the output
+ extern uint64_t logCounter; ///< Count of logged messages since boot
++ extern uint64_t systemBoot; ///< Unix time in milliseconds of system boot
+
+ Util::RelAccX *logAccessor();
+ Util::RelAccX *accesslogAccessor();
+diff --git a/src/input/input.cpp b/src/input/input.cpp
+index 0524f55b..782dc4ef 100644
+--- a/src/input/input.cpp
++++ b/src/input/input.cpp
+@@ -502,6 +502,7 @@ namespace Mist{
+ }
+
+ int Input::run(){
++ Comms::sessionConfigCache();
+ if (streamStatus){streamStatus.mapped[0] = STRMSTAT_BOOT;}
+ checkHeaderTimes(config->getString("input"));
+ if (needHeader()){
+@@ -623,6 +624,8 @@ namespace Mist{
+ /// ~~~~~~~~~~~~~~~
+ void Input::serve(){
+ users.reload(streamName, true);
++ Comms::Connections statComm;
++ uint64_t startTime = Util::bootSecs();
+
+ if (!M){
+ // Initialize meta page
+@@ -636,6 +639,7 @@ namespace Mist{
+ meta.setSource(config->getString("input"));
+
+ bool internalOnly = (config->getString("input").find("INTERNAL_ONLY") != std::string::npos);
++ bool isBuffer = (capa["name"].asStringRef() == "Buffer");
+
+ /*LTS-START*/
+ if (Triggers::shouldTrigger("STREAM_READY", config->getString("streamname"))){
+@@ -666,6 +670,18 @@ namespace Mist{
+ }else{
+ if (connectedUsers && M.getValidTracks().size()){activityCounter = Util::bootSecs();}
+ }
++ // Connect to stats for INPUT detection
++ if (!internalOnly && !isBuffer){
++ if (!statComm){statComm.reload(streamName, getConnectedBinHost(), JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "");}
++ if (statComm){
++ uint64_t now = Util::bootSecs();
++ statComm.setNow(now);
++ statComm.setStream(streamName);
++ statComm.setTime(now - startTime);
++ statComm.setLastSecond(0);
++ connStats(statComm);
++ }
++ }
+ // if not shutting down, wait 1 second before looping
+ if (config->is_active){Util::wait(INPUT_USER_INTERVAL);}
+ }
+@@ -820,7 +836,7 @@ namespace Mist{
+
+ if (Util::bootSecs() - statTimer > 1){
+ // Connect to stats for INPUT detection
+- if (!statComm){statComm.reload(streamName, "", JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "", SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID);}
++ if (!statComm){statComm.reload(streamName, getConnectedBinHost(), JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "");}
+ if (statComm){
+ if (!statComm){
+ config->is_active = false;
+@@ -830,7 +846,6 @@ namespace Mist{
+ uint64_t now = Util::bootSecs();
+ statComm.setNow(now);
+ statComm.setStream(streamName);
+- statComm.setConnector("INPUT:" + capa["name"].asStringRef());
+ statComm.setTime(now - startTime);
+ statComm.setLastSecond(0);
+ connStats(statComm);
+@@ -984,7 +999,7 @@ namespace Mist{
+
+ if (Util::bootSecs() - statTimer > 1){
+ // Connect to stats for INPUT detection
+- if (!statComm){statComm.reload(streamName, "", JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "", SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID);}
++ if (!statComm){statComm.reload(streamName, getConnectedBinHost(), JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "");}
+ if (statComm){
+ if (statComm.getStatus() & COMM_STATUS_REQDISCONNECT){
+ config->is_active = false;
+diff --git a/src/input/input_rtsp.cpp b/src/input/input_rtsp.cpp
+index ebb812e1..25be6e23 100644
+--- a/src/input/input_rtsp.cpp
++++ b/src/input/input_rtsp.cpp
+@@ -210,7 +210,7 @@ namespace Mist{
+ if (lastSecs != currSecs){
+ lastSecs = currSecs;
+ // Connect to stats for INPUT detection
+- statComm.reload(streamName, "", JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "", SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID);
++ statComm.reload(streamName, getConnectedBinHost(), JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "");
+ if (statComm){
+ if (statComm.getStatus() & COMM_STATUS_REQDISCONNECT){
+ config->is_active = false;
+@@ -225,7 +225,6 @@ namespace Mist{
+ statComm.setDown(tcpCon.dataDown());
+ statComm.setTime(now - startTime);
+ statComm.setLastSecond(0);
+- statComm.setHost(getConnectedBinHost());
+ }
+ }
+ }
+diff --git a/src/input/input_sdp.cpp b/src/input/input_sdp.cpp
+index 3169a836..0b8ddeb5 100644
+--- a/src/input/input_sdp.cpp
++++ b/src/input/input_sdp.cpp
+@@ -202,7 +202,7 @@ namespace Mist{
+ if (lastSecs != currSecs){
+ lastSecs = currSecs;
+ // Connect to stats for INPUT detection
+- statComm.reload(streamName, "", JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "", SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID);
++ statComm.reload(streamName, getConnectedBinHost(), JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "");
+ if (statComm){
+ if (statComm.getStatus() == COMM_STATUS_REQDISCONNECT){
+ config->is_active = false;
+@@ -217,7 +217,6 @@ namespace Mist{
+ statComm.setUp(bytesUp);
+ statComm.setTime(now - startTime);
+ statComm.setLastSecond(0);
+- statComm.setHost(getConnectedBinHost());
+ }
+ }
+ // If the error flag is raised or we are lacking data, try to recover
+diff --git a/src/input/input_ts.cpp b/src/input/input_ts.cpp
+index 23311dc0..e405e3b6 100644
+--- a/src/input/input_ts.cpp
++++ b/src/input/input_ts.cpp
+@@ -621,7 +621,7 @@ namespace Mist{
+ // Check for and spawn threads here.
+ if (Util::bootSecs() - threadCheckTimer > 1){
+ // Connect to stats for INPUT detection
+- statComm.reload(streamName, "", JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "", SESS_BUNDLE_STREAMNAME_HOSTNAME_SESSIONID);
++ statComm.reload(streamName, getConnectedBinHost(), JSON::Value(getpid()).asString(), "INPUT:" + capa["name"].asStringRef(), "");
+ if (statComm){
+ if (statComm.getStatus() & COMM_STATUS_REQDISCONNECT){
+ config->is_active = false;
+@@ -636,7 +636,6 @@ namespace Mist{
+ statComm.setDown(downCounter + tcpCon.dataDown());
+ statComm.setTime(now - startTime);
+ statComm.setLastSecond(0);
+- statComm.setHost(getConnectedBinHost());
+ }
+
+ std::set<size_t> activeTracks = liveStream.getActiveTracks();
+diff --git a/src/input/input_tsrist.cpp b/src/input/input_tsrist.cpp
+index 5b95aa9f..df658da1 100644
+--- a/src/input/input_tsrist.cpp
++++ b/src/input/input_tsrist.cpp
+@@ -284,7 +284,7 @@ namespace Mist{
+ }
+
+
+- void inputTSRIST::connStats(Comms::Statistics &statComm){
++ void inputTSRIST::connStats(Comms::Connections &statComm){
+ statComm.setUp(0);
+ statComm.setDown(downBytes);
+ statComm.setHost(getConnectedBinHost());
+diff --git a/src/input/input_tsrist.h b/src/input/input_tsrist.h
+index 731f9b04..a75f6601 100644
+--- a/src/input/input_tsrist.h
++++ b/src/input/input_tsrist.h
+@@ -30,7 +30,7 @@ namespace Mist{
+ int64_t timeStampOffset;
+ uint64_t lastTimeStamp;
+
+- virtual void connStats(Comms::Statistics &statComm);
++ virtual void connStats(Comms::Connections &statComm);
+
+ struct rist_ctx *receiver_ctx;
+
+diff --git a/src/output/output.cpp b/src/output/output.cpp
+index 50d67549..d525b8ea 100644
+--- a/src/output/output.cpp
++++ b/src/output/output.cpp
+@@ -92,7 +92,7 @@ namespace Mist{
+ firstTime = 0;
+ firstPacketTime = 0xFFFFFFFFFFFFFFFFull;
+ lastPacketTime = 0;
+- sid = "";
++ tkn = "";
+ parseData = false;
+ wantRequest = true;
+ sought = false;
+@@ -111,7 +111,6 @@ namespace Mist{
+ lastPushUpdate = 0;
+ previousFile = "";
+ currentFile = "";
+- sessionMode = 0xFFFFFFFFFFFFFFFFull;
+
+ lastRecv = Util::bootSecs();
+ if (myConn){
+@@ -230,7 +229,7 @@ namespace Mist{
+ bool Output::isReadyForPlay(){
+ // If a protocol does not support any codecs, we assume you know what you're doing
+ if (!capa.isMember("codecs")){return true;}
+- if (!isInitialized){initialize();}
++ if (!isInitialized){return false;}
+ meta.reloadReplacedPagesIfNeeded();
+ if (getSupportedTracks().size()){
+ size_t minTracks = 2;
+@@ -277,6 +276,7 @@ namespace Mist{
+ /// Assumes streamName class member has been set already.
+ /// Will start input if not currently active, calls onFail() if this does not succeed.
+ void Output::reconnect(){
++ Comms::sessionConfigCache();
+ thisPacket.null();
+ if (config->hasOption("noinput") && config->getBool("noinput")){
+ Util::sanitizeName(streamName);
+@@ -347,11 +347,10 @@ namespace Mist{
+ isInitialized = true;
+
+ //Connect to stats reporting, if not connected already
+- if (!statComm){
+- statComm.reload(streamName, getConnectedHost(), sid, capa["name"].asStringRef(), reqUrl, sessionMode);
+- stats(true);
+- }
+-
++ stats(true);
++ //Abort if the stats code shut us down just now
++ if (!isInitialized){return;}
++
+ //push inputs do not need to wait for stream to be ready for playback
+ if (isPushing()){return;}
+
+@@ -1216,7 +1215,7 @@ namespace Mist{
+ /// request URL (if any)
+ /// ~~~~~~~~~~~~~~~
+ int Output::run(){
+- sessionMode = Util::getGlobalConfig("sessionMode").asInt();
++ Comms::sessionConfigCache();
+ /*LTS-START*/
+ // Connect to file target, if needed
+ if (isFileTarget()){
+@@ -1257,6 +1256,7 @@ namespace Mist{
+ /*LTS-END*/
+ DONTEVEN_MSG("MistOut client handler started");
+ while (keepGoing() && (wantRequest || parseData)){
++ Comms::sessionConfigCache();
+ if (wantRequest){requestHandler();}
+ if (parseData){
+ if (!isInitialized){
+@@ -1779,27 +1779,35 @@ namespace Mist{
+ }
+ }
+
+- if (!statComm){statComm.reload(streamName, getConnectedHost(), sid, capa["name"].asStringRef(), reqUrl, sessionMode);}
+- if (!statComm){return;}
+- if (statComm.getExit()){
++ // Disable stats for HTTP internal output
++ if (Comms::sessionStreamInfoMode == SESS_HTTP_DISABLED && capa["name"].asStringRef() == "HTTP"){return;}
++
++ // Set the token to the pid for outputs which do not generate it in the requestHandler
++ if (!tkn.size()){ tkn = JSON::Value(getpid()).asString(); }
++
++ if (!statComm){
++ statComm.reload(streamName, getConnectedBinHost(), tkn, getStatsName(), reqUrl);
++ }
++ if (!statComm || statComm.getExit()){
+ onFail("Shutting down since this session is not allowed to view this stream");
++ statComm.unload();
+ return;
+- }
++ }
+
+ lastStats = now;
+
+ VERYHIGH_MSG("Writing stats: %s, %s, %s, %" PRIu64 ", %" PRIu64, getConnectedHost().c_str(), streamName.c_str(),
+- sid.c_str(), myConn.dataUp(), myConn.dataDown());
++ tkn.c_str(), myConn.dataUp(), myConn.dataDown());
+ /*LTS-START*/
+ if (statComm.getStatus() & COMM_STATUS_REQDISCONNECT){
+ onFail("Shutting down on controller request");
++ statComm.unload();
+ return;
+ }
+ /*LTS-END*/
+ statComm.setNow(now);
+- statComm.setConnector(getStatsName());
+ connStats(now, statComm);
+- statComm.setLastSecond(thisPacket ? thisPacket.getTime() : 0);
++ statComm.setLastSecond(thisPacket ? thisPacket.getTime()/1000 : 0);
+ statComm.setPid(getpid());
+
+ /*LTS-START*/
+diff --git a/src/output/output.h b/src/output/output.h
+index 173b3840..441c06ab 100644
+--- a/src/output/output.h
++++ b/src/output/output.h
+@@ -130,8 +130,7 @@ namespace Mist{
+
+ Comms::Connections statComm;
+ bool isBlocking; ///< If true, indicates that myConn is blocking.
+- std::string sid; ///< Random identifier used to split connections into sessions
+- uint64_t sessionMode;
++ std::string tkn; ///< Random identifier used to split connections into sessions
+ uint64_t nextKeyTime();
+
+ // stream delaying variables
+diff --git a/src/output/output_cmaf.cpp b/src/output/output_cmaf.cpp
+index c2fbc5bc..eb7c87da 100644
+--- a/src/output/output_cmaf.cpp
++++ b/src/output/output_cmaf.cpp
+@@ -222,24 +222,17 @@ namespace Mist{
+ void OutCMAF::sendHlsMasterManifest(){
+ selectDefaultTracks();
+
+- std::string sessId = "";
+- if (hasSessionIDs()){
+- std::string ua = UA + JSON::Value(getpid()).asString();
+- crc = checksum::crc32(0, ua.data(), ua.size());
+- sessId = JSON::Value(crc).asString();
+- }
+-
+ // check for forced "no low latency" parameter
+ bool noLLHLS = H.GetVar("llhls").size() ? H.GetVar("llhls") == "0" : false;
+
+ // Populate the struct that will help generate the master playlist
+ const HLS::MasterData masterData ={
+- hasSessionIDs(),
++ false,//hasSessionIDs, unused
+ noLLHLS,
+ hlsMediaFormat == ".ts",
+ getMainSelectedTrack(),
+ H.GetHeader("User-Agent"),
+- sessId,
++ (Comms::tknMode & 0x04)?tkn:"",
+ systemBoot,
+ bootMsOffset,
+ };
+@@ -261,11 +254,8 @@ namespace Mist{
+
+ // Chunkpath & Session ID logic
+ std::string urlPrefix = "";
+- std::string sessId = "";
+ if (config->getString("chunkpath").size()){
+ urlPrefix = HTTP::URL(config->getString("chunkpath")).link("./" + H.url).link("./").getUrl();
+- }else{
+- sessId = H.GetVar("sessId");
+ }
+
+ // check for forced "no low latency" parameter
+@@ -279,7 +269,7 @@ namespace Mist{
+ noLLHLS,
+ hlsMediaFormat,
+ M.getEncryption(requestTid),
+- sessId,
++ (Comms::tknMode & 0x04)?tkn:"",
+ timingTid,
+ requestTid,
+ M.biggestFragment(timingTid) / 1000,
+@@ -346,6 +336,16 @@ namespace Mist{
+ std::string url = H.url.substr(H.url.find('/', 6) + 1);
+ HTTP::URL req(reqUrl);
+
++
++ if (tkn.size()){
++ if (Comms::tknMode & 0x08){
++ const std::string koekjes = H.GetHeader("Cookie");
++ std::stringstream cookieHeader;
++ cookieHeader << "tkn=" << tkn << "; Max-Age=" << SESS_TIMEOUT;
++ H.SetHeader("Set-Cookie", cookieHeader.str());
++ }
++ }
++
+ // Send a dash manifest for any URL with .mpd in the path
+ if (req.getExt() == "mpd"){
+ sendDashManifest();
+@@ -438,6 +438,7 @@ namespace Mist{
+ H.SendResponse("400", "Bad Request: Could not parse the url", myConn);
+ return;
+ }
++
+ std::string headerData =
+ CMAF::keyHeader(M, idx, startTime, targetTime, fragmentIndex, false, false);
+
+diff --git a/src/output/output_hls.cpp b/src/output/output_hls.cpp
+index ad810aa7..112e7ee2 100644
+--- a/src/output/output_hls.cpp
++++ b/src/output/output_hls.cpp
+@@ -11,7 +11,7 @@ const std::string hlsMediaFormat = ".ts";
+
+ namespace Mist{
+ bool OutHLS::isReadyForPlay(){
+- if (!isInitialized){initialize();}
++ if (!isInitialized){return false;}
+ meta.reloadReplacedPagesIfNeeded();
+ if (!M.getValidTracks().size()){return false;}
+ uint32_t mainTrack = M.mainTrack();
+@@ -110,25 +110,17 @@ namespace Mist{
+ ///\return The master playlist file for (LL)HLS.
+ void OutHLS::sendHlsMasterManifest(){
+ selectDefaultTracks();
+-
+- std::string sessId = "";
+- if (hasSessionIDs()){
+- std::string ua = UA + JSON::Value(getpid()).asString();
+- crc = checksum::crc32(0, ua.data(), ua.size());
+- sessId = JSON::Value(crc).asString();
+- }
+-
+ // check for forced "no low latency" parameter
+ bool noLLHLS = H.GetVar("llhls").size() ? H.GetVar("llhls") == "0" : false;
+
+ // Populate the struct that will help generate the master playlist
+ const HLS::MasterData masterData ={
+- hasSessionIDs(),
++ false,//hasSessionIDs, unused
+ noLLHLS,
+ hlsMediaFormat == ".ts",
+ getMainSelectedTrack(),
+ H.GetHeader("User-Agent"),
+- sessId,
++ (Comms::tknMode & 0x04)?tkn:"",
+ systemBoot,
+ bootMsOffset,
+ };
+@@ -150,11 +142,8 @@ namespace Mist{
+
+ // Chunkpath & Session ID logic
+ std::string urlPrefix = "";
+- std::string sessId = "";
+ if (config->getString("chunkpath").size()){
+ urlPrefix = HTTP::URL(config->getString("chunkpath")).link("./" + H.url).link("./").getUrl();
+- }else{
+- sessId = H.GetVar("sessId");
+ }
+
+ // check for forced "no low latency" parameter
+@@ -168,7 +157,7 @@ namespace Mist{
+ noLLHLS,
+ hlsMediaFormat,
+ M.getEncryption(requestTid),
+- sessId,
++ (Comms::tknMode & 0x04)?tkn:"",
+ timingTid,
+ requestTid,
+ M.biggestFragment(timingTid) / 1000,
+@@ -226,6 +215,15 @@ namespace Mist{
+ bootMsOffset = 0;
+ if (M.getLive()){bootMsOffset = M.getBootMsOffset();}
+
++ if (tkn.size()){
++ if (Comms::tknMode & 0x08){
++ const std::string koekjes = H.GetHeader("Cookie");
++ std::stringstream cookieHeader;
++ cookieHeader << "tkn=" << tkn << "; Max-Age=" << SESS_TIMEOUT;
++ H.SetHeader("Set-Cookie", cookieHeader.str());
++ }
++ }
++
+ if (H.url == "/crossdomain.xml"){
+ H.SetHeader("Content-Type", "text/xml");
+ H.SetHeader("Server", APPIDENT);
+diff --git a/src/output/output_http.cpp b/src/output/output_http.cpp
+index f55b974d..ed8fa33b 100644
+--- a/src/output/output_http.cpp
++++ b/src/output/output_http.cpp
+@@ -217,7 +217,9 @@ namespace Mist{
+ myConn.close();
+ return;
+ }
+- if (handler != capa["name"].asStringRef() || H.GetVar("stream") != streamName){
++
++ //Check if we need to change binary and/or reconnect
++ if (handler != capa["name"].asStringRef() || H.GetVar("stream") != streamName || (statComm && (statComm.getHost() != getConnectedBinHost() || statComm.getTkn() != tkn))){
+ MEDIUM_MSG("Switching from %s (%s) to %s (%s)", capa["name"].asStringRef().c_str(),
+ streamName.c_str(), handler.c_str(), H.GetVar("stream").c_str());
+ streamName = H.GetVar("stream");
+@@ -268,21 +270,32 @@ namespace Mist{
+ realTime = 0;
+ }
+ }
+- // Get session ID cookie or generate a random one if it wasn't set
+- if (!sid.size()){
++ // Read the session token
++ if (Comms::tknMode & 0x01){
++ // Get session token from the request url
++ if (H.GetVar("tkn") != ""){
++ tkn = H.GetVar("tkn");
++ } else if (H.GetVar("sid") != ""){
++ tkn = H.GetVar("sid");
++ } else if (H.GetVar("sessId") != ""){
++ tkn = H.GetVar("sessId");
++ }
++ }
++ if ((Comms::tknMode & 0x02) && !tkn.size()){
++ // Get session token from the request cookie
+ std::map<std::string, std::string> storage;
+ const std::string koekjes = H.GetHeader("Cookie");
+- HTTP::parseVars(koekjes, storage);
+- if (storage.count("sid")){
+- // Get sid cookie, which is used to divide connections into sessions
+- sid = storage.at("sid");
+- }else{
+- // Else generate one
+- const std::string newSid = UA + JSON::Value(getpid()).asString();
+- sid = JSON::Value(checksum::crc32(0, newSid.data(), newSid.size())).asString();
+- H.SetHeader("sid", sid.c_str());
++ HTTP::parseVars(koekjes, storage, "; ");
++ if (storage.count("tkn")){
++ tkn = storage.at("tkn");
+ }
+ }
++ // Generate a session token if it is being sent as a cookie or url parameter and we couldn't read one
++ if (!tkn.size() && Comms::tknMode > 3){
++ const std::string newTkn = UA + JSON::Value(getpid()).asString();
++ tkn = JSON::Value(checksum::crc32(0, newTkn.data(), newTkn.size())).asString();
++ HIGH_MSG("Generated tkn '%s'", tkn.c_str());
++ }
+ // Handle upgrade to websocket if the output supports it
+ std::string upgradeHeader = H.GetHeader("Upgrade");
+ Util::stringToLower(upgradeHeader);
+@@ -290,7 +303,9 @@ namespace Mist{
+ INFO_MSG("Switching to Websocket mode");
+ setBlocking(false);
+ preWebsocketConnect();
+- webSock = new HTTP::Websocket(myConn, H);
++ HTTP::Parser req = H;
++ H.Clean();
++ webSock = new HTTP::Websocket(myConn, req, H);
+ if (!(*webSock)){
+ delete webSock;
+ webSock = 0;
+@@ -333,6 +348,14 @@ namespace Mist{
+ void HTTPOutput::respondHTTP(const HTTP::Parser & req, bool headersOnly){
+ //We generally want the CORS headers to be set for all responses
+ H.setCORSHeaders();
++ H.SetHeader("Server", APPIDENT);
++ if (tkn.size()){
++ if (Comms::tknMode & 0x08){
++ std::stringstream cookieHeader;
++ cookieHeader << "tkn=" << tkn << "; Max-Age=" << SESS_TIMEOUT;
++ H.SetHeader("Set-Cookie", cookieHeader.str());
++ }
++ }
+ //Set attachment header to force download, if applicable
+ if (req.GetVar("dl").size()){
+ //If we want to download, and the string contains a dot, use as-is.
+@@ -395,6 +418,8 @@ namespace Mist{
+ ///\brief Handles requests by starting a corresponding output process.
+ ///\param connector The type of connector to be invoked.
+ void HTTPOutput::reConnector(std::string &connector){
++ // Clear tkn in order to deal with reverse proxies
++ tkn = "";
+ // taken from CheckProtocols (controller_connectors.cpp)
+ char *argarr[32];
+ for (int i = 0; i < 32; i++){argarr[i] = 0;}
+diff --git a/src/output/output_http_internal.cpp b/src/output/output_http_internal.cpp
+index 97ca455f..be955196 100644
+--- a/src/output/output_http_internal.cpp
++++ b/src/output/output_http_internal.cpp
+@@ -76,11 +76,11 @@ namespace Mist{
+ std::string method = H.method;
+ // send logo icon
+ if (H.url.length() > 4 && H.url.substr(H.url.length() - 4, 4) == ".ico"){
+- sendIcon();
++ sendIcon(false);
+ return;
+ }
+ if (H.url.length() > 6 && H.url.substr(H.url.length() - 5, 5) == ".html"){
+- HTMLResponse();
++ HTMLResponse(H, false);
+ return;
+ }
+ if (H.url.size() >= 3 && H.url.substr(H.url.size() - 3) == ".js"){
+@@ -337,9 +337,9 @@ namespace Mist{
+ }
+ }
+
+- void OutHTTP::HTMLResponse(){
+- std::string method = H.method;
+- HTTP::URL fullURL(H.GetHeader("Host"));
++ void OutHTTP::HTMLResponse(const HTTP::Parser & req, bool headersOnly){
++ HTTPOutput::respondHTTP(req, headersOnly);
++ HTTP::URL fullURL(req.GetHeader("Host"));
+ if (!fullURL.protocol.size()){fullURL.protocol = getProtocolForPort(fullURL.getPort());}
+ if (config->getString("pubaddr") != ""){
+ HTTP::URL altURL(config->getString("pubaddr"));
+@@ -349,24 +349,22 @@ namespace Mist{
+ fullURL.path = altURL.path;
+ }
+ if (mistPath.size()){fullURL = mistPath;}
+- std::string uAgent = H.GetHeader("User-Agent");
++ std::string uAgent = req.GetHeader("User-Agent");
+
+ std::string forceType = "";
+ if (H.GetVar("forcetype").size()){
+- forceType = ",forceType:\"" + H.GetVar("forcetype") + "\"";
++ forceType = ",forceType:\"" + req.GetVar("forcetype") + "\"";
+ }
+
+ std::string devSkin = "";
+- if (H.GetVar("dev").size()){devSkin = ",skin:\"dev\"";}
+- H.SetVar("stream", "");
+- H.SetVar("dev", "");
++ if (req.GetVar("dev").size()){devSkin = ",skin:\"dev\"";}
+ devSkin += ",urlappend:\"" + H.allVars() + "\"";
+ H.SetVar("stream", streamName);
+
+ std::string seekTo = "";
+- if (H.GetVar("t").size()){
++ if (req.GetVar("t").size()){
+ uint64_t autoSeekTime = 0;
+- std::string sTime = H.GetVar("t");
++ std::string sTime = req.GetVar("t");
+ unsigned long long h = 0, m = 0, s = 0;
+ autoSeekTime = JSON::Value(sTime).asInt();
+ if (sscanf(sTime.c_str(), "%llum%llus", &m, &s) == 2){autoSeekTime = m * 60 + s;}
+@@ -385,13 +383,10 @@ namespace Mist{
+ streamName + "\").addEventListener(\"initialized\",f);";
+ }
+ }
+-
+- H.Clean();
++
+ H.SetHeader("Content-Type", "text/html");
+ H.SetHeader("X-UA-Compatible", "IE=edge");
+- H.SetHeader("Server", APPIDENT);
+- H.setCORSHeaders();
+- if (method == "OPTIONS" || method == "HEAD"){
++ if (headersOnly){
+ H.SendResponse("200", "OK", myConn);
+ responded = true;
+ H.Clean();
+@@ -427,6 +422,7 @@ namespace Mist{
+ }
+ H.SendResponse("200", "OK", myConn);
+ responded = true;
++ H.Clean();
+ }
+
+ JSON::Value OutHTTP::getStatusJSON(std::string &reqHost, const std::string &useragent){
+@@ -634,23 +630,31 @@ namespace Mist{
+
+ // loop over the added sources, add them to json_resp["sources"]
+ for (std::set<JSON::Value, sourceCompare>::iterator it = sources.begin(); it != sources.end(); it++){
+- if ((*it)["simul_tracks"].asInt() > 0){json_resp["source"].append(*it);}
++ if ((*it)["simul_tracks"].asInt() > 0){
++ if (Comms::tknMode & 0x04){
++ JSON::Value tmp;
++ tmp = (*it);
++ tmp["url"] = tmp["url"].asStringRef() + "?tkn=" + tkn;
++ tmp["relurl"] = tmp["relurl"].asStringRef() + "?tkn=" + tkn;
++ json_resp["source"].append(tmp);
++ }else{
++ json_resp["source"].append(*it);
++ }
++ }
+ }
+ return json_resp;
+ }
+
+- void OutHTTP::onHTTP(){
++ void OutHTTP::respondHTTP(const HTTP::Parser & req, bool headersOnly){
+ origStreamName = streamName;
+- std::string method = H.method;
+
+- if (H.GetHeader("X-Mst-Path").size()){mistPath = H.GetHeader("X-Mst-Path");}
++ if (req.GetHeader("X-Mst-Path").size()){mistPath = req.GetHeader("X-Mst-Path");}
+
+ // Handle certbot validations
+- if (H.url.substr(0, 28) == "/.well-known/acme-challenge/"){
++ if (req.url.substr(0, 28) == "/.well-known/acme-challenge/"){
+ std::string cbToken = H.url.substr(28);
+ jsonForEach(config->getOption("certbot", true), it){
+ if (it->asStringRef().substr(0, cbToken.size() + 1) == cbToken + ":"){
+- H.Clean();
+ H.SetHeader("Content-Type", "text/plain");
+ H.SetHeader("Server", APPIDENT);
+ H.setCORSHeaders();
+@@ -661,9 +665,7 @@ namespace Mist{
+ return;
+ }
+ }
+- H.Clean();
+ H.SetHeader("Content-Type", "text/plain");
+- H.SetHeader("Server", APPIDENT);
+ H.setCORSHeaders();
+ H.SetBody("No matching validation found for token '" + cbToken + "'");
+ H.SendResponse("404", "Not found", myConn);
+@@ -672,12 +674,11 @@ namespace Mist{
+ return;
+ }
+
+- if (H.url == "/crossdomain.xml"){
+- H.Clean();
++ if (req.url == "/crossdomain.xml"){
+ H.SetHeader("Content-Type", "text/xml");
+ H.SetHeader("Server", APPIDENT);
+ H.setCORSHeaders();
+- if (method == "OPTIONS" || method == "HEAD"){
++ if (headersOnly){
+ H.SendResponse("200", "OK", myConn);
+ responded = true;
+ H.Clean();
+@@ -693,12 +694,11 @@ namespace Mist{
+ return;
+ }// crossdomain.xml
+
+- if (H.url == "/clientaccesspolicy.xml"){
+- H.Clean();
++ if (req.url == "/clientaccesspolicy.xml"){
+ H.SetHeader("Content-Type", "text/xml");
+ H.SetHeader("Server", APPIDENT);
+ H.setCORSHeaders();
+- if (method == "OPTIONS" || method == "HEAD"){
++ if (headersOnly){
+ H.SendResponse("200", "OK", myConn);
+ responded = true;
+ H.Clean();
+@@ -716,8 +716,7 @@ namespace Mist{
+ return;
+ }// clientaccesspolicy.xml
+
+- if (H.url == "/flashplayer.swf"){
+- H.Clean();
++ if (req.url == "/flashplayer.swf"){
+ H.SetHeader("Content-Type", "application/x-shockwave-flash");
+ H.SetHeader("Server", APPIDENT);
+ H.SetBody((const char *)FlashMediaPlayback_101_swf, FlashMediaPlayback_101_swf_len);
+@@ -725,8 +724,7 @@ namespace Mist{
+ responded = true;
+ return;
+ }
+- if (H.url == "/oldflashplayer.swf"){
+- H.Clean();
++ if (req.url == "/oldflashplayer.swf"){
+ H.SetHeader("Content-Type", "application/x-shockwave-flash");
+ H.SetHeader("Server", APPIDENT);
+ H.SetBody((const char *)FlashMediaPlayback_swf, FlashMediaPlayback_swf_len);
+@@ -735,20 +733,21 @@ namespace Mist{
+ return;
+ }
+ // send logo icon
+- if (H.url.length() > 4 && H.url.substr(H.url.length() - 4, 4) == ".ico"){
+- sendIcon();
++ if (req.url.length() > 4 && req.url.substr(req.url.length() - 4, 4) == ".ico"){
++ sendIcon(headersOnly);
+ return;
+ }
+
+ // send generic HTML page
+- if (H.url.length() > 6 && H.url.substr(H.url.length() - 5, 5) == ".html"){
+- HTMLResponse();
++ if (req.url.length() > 6 && req.url.substr(req.url.length() - 5, 5) == ".html"){
++ HTMLResponse(req, headersOnly);
+ return;
+ }
+
+ // send smil MBR index
+- if (H.url.length() > 6 && H.url.substr(H.url.length() - 5, 5) == ".smil"){
+- std::string reqHost = HTTP::URL(H.GetHeader("Host")).host;
++ if (req.url.length() > 6 && req.url.substr(req.url.length() - 5, 5) == ".smil"){
++ HTTPOutput::respondHTTP(req, headersOnly);
++ std::string reqHost = HTTP::URL(req.GetHeader("Host")).host;
+ std::string port, url_rel;
+ std::string trackSources; // this string contains all track sources for MBR smil
+ {
+@@ -782,11 +781,8 @@ namespace Mist{
+ }
+ }
+
+- H.Clean();
+ H.SetHeader("Content-Type", "application/smil");
+- H.SetHeader("Server", APPIDENT);
+- H.setCORSHeaders();
+- if (method == "OPTIONS" || method == "HEAD"){
++ if (headersOnly){
+ H.SendResponse("200", "OK", myConn);
+ responded = true;
+ H.Clean();
+@@ -800,24 +796,22 @@ namespace Mist{
+ return;
+ }
+
+- if ((H.url.length() > 9 && H.url.substr(0, 6) == "/info_" && H.url.substr(H.url.length() - 3, 3) == ".js") ||
+- (H.url.length() > 9 && H.url.substr(0, 6) == "/json_" && H.url.substr(H.url.length() - 3, 3) == ".js")){
+- if (websocketHandler()){return;}
+- std::string reqHost = HTTP::URL(H.GetHeader("Host")).host;
+- std::string useragent = H.GetVar("ua");
+- if (!useragent.size()){useragent = H.GetHeader("User-Agent");}
++ if ((req.url.length() > 9 && req.url.substr(0, 6) == "/info_" && req.url.substr(req.url.length() - 3, 3) == ".js") ||
++ (req.url.length() > 9 && req.url.substr(0, 6) == "/json_" && req.url.substr(req.url.length() - 3, 3) == ".js")){
++ HTTPOutput::respondHTTP(req, headersOnly);
++ if (websocketHandler(req, headersOnly)){return;}
++ std::string reqHost = HTTP::URL(req.GetHeader("Host")).host;
++ std::string useragent = req.GetVar("ua");
++ if (!useragent.size()){useragent = req.GetHeader("User-Agent");}
+ std::string response;
+- std::string rURL = H.url;
+- if (method != "OPTIONS" && method != "HEAD"){initialize();}
+- H.Clean();
+- H.SetHeader("Server", APPIDENT);
+- H.setCORSHeaders();
++ std::string rURL = req.url;
++ if (headersOnly){initialize();}
+ if (rURL.substr(0, 6) != "/json_"){
+ H.SetHeader("Content-Type", "application/javascript");
+ }else{
+ H.SetHeader("Content-Type", "application/json");
+ }
+- if (method == "OPTIONS" || method == "HEAD"){
++ if (headersOnly){
+ H.SendResponse("200", "OK", myConn);
+ responded = true;
+ H.Clean();
+@@ -837,9 +831,9 @@ namespace Mist{
+ return;
+ }// embed code generator
+
+- if ((H.url == "/player.js") || ((H.url.substr(0, 7) == "/embed_") && (H.url.length() > 10) &&
+- (H.url.substr(H.url.length() - 3, 3) == ".js"))){
+- HTTP::URL fullURL(H.GetHeader("Host"));
++ if ((req.url == "/player.js") || ((req.url.substr(0, 7) == "/embed_") && (req.url.length() > 10) &&
++ (req.url.substr(H.url.length() - 3, 3) == ".js"))){
++ HTTP::URL fullURL(req.GetHeader("Host"));
+ if (!fullURL.protocol.size()){fullURL.protocol = getProtocolForPort(fullURL.getPort());}
+ if (config->getString("pubaddr") != ""){
+ HTTP::URL altURL(config->getString("pubaddr"));
+@@ -850,12 +844,17 @@ namespace Mist{
+ }
+ if (mistPath.size()){fullURL = mistPath;}
+ std::string response;
+- std::string rURL = H.url;
+- H.Clean();
++ std::string rURL = req.url;
++
++ if ((rURL.substr(0, 7) == "/embed_") && (rURL.length() > 10) &&
++ (rURL.substr(rURL.length() - 3, 3) == ".js")){
++ HTTPOutput::respondHTTP(req, headersOnly);
++ }
++
+ H.SetHeader("Server", APPIDENT);
+ H.setCORSHeaders();
+ H.SetHeader("Content-Type", "application/javascript; charset=utf-8");
+- if (method == "OPTIONS" || method == "HEAD"){
++ if (headersOnly){
+ H.SendResponse("200", "OK", myConn);
+ responded = true;
+ H.Clean();
+@@ -933,14 +932,13 @@ namespace Mist{
+ return;
+ }
+
+- if (H.url.substr(0, 7) == "/skins/"){
++ if (req.url.substr(0, 7) == "/skins/"){
+ std::string response;
+- std::string url = H.url;
+- H.Clean();
++ std::string url = req.url;
+ H.SetHeader("Server", APPIDENT);
+ H.setCORSHeaders();
+ H.SetHeader("Content-Type", "text/css");
+- if (method == "OPTIONS" || method == "HEAD"){
++ if (headersOnly){
+ H.SendResponse("200", "OK", myConn);
+ responded = true;
+ H.Clean();
+@@ -970,13 +968,12 @@ namespace Mist{
+ H.Clean();
+ return;
+ }
+- if (H.url == "/videojs.js"){
++ if (req.url == "/videojs.js"){
+ std::string response;
+- H.Clean();
+ H.SetHeader("Server", APPIDENT);
+ H.setCORSHeaders();
+ H.SetHeader("Content-Type", "application/javascript");
+- if (method == "OPTIONS" || method == "HEAD"){
++ if (headersOnly){
+ H.SendResponse("200", "OK", myConn);
+ responded = true;
+ H.Clean();
+@@ -992,13 +989,12 @@ namespace Mist{
+ H.Clean();
+ return;
+ }
+- if (H.url == "/dashjs.js"){
++ if (req.url == "/dashjs.js"){
+ std::string response;
+- H.Clean();
+ H.SetHeader("Server", APPIDENT);
+ H.setCORSHeaders();
+ H.SetHeader("Content-Type", "application/javascript");
+- if (method == "OPTIONS" || method == "HEAD"){
++ if (headersOnly){
+ H.SendResponse("200", "OK", myConn);
+ responded = true;
+ H.Clean();
+@@ -1016,13 +1012,12 @@ namespace Mist{
+ H.Clean();
+ return;
+ }
+- if (H.url == "/webrtc.js"){
++ if (req.url == "/webrtc.js"){
+ std::string response;
+- H.Clean();
+ H.SetHeader("Server", APPIDENT);
+ H.setCORSHeaders();
+ H.SetHeader("Content-Type", "application/javascript");
+- if (method == "OPTIONS" || method == "HEAD"){
++ if (headersOnly){
+ H.SendResponse("200", "OK", myConn);
+ responded = true;
+ H.Clean();
+@@ -1038,13 +1033,12 @@ namespace Mist{
+ H.Clean();
+ return;
+ }
+- if (H.url == "/flv.js"){
++ if (req.url == "/flv.js"){
+ std::string response;
+- H.Clean();
+ H.SetHeader("Server", "MistServer/" PACKAGE_VERSION);
+ H.setCORSHeaders();
+ H.SetHeader("Content-Type", "application/javascript");
+- if (method == "OPTIONS" || method == "HEAD"){
++ if (headersOnly){
+ H.SendResponse("200", "OK", myConn);
+ H.Clean();
+ return;
+@@ -1058,13 +1052,12 @@ namespace Mist{
+ H.Clean();
+ return;
+ }
+- if (H.url == "/hlsjs.js"){
++ if (req.url == "/hlsjs.js"){
+ std::string response;
+- H.Clean();
+ H.SetHeader("Server", "MistServer/" PACKAGE_VERSION);
+ H.setCORSHeaders();
+ H.SetHeader("Content-Type", "application/javascript");
+- if (method == "OPTIONS" || method == "HEAD"){
++ if (headersOnly){
+ H.SendResponse("200", "OK", myConn);
+ H.Clean();
+ return;
+@@ -1084,7 +1077,7 @@ namespace Mist{
+ H.SetHeader("Server", "MistServer/" PACKAGE_VERSION);
+ H.setCORSHeaders();
+ H.SetHeader("Content-Type", "application/javascript");
+- if (method == "OPTIONS" || method == "HEAD"){
++ if (headersOnly){
+ H.SendResponse("200", "OK", myConn);
+ H.Clean();
+ return;
+@@ -1100,15 +1093,13 @@ namespace Mist{
+ }
+ }
+
+- void OutHTTP::sendIcon(){
+- std::string method = H.method;
+- H.Clean();
++ void OutHTTP::sendIcon(bool headersOnly){
+ #include "../icon.h"
+ H.SetHeader("Content-Type", "image/x-icon");
+ H.SetHeader("Server", APPIDENT);
+ H.SetHeader("Content-Length", icon_len);
+ H.setCORSHeaders();
+- if (method == "OPTIONS" || method == "HEAD"){
++ if (headersOnly){
+ H.SendResponse("200", "OK", myConn);
+ responded = true;
+ H.Clean();
+@@ -1120,16 +1111,16 @@ namespace Mist{
+ H.Clean();
+ }
+
+- bool OutHTTP::websocketHandler(){
++ bool OutHTTP::websocketHandler(const HTTP::Parser & req, bool headersOnly){
+ stayConnected = true;
+- std::string reqHost = HTTP::URL(H.GetHeader("Host")).host;
+- if (H.GetHeader("X-Mst-Path").size()){mistPath = H.GetHeader("X-Mst-Path");}
+- std::string useragent = H.GetVar("ua");
+- if (!useragent.size()){useragent = H.GetHeader("User-Agent");}
+- std::string upgradeHeader = H.GetHeader("Upgrade");
++ std::string reqHost = HTTP::URL(req.GetHeader("Host")).host;
++ if (req.GetHeader("X-Mst-Path").size()){mistPath = req.GetHeader("X-Mst-Path");}
++ std::string useragent = req.GetVar("ua");
++ if (!useragent.size()){useragent = req.GetHeader("User-Agent");}
++ std::string upgradeHeader = req.GetHeader("Upgrade");
+ Util::stringToLower(upgradeHeader);
+ if (upgradeHeader != "websocket"){return false;}
+- HTTP::Websocket ws(myConn, H);
++ HTTP::Websocket ws(myConn, req, H);
+ if (!ws){return false;}
+ setBlocking(false);
+ // start the stream, if needed
+diff --git a/src/output/output_http_internal.h b/src/output/output_http_internal.h
+index 774eb186..8e610145 100644
+--- a/src/output/output_http_internal.h
++++ b/src/output/output_http_internal.h
+@@ -10,10 +10,10 @@ namespace Mist{
+ virtual void onFail(const std::string &msg, bool critical = false);
+ /// preHTTP is disabled in the internal HTTP output, since most don't need the stream alive to work
+ virtual void preHTTP(){};
+- void HTMLResponse();
+- void onHTTP();
+- void sendIcon();
+- bool websocketHandler();
++ void HTMLResponse(const HTTP::Parser & req, bool headersOnly);
++ void respondHTTP(const HTTP::Parser & req, bool headersOnly);
++ void sendIcon(bool headersOnly);
++ bool websocketHandler(const HTTP::Parser & req, bool headersOnly);
+ JSON::Value getStatusJSON(std::string &reqHost, const std::string &useragent = "");
+ bool stayConnected;
+ virtual bool onFinish(){return stayConnected;}
+diff --git a/src/output/output_sdp.cpp b/src/output/output_sdp.cpp
+index 6baf9067..50d491e8 100644
+--- a/src/output/output_sdp.cpp
++++ b/src/output/output_sdp.cpp
+@@ -141,6 +141,18 @@ namespace Mist{
+ }
+ }
+
++ std::string OutSDP::getConnectedHost(){
++ if (!sdpState.tracks.size()) { return Output::getConnectedHost(); }
++ std::string hostname;
++ uint32_t port;
++ sdpState.tracks[0].data.GetDestination(hostname, port);
++ return hostname;
++ }
++ std::string OutSDP::getConnectedBinHost(){
++ if (!sdpState.tracks.size()) { return Output::getConnectedBinHost(); }
++ return sdpState.tracks[0].data.getBinDestination();
++ }
++
+ void OutSDP::sendNext(){
+ char *dataPointer = 0;
+ size_t dataLen = 0;
+diff --git a/src/output/output_sdp.h b/src/output/output_sdp.h
+index 2d4b3368..6c466753 100644
+--- a/src/output/output_sdp.h
++++ b/src/output/output_sdp.h
+@@ -16,6 +16,8 @@ namespace Mist{
+ void sendNext();
+ void sendHeader();
+ bool onFinish();
++ std::string getConnectedHost();
++ std::string getConnectedBinHost();
+
+ private:
+ void initTracks(uint32_t & port, std::string targetIP);
+diff --git a/src/output/output_ts.cpp b/src/output/output_ts.cpp
+index 561bf9aa..bf46663b 100644
+--- a/src/output/output_ts.cpp
++++ b/src/output/output_ts.cpp
+@@ -239,6 +239,18 @@ namespace Mist{
+ }
+ }
+
++ std::string OutTS::getConnectedHost(){
++ if (!pushOut) { return Output::getConnectedHost(); }
++ std::string hostname;
++ uint32_t port;
++ pushSock.GetDestination(hostname, port);
++ return hostname;
++ }
++ std::string OutTS::getConnectedBinHost(){
++ if (!pushOut) { return Output::getConnectedBinHost(); }
++ return pushSock.getBinDestination();
++ }
++
+ bool OutTS::listenMode(){return !(config->getString("target").size());}
+
+ void OutTS::onRequest(){
+diff --git a/src/output/output_ts.h b/src/output/output_ts.h
+index 32aa6958..0c38cc70 100644
+--- a/src/output/output_ts.h
++++ b/src/output/output_ts.h
+@@ -12,6 +12,8 @@ namespace Mist{
+ virtual void initialSeek();
+ bool isReadyForPlay();
+ void onRequest();
++ std::string getConnectedHost();
++ std::string getConnectedBinHost();
+
+ private:
+ size_t udpSize;
+diff --git a/src/output/output_tsrist.cpp b/src/output/output_tsrist.cpp
+index c4e03f80..10b29673 100644
+--- a/src/output/output_tsrist.cpp
++++ b/src/output/output_tsrist.cpp
+@@ -178,6 +178,20 @@ namespace Mist{
+ }
+
+ OutTSRIST::~OutTSRIST(){}
++
++ std::string OutTSRIST::getConnectedHost(){
++ if (!pushOut) { return Output::getConnectedHost(); }
++ return target.host;
++ }
++ std::string OutTSRIST::getConnectedBinHost(){
++ if (!pushOut) { return Output::getConnectedBinHost(); }
++ std::string binHost = Socket::getBinForms(target.host);
++ if (binHost.size() > 16){ binHost = binHost.substr(0, 16); }
++ if (binHost.size() < 16){
++ binHost = std::string("\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\001", 16);
++ }
++ return binHost;
++ }
+
+ void OutTSRIST::init(Util::Config *cfg){
+ Output::init(cfg);
+@@ -319,7 +333,7 @@ namespace Mist{
+ }
+ }
+
+- void OutTSRIST::connStats(uint64_t now, Comms::Statistics &statComm){
++ void OutTSRIST::connStats(uint64_t now, Comms::Connections &statComm){
+ if (!myConn){return;}
+ statComm.setUp(upBytes);
+ statComm.setDown(0);
+diff --git a/src/output/output_tsrist.h b/src/output/output_tsrist.h
+index c9a5db39..80ee2376 100644
+--- a/src/output/output_tsrist.h
++++ b/src/output/output_tsrist.h
+@@ -16,9 +16,11 @@ namespace Mist{
+ bool isReadyForPlay(){return true;}
+ virtual void requestHandler();
+ static void listener(Util::Config &conf, int (*callback)(Socket::Connection &S));
++ std::string getConnectedHost();
++ std::string getConnectedBinHost();
+
+ protected:
+- virtual void connStats(uint64_t now, Comms::Statistics &statComm);
++ virtual void connStats(uint64_t now, Comms::Connections &statComm);
+ //virtual std::string getConnectedHost(){
+ // return srtConn.remotehost;
+ //}
+diff --git a/src/session.cpp b/src/session.cpp
+index 3865e0ec..d2331c53 100644
+--- a/src/session.cpp
++++ b/src/session.cpp
+@@ -7,17 +7,31 @@
+ #include <mist/triggers.h>
+ #include <signal.h>
+ #include <stdio.h>
+-// Stats of connections which have closed are added to these global counters
+-uint64_t globalNow = 0;
++
++// Global counters
++uint64_t now = Util::bootSecs();
++uint64_t currentConnections = 0;
++uint64_t lastSecond = 0;
+ uint64_t globalTime = 0;
+ uint64_t globalDown = 0;
+ uint64_t globalUp = 0;
+ uint64_t globalPktcount = 0;
+ uint64_t globalPktloss = 0;
+ uint64_t globalPktretrans = 0;
++// Stores last values of each connection
++std::map<size_t, uint64_t> connTime;
++std::map<size_t, uint64_t> connDown;
++std::map<size_t, uint64_t> connUp;
++std::map<size_t, uint64_t> connPktcount;
++std::map<size_t, uint64_t> connPktloss;
++std::map<size_t, uint64_t> connPktretrans;
+ // Counts the duration a connector has been active
+ std::map<std::string, uint64_t> connectorCount;
+ std::map<std::string, uint64_t> connectorLastActive;
++std::map<std::string, uint64_t> hostCount;
++std::map<std::string, uint64_t> hostLastActive;
++std::map<std::string, uint64_t> streamCount;
++std::map<std::string, uint64_t> streamLastActive;
+ // Set to True when a session gets invalidated, so that we know to run a new USER_NEW trigger
+ bool forceTrigger = false;
+ void handleSignal(int signum){
+@@ -26,96 +40,141 @@ void handleSignal(int signum){
+ }
+ }
+
+-void userOnActive(uint64_t &connections){
+- ++connections;
+-}
++const char nullAddress[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+
+-std::string getEnvWithDefault(const std::string variableName, const std::string defaultValue){
+- const char* value = getenv(variableName.c_str());
+- if (value){
+- unsetenv(variableName.c_str());
+- return value;
+- }else{
+- return defaultValue;
+- }
++void userOnActive(Comms::Connections &connections, size_t idx){
++ uint64_t lastUpdate = connections.getNow(idx);
++ if (lastUpdate < now - 10){return;}
++ ++currentConnections;
++ std::string thisConnector = connections.getConnector(idx);
++ std::string thisStreamName = connections.getStream(idx);
++ const std::string& thisHost = connections.getHost(idx);
++
++ if (connections.getLastSecond(idx) > lastSecond){lastSecond = connections.getLastSecond(idx);}
++ // Save info on the latest active stream, protocol and host separately
++ if (thisConnector.size() && thisConnector != "HTTP"){
++ connectorCount[thisConnector]++;
++ if (connectorLastActive[thisConnector] < lastUpdate){connectorLastActive[thisConnector] = lastUpdate;}
++ }
++ if (thisStreamName.size()){
++ streamCount[thisStreamName]++;
++ if (streamLastActive[thisStreamName] < lastUpdate){streamLastActive[thisStreamName] = lastUpdate;}
++ }
++ if (memcmp(thisHost.data(), nullAddress, 16)){
++ hostCount[thisHost]++;
++ if (!hostLastActive.count(thisHost) || hostLastActive[thisHost] < lastUpdate){hostLastActive[thisHost] = lastUpdate;}
++ }
++ // Sanity checks
++ if (connections.getDown(idx) < connDown[idx]){
++ WARN_MSG("Connection downloaded bytes should be a counter, but has decreased in value");
++ connDown[idx] = connections.getDown(idx);
++ }
++ if (connections.getUp(idx) < connUp[idx]){
++ WARN_MSG("Connection uploaded bytes should be a counter, but has decreased in value");
++ connUp[idx] = connections.getUp(idx);
++ }
++ if (connections.getPacketCount(idx) < connPktcount[idx]){
++ WARN_MSG("Connection packet count should be a counter, but has decreased in value");
++ connPktcount[idx] = connections.getPacketCount(idx);
++ }
++ if (connections.getPacketLostCount(idx) < connPktloss[idx]){
++ WARN_MSG("Connection packet loss count should be a counter, but has decreased in value");
++ connPktloss[idx] = connections.getPacketLostCount(idx);
++ }
++ if (connections.getPacketRetransmitCount(idx) < connPktretrans[idx]){
++ WARN_MSG("Connection packets retransmitted should be a counter, but has decreased in value");
++ connPktretrans[idx] = connections.getPacketRetransmitCount(idx);
++ }
++ // Add increase in stats to global stats
++ globalDown += connections.getDown(idx) - connDown[idx];
++ globalUp += connections.getUp(idx) - connUp[idx];
++ globalPktcount += connections.getPacketCount(idx) - connPktcount[idx];
++ globalPktloss += connections.getPacketLostCount(idx) - connPktloss[idx];
++ globalPktretrans += connections.getPacketRetransmitCount(idx) - connPktretrans[idx];
++ // Set last values of this connection
++ connTime[idx]++;
++ connDown[idx] = connections.getDown(idx);
++ connUp[idx] = connections.getUp(idx);
++ connPktcount[idx] = connections.getPacketCount(idx);
++ connPktloss[idx] = connections.getPacketLostCount(idx);
++ connPktretrans[idx] = connections.getPacketRetransmitCount(idx);
+ }
+
+-/// \brief Adds stats of closed connections to global counters
++/// \brief Remove mappings of inactive connections
+ void userOnDisconnect(Comms::Connections & connections, size_t idx){
+- std::string thisConnector = connections.getConnector(idx);
+- if (thisConnector != ""){
+- connectorCount[thisConnector] += connections.getTime(idx);
+- }
+- globalTime += connections.getTime(idx);
+- globalDown += connections.getDown(idx);
+- globalUp += connections.getUp(idx);
+- globalPktcount += connections.getPacketCount(idx);
+- globalPktloss += connections.getPacketLostCount(idx);
+- globalPktretrans += connections.getPacketRetransmitCount(idx);
++ connTime.erase(idx);
++ connDown.erase(idx);
++ connUp.erase(idx);
++ connPktcount.erase(idx);
++ connPktloss.erase(idx);
++ connPktretrans.erase(idx);
+ }
+
+ int main(int argc, char **argv){
+ Comms::Connections connections;
+ Comms::Sessions sessions;
+ uint64_t lastSeen = Util::bootSecs();
+- uint64_t currentConnections = 0;
+ Util::redirectLogsIfNeeded();
+ signal(SIGUSR1, handleSignal);
+ // Init config and parse arguments
+ Util::Config config = Util::Config("MistSession");
+ JSON::Value option;
++ char * tmpStr = 0;
+
+ option.null();
+ option["arg_num"] = 1;
+ option["arg"] = "string";
+ option["help"] = "Session identifier of the entire session";
+- option["default"] = "";
+ config.addOption("sessionid", option);
+
+- option.null();
+- option["long"] = "sessionmode";
+- option["short"] = "m";
+- option["arg"] = "integer";
+- option["default"] = 0;
+- config.addOption("sessionmode", option);
+-
+ option.null();
+ option["long"] = "streamname";
+- option["short"] = "n";
++ option["short"] = "s";
+ option["arg"] = "string";
+- option["default"] = "";
++ option["help"] = "Stream name initial value. May also be passed as SESSION_STREAM";
++ tmpStr = getenv("SESSION_STREAM");
++ option["default"] = tmpStr?tmpStr:"";
+ config.addOption("streamname", option);
+
+ option.null();
+ option["long"] = "ip";
+ option["short"] = "i";
+ option["arg"] = "string";
+- option["default"] = "";
++ option["help"] = "IP address initial value. May also be passed as SESSION_IP";
++ tmpStr = getenv("SESSION_IP");
++ option["default"] = tmpStr?tmpStr:"";
+ config.addOption("ip", option);
+
+ option.null();
+- option["long"] = "sid";
+- option["short"] = "s";
++ option["long"] = "tkn";
++ option["short"] = "t";
+ option["arg"] = "string";
+- option["default"] = "";
+- config.addOption("sid", option);
++ option["help"] = "Client-side session ID initial value. May also be passed as SESSION_TKN";
++ tmpStr = getenv("SESSION_TKN");
++ option["default"] = tmpStr?tmpStr:"";
++ config.addOption("tkn", option);
+
+ option.null();
+ option["long"] = "protocol";
+ option["short"] = "p";
+ option["arg"] = "string";
+- option["default"] = "";
++ option["help"] = "Protocol initial value. May also be passed as SESSION_PROTOCOL";
++ tmpStr = getenv("SESSION_PROTOCOL");
++ option["default"] = tmpStr?tmpStr:"";
+ config.addOption("protocol", option);
+
+ option.null();
+ option["long"] = "requrl";
+ option["short"] = "r";
+ option["arg"] = "string";
+- option["default"] = "";
++ option["help"] = "Request URL initial value. May also be passed as SESSION_REQURL";
++ tmpStr = getenv("SESSION_REQURL");
++ option["default"] = tmpStr?tmpStr:"";
+ config.addOption("requrl", option);
+
+ config.activate();
+ if (!(config.parseArgs(argc, argv))){
++ config.printHelp(std::cout);
+ FAIL_MSG("Cannot start a new session due to invalid arguments");
+ return 1;
+ }
+@@ -123,24 +182,17 @@ int main(int argc, char **argv){
+ const uint64_t bootTime = Util::getMicros();
+ // Get session ID, session mode and other variables used as payload for the USER_NEW and USER_END triggers
+ const std::string thisStreamName = config.getString("streamname");
+- const std::string thisHost = config.getString("ip");
+- const std::string thisSid = config.getString("sid");
++ const std::string thisToken = config.getString("tkn");
+ const std::string thisProtocol = config.getString("protocol");
+ const std::string thisReqUrl = config.getString("requrl");
+ const std::string thisSessionId = config.getString("sessionid");
+- const uint64_t sessionMode = config.getInteger("sessionmode");
+-
+- if (thisSessionId == "" || thisProtocol == "" || thisStreamName == ""){
+- FAIL_MSG("Given the following incomplete arguments: SessionId: '%s', protocol: '%s', stream name: '%s'. Aborting opening a new session",
+- thisSessionId.c_str(), thisProtocol.c_str(), thisStreamName.c_str());
+- return 1;
+- }
++ std::string thisHost = Socket::getBinForms(config.getString("ip"));
++ if (thisHost.size() > 16){thisHost = thisHost.substr(0, 16);}
+
+- MEDIUM_MSG("Starting a new session for sessionId '%s'", thisSessionId.c_str());
+- if (sessionMode < 1 || sessionMode > 15) {
+- FAIL_MSG("Invalid session mode of value %lu. Should be larger than 0 and smaller than 16", sessionMode);
+- return 1;
+- }
++ std::string ipHex;
++ Socket::hostBytesToStr(thisHost.c_str(), thisHost.size(), ipHex);
++ VERYHIGH_MSG("Starting a new session. Passed variables are stream name '%s', session token '%s', protocol '%s', requested URL '%s', IP '%s' and session id '%s'",
++ thisStreamName.c_str(), thisToken.c_str(), thisProtocol.c_str(), thisReqUrl.c_str(), ipHex.c_str(), thisSessionId.c_str());
+
+ // Try to lock to ensure we are the only process initialising this session
+ IPC::semaphore sessionLock;
+@@ -174,194 +226,200 @@ int main(int argc, char **argv){
+ sessionLock.post();
+ return 1;
+ }
+- // Open the shared memory page containing statistics for each individual connection in this session
+- connections.reload(thisStreamName, thisHost, thisSid, thisProtocol, thisReqUrl, sessionMode, true, false);
++
+ // Initialise global session data
+ sessions.setHost(thisHost);
+ sessions.setSessId(thisSessionId);
+ sessions.setStream(thisStreamName);
+- sessionLock.post();
++ if (thisProtocol.size() && thisProtocol != "HTTP"){connectorLastActive[thisProtocol] = now;}
++ if (thisStreamName.size()){streamLastActive[thisStreamName] = now;}
++ if (memcmp(thisHost.data(), nullAddress, 16)){hostLastActive[thisHost] = now;}
++
++ // Open the shared memory page containing statistics for each individual connection in this session
++ connections.reload(thisSessionId, true);
+
+ // Determine session type, since triggers only get run for viewer type sessions
+ uint64_t thisType = 0;
+ if (thisSessionId[0] == 'I'){
+- INFO_MSG("Started new input session %s in %lu microseconds", thisSessionId.c_str(), Util::getMicros(bootTime));
+ thisType = 1;
+- }
+- else if (thisSessionId[0] == 'O'){
+- INFO_MSG("Started new output session %s in %lu microseconds", thisSessionId.c_str(), Util::getMicros(bootTime));
++ } else if (thisSessionId[0] == 'O'){
+ thisType = 2;
+- }
+- else{
+- INFO_MSG("Started new viewer session %s in %lu microseconds", thisSessionId.c_str(), Util::getMicros(bootTime));
++ } else if (thisSessionId[0] == 'U'){
++ thisType = 3;
+ }
+
+ // Do a USER_NEW trigger if it is defined for this stream
+ if (!thisType && Triggers::shouldTrigger("USER_NEW", thisStreamName)){
+- std::string payload = thisStreamName + "\n" + thisHost + "\n" +
+- thisSid + "\n" + thisProtocol +
++ std::string payload = thisStreamName + "\n" + config.getString("ip") + "\n" +
++ thisToken + "\n" + thisProtocol +
+ "\n" + thisReqUrl + "\n" + thisSessionId;
+ if (!Triggers::doTrigger("USER_NEW", payload, thisStreamName)){
+ // Mark all connections of this session as finished, since this viewer is not allowed to view this stream
++ Util::logExitReason("Session rejected by USER_NEW");
+ connections.setExit();
+ connections.finishAll();
+ }
+ }
+
+- uint64_t lastSecond = 0;
+- uint64_t now = 0;
+- uint64_t time = 0;
+- uint64_t down = 0;
+- uint64_t up = 0;
+- uint64_t pktcount = 0;
+- uint64_t pktloss = 0;
+- uint64_t pktretrans = 0;
+- std::string connector = "";
++ //start allowing viewers
++ sessionLock.post();
++
++ INFO_MSG("Started new session %s in %.3f ms", thisSessionId.c_str(), (double)Util::getMicros(bootTime)/1000.0);
++
+ // Stay active until Mist exits or we no longer have an active connection
+- while (config.is_active && (currentConnections || Util::bootSecs() - lastSeen <= 10)){
+- time = 0;
+- connector = "";
+- down = 0;
+- up = 0;
+- pktcount = 0;
+- pktloss = 0;
+- pktretrans = 0;
++ while (config.is_active && (currentConnections || now - lastSeen <= STATS_DELAY) && !connections.getExit()){
+ currentConnections = 0;
++ lastSecond = 0;
++ now = Util::bootSecs();
+
+- // Count active connections
+- COMM_LOOP(connections, userOnActive(currentConnections), userOnDisconnect(connections, id));
+ // Loop through all connection entries to get a summary of statistics
+- for (uint64_t idx = 0; idx < connections.recordCount(); idx++){
+- if (connections.getStatus(idx) == COMM_STATUS_INVALID || connections.getStatus(idx) & COMM_STATUS_DISCONNECT){continue;}
+- uint64_t thisLastSecond = connections.getLastSecond(idx);
+- std::string thisConnector = connections.getConnector(idx);
+- // Save info on the latest active connection separately
+- if (thisLastSecond > lastSecond){
+- lastSecond = thisLastSecond;
+- now = connections.getNow(idx);
+- }
+- connectorLastActive[thisConnector] = thisLastSecond;
+- // Sum all other variables
+- time += connections.getTime(idx);
+- down += connections.getDown(idx);
+- up += connections.getUp(idx);
+- pktcount += connections.getPacketCount(idx);
+- pktloss += connections.getPacketLostCount(idx);
+- pktretrans += connections.getPacketRetransmitCount(idx);
++ COMM_LOOP(connections, userOnActive(connections, id), userOnDisconnect(connections, id));
++ if (currentConnections){
++ globalTime++;
++ lastSeen = now;
+ }
+
+- // Convert connector duration to string
+- std::stringstream connectorSummary;
+- bool addDelimiter = false;
+- connectorSummary << "{";
+- for (std::map<std::string, uint64_t>::iterator it = connectorLastActive.begin();
+- it != connectorLastActive.end(); ++it){
+- if (lastSecond - it->second < 10000){
+- connectorSummary << (addDelimiter ? "," : "") << it->first;
+- addDelimiter = true;
+- }
+- }
+- connectorSummary << "}";
+-
+- // Write summary to global statistics
+- sessions.setTime(time + globalTime);
+- sessions.setDown(down + globalDown);
+- sessions.setUp(up + globalUp);
+- sessions.setPacketCount(pktcount + globalPktcount);
+- sessions.setPacketLostCount(pktloss + globalPktloss);
+- sessions.setPacketRetransmitCount(pktretrans + globalPktretrans);
++
++ sessions.setTime(globalTime);
++ sessions.setDown(globalDown);
++ sessions.setUp(globalUp);
++ sessions.setPacketCount(globalPktcount);
++ sessions.setPacketLostCount(globalPktloss);
++ sessions.setPacketRetransmitCount(globalPktretrans);
+ sessions.setLastSecond(lastSecond);
+- sessions.setConnector(connectorSummary.str());
+ sessions.setNow(now);
+
++ if (currentConnections){
++ {
++ // Convert active protocols to string
++ std::stringstream connectorSummary;
++ for (std::map<std::string, uint64_t>::iterator it = connectorLastActive.begin();
++ it != connectorLastActive.end(); ++it){
++ if (now - it->second < STATS_DELAY){
++ connectorSummary << (connectorSummary.str().size() ? "," : "") << it->first;
++ }
++ }
++ sessions.setConnector(connectorSummary.str());
++ }
++
++ {
++ // Set active host to last active or 0 if there were various hosts active recently
++ std::string thisHost;
++ for (std::map<std::string, uint64_t>::iterator it = hostLastActive.begin();
++ it != hostLastActive.end(); ++it){
++ if (now - it->second < STATS_DELAY){
++ if (!thisHost.size()){
++ thisHost = it->first;
++ }else if (thisHost != it->first){
++ thisHost = nullAddress;
++ break;
++ }
++ }
++ }
++ if (!thisHost.size()){
++ thisHost = nullAddress;
++ }
++ sessions.setHost(thisHost);
++ }
++
++ {
++ // Set active stream name to last active or "" if there were multiple streams active recently
++ std::string thisStream = "";
++ for (std::map<std::string, uint64_t>::iterator it = streamLastActive.begin();
++ it != streamLastActive.end(); ++it){
++ if (now - it->second < STATS_DELAY){
++ if (!thisStream.size()){
++ thisStream = it->first;
++ }else if (thisStream != it->first){
++ thisStream = "";
++ break;
++ }
++ }
++ }
++ sessions.setStream(thisStream);
++ }
++ }
++
+ // Retrigger USER_NEW if a re-sync was requested
+ if (!thisType && forceTrigger){
+ forceTrigger = false;
++ std::string host;
++ Socket::hostBytesToStr(thisHost.data(), 16, host);
+ if (Triggers::shouldTrigger("USER_NEW", thisStreamName)){
+ INFO_MSG("Triggering USER_NEW for stream %s", thisStreamName.c_str());
+- std::string payload = thisStreamName + "\n" + thisHost + "\n" +
+- thisSid + "\n" + thisProtocol +
++ std::string payload = thisStreamName + "\n" + host + "\n" +
++ thisToken + "\n" + thisProtocol +
+ "\n" + thisReqUrl + "\n" + thisSessionId;
+ if (!Triggers::doTrigger("USER_NEW", payload, thisStreamName)){
+ INFO_MSG("USER_NEW rejected stream %s", thisStreamName.c_str());
++ Util::logExitReason("Session rejected by USER_NEW");
+ connections.setExit();
+ connections.finishAll();
++ break;
+ }else{
+ INFO_MSG("USER_NEW accepted stream %s", thisStreamName.c_str());
+ }
+ }
+ }
+
+- // Invalidate connections if the session is marked as invalid
+- if(connections.getExit()){
+- connections.finishAll();
+- break;
+- }
+ // Remember latest activity so we know when this session ends
+ if (currentConnections){
+- lastSeen = Util::bootSecs();
+ }
+- Util::sleep(1000);
++ Util::wait(1000);
++ }
++ if (Util::bootSecs() - lastSeen > STATS_DELAY){
++ Util::logExitReason("Session inactive for %d seconds", STATS_DELAY);
+ }
+
+ // Trigger USER_END
+ if (!thisType && Triggers::shouldTrigger("USER_END", thisStreamName)){
+- lastSecond = 0;
+- time = 0;
+- down = 0;
+- up = 0;
+-
+- // Get a final summary of this session
+- for (uint64_t idx = 0; idx < connections.recordCount(); idx++){
+- if (connections.getStatus(idx) == COMM_STATUS_INVALID || connections.getStatus(idx) & COMM_STATUS_DISCONNECT){continue;}
+- uint64_t thisLastSecond = connections.getLastSecond(idx);
+- // Set last second to the latest entry
+- if (thisLastSecond > lastSecond){
+- lastSecond = thisLastSecond;
+- }
+- // Count protocol durations across the entire session
+- std::string thisConnector = connections.getConnector(idx);
+- if (thisConnector != ""){
+- connectorCount[thisConnector] += connections.getTime(idx);
+- }
+- // Sum all other variables
+- time += connections.getTime(idx);
+- down += connections.getDown(idx);
+- up += connections.getUp(idx);
+- }
+
+- // Convert connector duration to string
++ // Convert connector, host and stream into lists and counts
+ std::stringstream connectorSummary;
+- bool addDelimiter = false;
+- connectorSummary << "{";
+- for (std::map<std::string, uint64_t>::iterator it = connectorCount.begin();
+- it != connectorCount.end(); ++it){
+- connectorSummary << (addDelimiter ? "," : "") << it->first << ":" << it->second;
+- addDelimiter = true;
++ std::stringstream connectorTimes;
++ for (std::map<std::string, uint64_t>::iterator it = connectorCount.begin(); it != connectorCount.end(); ++it){
++ connectorSummary << (connectorSummary.str().size() ? "," : "") << it->first;
++ connectorTimes << (connectorTimes.str().size() ? "," : "") << it->second;
++ }
++ std::stringstream hostSummary;
++ std::stringstream hostTimes;
++ for (std::map<std::string, uint64_t>::iterator it = hostCount.begin(); it != hostCount.end(); ++it){
++ std::string host;
++ Socket::hostBytesToStr(it->first.data(), 16, host);
++ hostSummary << (hostSummary.str().size() ? "," : "") << host;
++ hostTimes << (hostTimes.str().size() ? "," : "") << it->second;
++ }
++ std::stringstream streamSummary;
++ std::stringstream streamTimes;
++ for (std::map<std::string, uint64_t>::iterator it = streamCount.begin(); it != streamCount.end(); ++it){
++ streamSummary << (streamSummary.str().size() ? "," : "") << it->first;
++ streamTimes << (streamTimes.str().size() ? "," : "") << it->second;
+ }
+- connectorSummary << "}";
+
+- const uint64_t duration = lastSecond - (bootTime / 1000);
+ std::stringstream summary;
+- summary << thisSessionId << "\n"
+- << thisStreamName << "\n"
++ summary << thisToken << "\n"
++ << streamSummary.str() << "\n"
+ << connectorSummary.str() << "\n"
+- << thisHost << "\n"
+- << duration << "\n"
+- << up << "\n"
+- << down << "\n"
+- << sessions.getTags();
++ << hostSummary.str() << "\n"
++ << globalTime << "\n"
++ << globalUp << "\n"
++ << globalDown << "\n"
++ << sessions.getTags() << "\n"
++ << hostTimes.str() << "\n"
++ << connectorTimes.str() << "\n"
++ << streamTimes.str() << "\n"
++ << thisSessionId;
+ Triggers::doTrigger("USER_END", summary.str(), thisStreamName);
+ }
+
+ if (!thisType && connections.getExit()){
+- WARN_MSG("Session %s has been invalidated since it is not allowed to view stream %s", thisSessionId.c_str(), thisStreamName.c_str());
+ uint64_t sleepStart = Util::bootSecs();
+ // Keep session invalidated for 10 minutes, or until the session stops
+- while (config.is_active && sleepStart - Util::bootSecs() < 600){
++ while (config.is_active && Util::bootSecs() - sleepStart < SESS_TIMEOUT){
+ Util::sleep(1000);
++ if (forceTrigger){break;}
+ }
+ }
+- INFO_MSG("Shutting down session %s", thisSessionId.c_str());
++ INFO_MSG("Shutting down session %s: %s", thisSessionId.c_str(), Util::exitReason);
+ return 0;
+ }
+--
+2.25.1
+
+
+From 1a4a526a112d0bdeaa0f269a133fe55c39a0d129 Mon Sep 17 00:00:00 2001
+From: Cat <carinasadres@gmail.com>
+Date: Wed, 5 Oct 2022 03:10:04 +0200
+Subject: [PATCH 28/38] LSP: Moved some settings to new "General" tab, added
+ bitmask inputtype, removed LTSonly code
+
+---
+ lsp/main.css | 30 +++
+ lsp/minified.js | 396 ++++++++++++++++----------------
+ lsp/mist.js | 582 +++++++++++++++++++++++++++++-------------------
+ 3 files changed, 590 insertions(+), 418 deletions(-)
+
+diff --git a/lsp/main.css b/lsp/main.css
+index 6c72e6a7..9289d211 100644
+--- a/lsp/main.css
++++ b/lsp/main.css
+@@ -279,6 +279,21 @@ main > button {
+ width: 1.5em;
+ flex-grow: 0;
+ }
++.input_container .field_container .field.bitmask {
++ display: flex;
++ flex-flow: row wrap;
++ justify-content: space-between;
++ padding: 0;
++ margin-bottom: 0.5em;
++}
++.input_container .field_container .field.bitmask > label {
++ display: flex;
++ flex-flow: row nowrap;
++ align-items: center;
++ margin-right: -0.25em;
++}
++.input_container .field_container .field.bitmask > label > input { margin: 0; }
++.input_container .field_container .field.bitmask > label > span { margin: 0 0.25em; }
+ textarea {
+ font-size: 0.8em;
+ }
+@@ -349,6 +364,20 @@ select option[value=""] {
+ display: block;
+ z-index: 0;
+ }
++.input_container label.active { position: relative; }
++.input_container label.active:after {
++ content: "";
++ display: block;
++ position: absolute;
++ z-index: -1;
++ top: -0.25em;
++ left: -0.25em;
++ width: 55.5em;
++ bottom: -0.25em;
++ background: linear-gradient(to right,#9ec2da,transparent 55em);
++ opacity: 0.3;
++ filter: blur(0.1em);
++}
+ .input_container label.active .ih_balloon,
+ .input_container > .help_container .ih_balloon {
+ display: block;
+@@ -577,6 +606,7 @@ table.valigntop th {
+ }
+ .input_container .field_container .field.inputlist > * {
+ width: 100%;
++ box-sizing: border-box;
+ }
+ .input_container .field_container .field.checkcontainer {
+ font-size: 0.8em;
+diff --git a/lsp/minified.js b/lsp/minified.js
+index 52a6f6bc..9db52d63 100644
+--- a/lsp/minified.js
++++ b/lsp/minified.js
+@@ -1,11 +1,11 @@
+-var MD5=function(a){function b(a,b){var c,d,g,i,e;g=a&2147483648;i=b&2147483648;c=a&1073741824;d=b&1073741824;e=(a&1073741823)+(b&1073741823);return c&d?e^2147483648^g^i:c|d?e&1073741824?e^3221225472^g^i:e^1073741824^g^i:e^g^i}function c(a,c,d,g,i,e,f){a=b(a,b(b(c&d|~c&g,i),f));return b(a<<e|a>>>32-e,c)}function d(a,c,d,g,i,e,f){a=b(a,b(b(c&g|d&~g,i),f));return b(a<<e|a>>>32-e,c)}function e(a,c,d,g,e,i,f){a=b(a,b(b(c^d^g,e),f));return b(a<<i|a>>>32-i,c)}function l(a,c,d,g,i,e,f){a=b(a,b(b(d^(c|~g),
+-i),f));return b(a<<e|a>>>32-e,c)}function m(a){var b="",c="",d;for(d=0;3>=d;d++)c=a>>>8*d&255,c="0"+c.toString(16),b+=c.substr(c.length-2,2);return b}var f=[],t,o,k,w,h,g,i,j,f=a.replace(/\r\n/g,"\n"),a="";for(t=0;t<f.length;t++)o=f.charCodeAt(t),128>o?a+=String.fromCharCode(o):(127<o&&2048>o?a+=String.fromCharCode(o>>6|192):(a+=String.fromCharCode(o>>12|224),a+=String.fromCharCode(o>>6&63|128)),a+=String.fromCharCode(o&63|128));f=a;a=f.length;t=a+8;o=16*((t-t%64)/64+1);k=Array(o-1);for(h=w=0;h<a;)t=
+-(h-h%4)/4,w=8*(h%4),k[t]|=f.charCodeAt(h)<<w,h++;t=(h-h%4)/4;k[t]|=128<<8*(h%4);k[o-2]=a<<3;k[o-1]=a>>>29;f=k;h=1732584193;g=4023233417;i=2562383102;j=271733878;for(a=0;a<f.length;a+=16)t=h,o=g,k=i,w=j,h=c(h,g,i,j,f[a+0],7,3614090360),j=c(j,h,g,i,f[a+1],12,3905402710),i=c(i,j,h,g,f[a+2],17,606105819),g=c(g,i,j,h,f[a+3],22,3250441966),h=c(h,g,i,j,f[a+4],7,4118548399),j=c(j,h,g,i,f[a+5],12,1200080426),i=c(i,j,h,g,f[a+6],17,2821735955),g=c(g,i,j,h,f[a+7],22,4249261313),h=c(h,g,i,j,f[a+8],7,1770035416),
+-j=c(j,h,g,i,f[a+9],12,2336552879),i=c(i,j,h,g,f[a+10],17,4294925233),g=c(g,i,j,h,f[a+11],22,2304563134),h=c(h,g,i,j,f[a+12],7,1804603682),j=c(j,h,g,i,f[a+13],12,4254626195),i=c(i,j,h,g,f[a+14],17,2792965006),g=c(g,i,j,h,f[a+15],22,1236535329),h=d(h,g,i,j,f[a+1],5,4129170786),j=d(j,h,g,i,f[a+6],9,3225465664),i=d(i,j,h,g,f[a+11],14,643717713),g=d(g,i,j,h,f[a+0],20,3921069994),h=d(h,g,i,j,f[a+5],5,3593408605),j=d(j,h,g,i,f[a+10],9,38016083),i=d(i,j,h,g,f[a+15],14,3634488961),g=d(g,i,j,h,f[a+4],20,3889429448),
+-h=d(h,g,i,j,f[a+9],5,568446438),j=d(j,h,g,i,f[a+14],9,3275163606),i=d(i,j,h,g,f[a+3],14,4107603335),g=d(g,i,j,h,f[a+8],20,1163531501),h=d(h,g,i,j,f[a+13],5,2850285829),j=d(j,h,g,i,f[a+2],9,4243563512),i=d(i,j,h,g,f[a+7],14,1735328473),g=d(g,i,j,h,f[a+12],20,2368359562),h=e(h,g,i,j,f[a+5],4,4294588738),j=e(j,h,g,i,f[a+8],11,2272392833),i=e(i,j,h,g,f[a+11],16,1839030562),g=e(g,i,j,h,f[a+14],23,4259657740),h=e(h,g,i,j,f[a+1],4,2763975236),j=e(j,h,g,i,f[a+4],11,1272893353),i=e(i,j,h,g,f[a+7],16,4139469664),
+-g=e(g,i,j,h,f[a+10],23,3200236656),h=e(h,g,i,j,f[a+13],4,681279174),j=e(j,h,g,i,f[a+0],11,3936430074),i=e(i,j,h,g,f[a+3],16,3572445317),g=e(g,i,j,h,f[a+6],23,76029189),h=e(h,g,i,j,f[a+9],4,3654602809),j=e(j,h,g,i,f[a+12],11,3873151461),i=e(i,j,h,g,f[a+15],16,530742520),g=e(g,i,j,h,f[a+2],23,3299628645),h=l(h,g,i,j,f[a+0],6,4096336452),j=l(j,h,g,i,f[a+7],10,1126891415),i=l(i,j,h,g,f[a+14],15,2878612391),g=l(g,i,j,h,f[a+5],21,4237533241),h=l(h,g,i,j,f[a+12],6,1700485571),j=l(j,h,g,i,f[a+3],10,2399980690),
+-i=l(i,j,h,g,f[a+10],15,4293915773),g=l(g,i,j,h,f[a+1],21,2240044497),h=l(h,g,i,j,f[a+8],6,1873313359),j=l(j,h,g,i,f[a+15],10,4264355552),i=l(i,j,h,g,f[a+6],15,2734768916),g=l(g,i,j,h,f[a+13],21,1309151649),h=l(h,g,i,j,f[a+4],6,4149444226),j=l(j,h,g,i,f[a+11],10,3174756917),i=l(i,j,h,g,f[a+2],15,718787259),g=l(g,i,j,h,f[a+9],21,3951481745),h=b(h,t),g=b(g,o),i=b(i,k),j=b(j,w);return(m(h)+m(g)+m(i)+m(j)).toLowerCase()};(function(a){a.fn.stupidtable=function(){a(this).on("click","thead th",function(){a(this).stupidsort()})};a.fn.stupidsort=function(){function b(b){var c=0,d;a(b).children("td,th").each(function(){if(c==t)return d=a(this),!1;var b=a(this).attr("colspan");c+=b?Number(b):1});b="undefined"!=typeof d.data("sort-value")?d.data("sort-value"):"undefined"!=typeof d.attr("data-sort-value")?d.attr("data-sort-value"):d.text();switch(m){case "string":case "string-ins":b=String(b).toLowerCase();break;case "int":b=
+-parseInt(Number(b));break;case "float":b=Number(b)}return b}var c=a(this),d=c.closest("table"),e=d.children("tbody"),l=e.children("tr"),m=c.attr("data-sort-type");if(m){var f=!0;c.hasClass("sorting-asc")&&(f=!1);var t=0;c.prevAll().each(function(){var b=a(this).attr("colspan");t+=b?Number(b):1});l.sort(function(a,c){var d=f?1:-1,a=b(a),c=b(c);return a>c?1*d:a<c?-1*d:0});e.append(l);d.find("thead th").removeClass("sorting-asc").removeClass("sorting-desc");c.addClass(f?"sorting-asc":"sorting-desc")}}})(jQuery);$(function(){UI.elements={menu:$("nav > .menu"),main:$("main"),header:$("header"),connection:{status:$("#connection"),user_and_host:$("#user_and_host"),msg:$("#message")}};UI.buildMenu();UI.stored.getOpts();try{if("mistLogin"in sessionStorage){var a=JSON.parse(sessionStorage.mistLogin);mist.user.name=a.name;mist.user.password=a.password;mist.user.host=a.host}}catch(b){}location.hash&&(a=decodeURIComponent(location.hash).substring(1).split("@")[0].split("&"),mist.user.name=a[0],a[1]&&(mist.user.host=
++var MD5=function(a){function b(a,b){var c,d,g,f,e;g=a&2147483648;f=b&2147483648;c=a&1073741824;d=b&1073741824;e=(a&1073741823)+(b&1073741823);return c&d?e^2147483648^g^f:c|d?e&1073741824?e^3221225472^g^f:e^1073741824^g^f:e^g^f}function c(a,c,d,g,f,e,h){a=b(a,b(b(c&d|~c&g,f),h));return b(a<<e|a>>>32-e,c)}function d(a,c,d,g,e,f,h){a=b(a,b(b(c&g|d&~g,e),h));return b(a<<f|a>>>32-f,c)}function e(a,c,d,g,f,e,h){a=b(a,b(b(c^d^g,f),h));return b(a<<e|a>>>32-e,c)}function l(a,c,d,g,e,f,h){a=b(a,b(b(d^(c|~g),
++e),h));return b(a<<f|a>>>32-f,c)}function n(a){var b="",c="",d;for(d=0;3>=d;d++)c=a>>>8*d&255,c="0"+c.toString(16),b+=c.substr(c.length-2,2);return b}var h=[],t,m,k,w,i,g,f,j,h=a.replace(/\r\n/g,"\n"),a="";for(t=0;t<h.length;t++)m=h.charCodeAt(t),128>m?a+=String.fromCharCode(m):(127<m&&2048>m?a+=String.fromCharCode(m>>6|192):(a+=String.fromCharCode(m>>12|224),a+=String.fromCharCode(m>>6&63|128)),a+=String.fromCharCode(m&63|128));h=a;a=h.length;t=a+8;m=16*((t-t%64)/64+1);k=Array(m-1);for(i=w=0;i<a;)t=
++(i-i%4)/4,w=8*(i%4),k[t]|=h.charCodeAt(i)<<w,i++;t=(i-i%4)/4;k[t]|=128<<8*(i%4);k[m-2]=a<<3;k[m-1]=a>>>29;h=k;i=1732584193;g=4023233417;f=2562383102;j=271733878;for(a=0;a<h.length;a+=16)t=i,m=g,k=f,w=j,i=c(i,g,f,j,h[a+0],7,3614090360),j=c(j,i,g,f,h[a+1],12,3905402710),f=c(f,j,i,g,h[a+2],17,606105819),g=c(g,f,j,i,h[a+3],22,3250441966),i=c(i,g,f,j,h[a+4],7,4118548399),j=c(j,i,g,f,h[a+5],12,1200080426),f=c(f,j,i,g,h[a+6],17,2821735955),g=c(g,f,j,i,h[a+7],22,4249261313),i=c(i,g,f,j,h[a+8],7,1770035416),
++j=c(j,i,g,f,h[a+9],12,2336552879),f=c(f,j,i,g,h[a+10],17,4294925233),g=c(g,f,j,i,h[a+11],22,2304563134),i=c(i,g,f,j,h[a+12],7,1804603682),j=c(j,i,g,f,h[a+13],12,4254626195),f=c(f,j,i,g,h[a+14],17,2792965006),g=c(g,f,j,i,h[a+15],22,1236535329),i=d(i,g,f,j,h[a+1],5,4129170786),j=d(j,i,g,f,h[a+6],9,3225465664),f=d(f,j,i,g,h[a+11],14,643717713),g=d(g,f,j,i,h[a+0],20,3921069994),i=d(i,g,f,j,h[a+5],5,3593408605),j=d(j,i,g,f,h[a+10],9,38016083),f=d(f,j,i,g,h[a+15],14,3634488961),g=d(g,f,j,i,h[a+4],20,3889429448),
++i=d(i,g,f,j,h[a+9],5,568446438),j=d(j,i,g,f,h[a+14],9,3275163606),f=d(f,j,i,g,h[a+3],14,4107603335),g=d(g,f,j,i,h[a+8],20,1163531501),i=d(i,g,f,j,h[a+13],5,2850285829),j=d(j,i,g,f,h[a+2],9,4243563512),f=d(f,j,i,g,h[a+7],14,1735328473),g=d(g,f,j,i,h[a+12],20,2368359562),i=e(i,g,f,j,h[a+5],4,4294588738),j=e(j,i,g,f,h[a+8],11,2272392833),f=e(f,j,i,g,h[a+11],16,1839030562),g=e(g,f,j,i,h[a+14],23,4259657740),i=e(i,g,f,j,h[a+1],4,2763975236),j=e(j,i,g,f,h[a+4],11,1272893353),f=e(f,j,i,g,h[a+7],16,4139469664),
++g=e(g,f,j,i,h[a+10],23,3200236656),i=e(i,g,f,j,h[a+13],4,681279174),j=e(j,i,g,f,h[a+0],11,3936430074),f=e(f,j,i,g,h[a+3],16,3572445317),g=e(g,f,j,i,h[a+6],23,76029189),i=e(i,g,f,j,h[a+9],4,3654602809),j=e(j,i,g,f,h[a+12],11,3873151461),f=e(f,j,i,g,h[a+15],16,530742520),g=e(g,f,j,i,h[a+2],23,3299628645),i=l(i,g,f,j,h[a+0],6,4096336452),j=l(j,i,g,f,h[a+7],10,1126891415),f=l(f,j,i,g,h[a+14],15,2878612391),g=l(g,f,j,i,h[a+5],21,4237533241),i=l(i,g,f,j,h[a+12],6,1700485571),j=l(j,i,g,f,h[a+3],10,2399980690),
++f=l(f,j,i,g,h[a+10],15,4293915773),g=l(g,f,j,i,h[a+1],21,2240044497),i=l(i,g,f,j,h[a+8],6,1873313359),j=l(j,i,g,f,h[a+15],10,4264355552),f=l(f,j,i,g,h[a+6],15,2734768916),g=l(g,f,j,i,h[a+13],21,1309151649),i=l(i,g,f,j,h[a+4],6,4149444226),j=l(j,i,g,f,h[a+11],10,3174756917),f=l(f,j,i,g,h[a+2],15,718787259),g=l(g,f,j,i,h[a+9],21,3951481745),i=b(i,t),g=b(g,m),f=b(f,k),j=b(j,w);return(n(i)+n(g)+n(f)+n(j)).toLowerCase()};(function(a){a.fn.stupidtable=function(){a(this).on("click","thead th",function(){a(this).stupidsort()})};a.fn.stupidsort=function(){function b(b){var c=0,d;a(b).children("td,th").each(function(){if(c==t)return d=a(this),!1;var b=a(this).attr("colspan");c+=b?Number(b):1});b="undefined"!=typeof d.data("sort-value")?d.data("sort-value"):"undefined"!=typeof d.attr("data-sort-value")?d.attr("data-sort-value"):d.text();switch(n){case "string":case "string-ins":b=String(b).toLowerCase();break;case "int":b=
++parseInt(Number(b));break;case "float":b=Number(b)}return b}var c=a(this),d=c.closest("table"),e=d.children("tbody"),l=e.children("tr"),n=c.attr("data-sort-type");if(n){var h=!0;c.hasClass("sorting-asc")&&(h=!1);var t=0;c.prevAll().each(function(){var b=a(this).attr("colspan");t+=b?Number(b):1});l.sort(function(a,c){var d=h?1:-1,a=b(a),c=b(c);return a>c?1*d:a<c?-1*d:0});e.append(l);d.find("thead th").removeClass("sorting-asc").removeClass("sorting-desc");c.addClass(h?"sorting-asc":"sorting-desc")}}})(jQuery);$(function(){UI.elements={menu:$("nav > .menu"),main:$("main"),header:$("header"),connection:{status:$("#connection"),user_and_host:$("#user_and_host"),msg:$("#message")}};UI.buildMenu();UI.stored.getOpts();try{if("mistLogin"in sessionStorage){var a=JSON.parse(sessionStorage.mistLogin);mist.user.name=a.name;mist.user.password=a.password;mist.user.host=a.host}}catch(b){}location.hash&&(a=decodeURIComponent(location.hash).substring(1).split("@")[0].split("&"),mist.user.name=a[0],a[1]&&(mist.user.host=
+ a[1]));mist.send(function(){$(window).trigger("hashchange")},{},{timeout:5,hide:!0});var c=0;$("body > div.filler").on("scroll",function(){var a=$(this).scrollLeft();a!=c&&UI.elements.header.css("margin-right",-1*a+"px");c=a})});$(window).on("hashchange",function(){var a=decodeURIComponent(location.hash).substring(1).split("@");a[1]||(a[1]="");a=a[1].split("&");""==a[0]&&(a[0]="Overview");UI.showTab(a[0],a[1])});
+ var MistVideoObject={},otherhost={host:!1,https:!1},UI={debug:!1,elements:{},stored:{getOpts:function(){var a=localStorage.stored;a&&(a=JSON.parse(a));$.extend(!0,this.vars,a);return this.vars},saveOpt:function(a,b){this.vars[a]=b;localStorage.stored=JSON.stringify(this.vars);return this.vars},vars:{helpme:!0}},interval:{clear:function(){"undefined"!=typeof this.opts&&(clearInterval(this.opts.id),delete this.opts)},set:function(a,b){this.opts&&log("[interval]","Set called on interval, but an interval is already active.");
+ this.opts={delay:b,callback:a};this.opts.id=setInterval(a,b)}},returnTab:["Overview"],countrylist:{AF:"Afghanistan",AX:"&Aring;land Islands",AL:"Albania",DZ:"Algeria",AS:"American Samoa",AD:"Andorra",AO:"Angola",AI:"Anguilla",AQ:"Antarctica",AG:"Antigua and Barbuda",AR:"Argentina",AM:"Armenia",AW:"Aruba",AU:"Australia",AT:"Austria",AZ:"Azerbaijan",BS:"Bahamas",BH:"Bahrain",BD:"Bangladesh",BB:"Barbados",BY:"Belarus",BE:"Belgium",BZ:"Belize",BJ:"Benin",BM:"Bermuda",BT:"Bhutan",BO:"Bolivia, Plurinational State of",
+@@ -20,61 +20,61 @@ TV:"Tuvalu",UG:"Uganda",UA:"Ukraine",AE:"United Arab Emirates",GB:"United Kingdo
+ clearTimeout(this.hiding);delete this.hiding;var c=$(document).height()-$tooltip.outerHeight(),d=$(document).width()-$tooltip.outerWidth();$tooltip.css("left",Math.min(a.pageX+10,d-10));$tooltip.css("top",Math.min(a.pageY+25,c-10));$tooltip.show().addClass("show")},hide:function(){$tooltip=this.element;$tooltip.removeClass("show");this.hiding=setTimeout(function(){$tooltip.hide()},500)},element:$("<div>").attr("id","tooltip")},humanMime:function(a){var b=!1;switch(a){case "html5/application/vnd.apple.mpegurl":b=
+ "HLS (TS)";break;case "html5/application/vnd.apple.mpegurl;version=7":b="HLS (CMAF)";break;case "html5/video/webm":b="WebM";break;case "html5/video/mp4":b="MP4";break;case "dash/video/mp4":b="DASH";break;case "flash/11":b="HDS";break;case "flash/10":b="RTMP";break;case "flash/7":b="Progressive";break;case "html5/audio/mp3":b="MP3";break;case "html5/audio/wav":b="WAV";break;case "html5/video/mp2t":case "html5/video/mpeg":b="TS";break;case "html5/application/vnd.ms-sstr+xml":case "html5/application/vnd.ms-ss":b=
+ "Smooth Streaming";break;case "html5/text/vtt":b="VTT Subtitles";break;case "html5/text/plain":b="SRT Subtitles";break;case "html5/text/javascript":b="JSON Subtitles";break;case "rtsp":b="RTSP";break;case "webrtc":b="WebRTC"}return b},popup:{element:null,show:function(a){this.element=$("<div>").attr("id","popup").append($("<button>").text("Close").addClass("close").click(function(){UI.popup.element.fadeOut("fast",function(){UI.popup.element.remove();UI.popup.element=null})})).append(a);$("body").append(this.element)}},
+-menu:[{Overview:{},Protocols:{},Streams:{hiddenmenu:{Edit:{},Preview:{},Embed:{}}},Push:{LTSonly:!0},Triggers:{LTSonly:!1},Logs:{},Statistics:{},"Server Stats":{}},{Disconnect:{classes:["red"]}},{Guides:{link:"http://mistserver.org/documentation#Userdocs"},Tools:{submenu:{"Release notes":{link:"http://mistserver.org/documentation#Devdocs"},"Mist Shop":{link:"http://mistserver.org/products"},"Email for Help":{}}}}],buildMenu:function(){function a(a,b){var c=$("<a>").addClass("button");c.html($("<span>").addClass("plain").text(a)).append($("<span>").addClass("highlighted").text(a));
+-for(var d in b.classes)c.addClass(b.classes[d]);"LTSonly"in b&&c.addClass("LTSonly");"link"in b?c.attr("href",b.link).attr("target","_blank"):"submenu"in b||c.click(function(b){$(this).closest(".menu").hasClass("hide")||(UI.navto(a),b.stopPropagation())});return c}var b=UI.elements.menu,c;for(c in UI.menu){0<c&&b.append($("<br>"));for(var d in UI.menu[c]){var e=UI.menu[c][d],l=a(d,e);b.append(l);if("submenu"in e){var m=$("<span>").addClass("submenu");l.addClass("arrowdown").append(m);for(var f in e.submenu)m.append(a(f,
+-e.submenu[f]))}else if("hiddenmenu"in e)for(f in m=$("<span>").addClass("hiddenmenu"),l.append(m),e.hiddenmenu)m.append(a(f,e.hiddenmenu[f]))}}c=$("<div>").attr("id","ih_button").text("?").click(function(){$("body").toggleClass("helpme");UI.stored.saveOpt("helpme",$("body").hasClass("helpme"))}).attr("title","Click to toggle the display of integrated help");UI.stored.getOpts().helpme&&$("body").addClass("helpme");b.after(c).after($("<div>").addClass("separator"))},findInput:function(a){return this.findInOutput("inputs",
+-a)},findOutput:function(a){return this.findInOutput("connectors",a)},findInOutput:function(a,b){if("capabilities"in mist.data){var c=!1,d=mist.data.capabilities[a];b in d&&(c=d[b]);b+".exe"in d&&(c=d[b+".exe"]);return c}throw"Request capabilities first";},buildUI:function(a){var b=$("<div>").addClass("input_container"),c;for(c in a){var d=a[c];if(d instanceof jQuery)b.append(d);else if("help"==d.type){var e=$("<span>").addClass("text_container").append($("<span>").addClass("description").append(d.help));
+-b.append(e);if("classes"in d)for(var l in d.classes)e.addClass(d.classes[l])}else if("text"==d.type)b.append($("<span>").addClass("text_container").append($("<span>").addClass("text").append(d.text)));else if("custom"==d.type)b.append(d.custom);else if("buttons"==d.type)for(l in e=$("<span>").addClass("button_container").on("keydown",function(a){a.stopPropagation()}),"css"in d&&e.css(d.css),b.append(e),d.buttons){var m=d.buttons[l],f=$("<button>").text(m.label).data("opts",m);"css"in m&&f.css(m.css);
+-if("classes"in m)for(var t in m.classes)f.addClass(m.classes[t]);e.append(f);switch(m.type){case "cancel":f.addClass("cancel").click(m["function"]);break;case "save":f.addClass("save").click(function(){var a=$(this).data("opts").preSave;a&&a.call(this);var b=$(this).closest(".input_container"),c=!1;b.find('.hasValidate:visible, input[type="hidden"].hasValidate').each(function(){if(c=$(this).data("validate")(this,!0))return!1});(a=$(this).data("opts").failedValidate)&&a.call(this);c||(b.find('.isSetting:visible, input[type="hidden"].isSetting').each(function(){var a=
+-$(this).getval(),b=$(this).data("pointer");if(""===a)if("default"in $(this).data("opts"))a=$(this).data("opts")["default"];else return b.main[b.index]=null,!0;b.main[b.index]=a}),(a=$(this).data("opts")["function"])&&a(this))});break;default:f.click(m["function"])}}else{m=$("<label>").addClass("UIelement");b.append(m);"css"in d&&m.css(d.css);m.append($("<span>").addClass("label").html("label"in d?d.label+":":""));if("classes"in d)for(t in d.classes)m.addClass(d.classes[t]);f=$("<span>").addClass("field_container");
+-m.append(f);switch(d.type){case "password":e=$("<input>").attr("type","password");break;case "int":e=$("<input>").attr("type","number");"min"in d&&e.attr("min",d.min);"max"in d&&e.attr("max",d.max);"step"in d&&e.attr("step",d.step);"validate"in d?d.validate.push("int"):d.validate=["int"];break;case "span":e=$("<span>");break;case "debug":d.select=[["","Default"],[0,"0 - All debugging messages disabled"],[1,"1 - Messages about failed operations"],[2,"2 - Previous level, and error messages"],[3,"3 - Previous level, and warning messages"],
+-[4,"4 - Previous level, and status messages for development"],[5,"5 - Previous level, and more status messages for development"],[6,"6 - Previous level, and verbose debugging messages"],[7,"7 - Previous level, and very verbose debugging messages"],[8,"8 - Report everything in extreme detail"],[9,"9 - Report everything in insane detail"],[10,"10 - All messages enabled"]];case "select":e=$("<select>");for(l in d.select){var o=$("<option>");"string"==typeof d.select[l]?o.text(d.select[l]):o.val(d.select[l][0]).text(d.select[l][1]);
+-e.append(o)}break;case "textarea":e=$("<textarea>").on("keydown",function(a){a.stopPropagation()});break;case "checkbox":e=$("<input>").attr("type","checkbox");break;case "hidden":e=$("<input>").attr("type","hidden");m.hide();break;case "email":e=$("<input>").attr("type","email").attr("autocomplete","on").attr("required","");break;case "browse":e=$("<input>").attr("type","text");"filetypes"in d&&e.data("filetypes",d.filetypes);break;case "geolimited":case "hostlimited":e=$("<input>").attr("type",
+-"hidden");break;case "radioselect":e=$("<div>").addClass("radioselect");for(c in d.radioselect){var k=$("<input>").attr("type","radio").val(d.radioselect[c][0]).attr("name",d.label);("LTSonly"in d&&!mist.data.LTS||d.readonly)&&k.prop("disabled",!0);o=$("<label>").append(k).append($("<span>").html(d.radioselect[c][1]));e.append(o);if(2<d.radioselect[c].length)for(l in k=$("<select>").change(function(){$(this).parent().find("input[type=radio]:enabled").prop("checked","true")}),o.append(k),("LTSonly"in
+-d&&!mist.data.LTS||d.readonly)&&k.prop("disabled",!0),d.radioselect[c][2])o=$("<option>"),k.append(o),d.radioselect[c][2][l]instanceof Array?o.val(d.radioselect[c][2][l][0]).html(d.radioselect[c][2][l][1]):o.html(d.radioselect[c][2][l])}break;case "checklist":e=$("<div>").addClass("checkcontainer");$controls=$("<div>").addClass("controls");$checklist=$("<div>").addClass("checklist");e.append($checklist);for(c in d.checklist)"string"==typeof d.checklist[c]&&(d.checklist[c]=[d.checklist[c],d.checklist[c]]),
+-$checklist.append($("<label>").text(d.checklist[c][1]).prepend($("<input>").attr("type","checkbox").attr("name",d.checklist[c][0])));break;case "DOMfield":e=d.DOMfield;break;case "unix":e=$("<input>").attr("type","datetime-local").attr("step",1);d.unit=$("<button>").text("Now").click(function(){$(this).closest(".field_container").find(".field").setval((new Date).getTime()/1E3)});break;case "selectinput":e=$("<div>").addClass("selectinput");k=$("<select>");e.append(k);k.data("input",!1);"LTSonly"in
+-d&&!mist.data.LTS&&k.prop("disabled",!0);for(c in d.selectinput)o=$("<option>"),k.append(o),"string"==typeof d.selectinput[c]?o.text(d.selectinput[c]):(o.text(d.selectinput[c][1]),"string"==typeof d.selectinput[c][0]?o.val(d.selectinput[c][0]):(o.val("CUSTOM"),k.data("input")||k.data("input",UI.buildUI([d.selectinput[c][0]]).children())));k.data("input")&&e.append(k.data("input"));k.change(function(){"CUSTOM"==$(this).val()?$(this).data("input").css("display","flex"):$(this).data("input").hide()});
+-k.trigger("change");break;case "inputlist":e=$("<div>").addClass("inputlist");e.data("newitem",function(){var a=$("<input>").attr("type","text").addClass("listitem");("LTSonly"in d&&!mist.data.LTS||d.readonly)&&a.prop("disabled",!0);var b=function(c){$(this).is(":last-child")?""!=$(this).val()?$(this).after(a.clone().keyup(b).val("")):8==c.which&&$(this).prev().focus():""==$(this).val()&&($(this).next().focus(),$(this).remove())};a.keyup(b);return a});e.append(e.data("newitem"));break;case "sublist":e=
+-$("<div>").addClass("sublist");k=$("<div>").addClass("curvals");k.append($("<span>").text("None."));var w=$("<div>").addClass("itemsettings"),h=$("<button>").text("New "+d.itemLabel),g=d.sublist,i=d,j=e,Q=m;e.data("build",function(a,b){for(var c in i.saveas)c in a||delete i.saveas[c];i.saveas=Object.assign(i.saveas,a);c="New";"undefined"!=typeof b&&(c="Edit");c=UI.buildUI([$("<h4>").text(c+" "+i.itemLabel)].concat(g).concat([{label:"Save first",type:"str",classes:["onlyshowhelp"],validate:[function(){return{msg:"Did you want to save this "+
+-i.itemLabel+"?",classes:["red"]}}]},{type:"buttons",buttons:[{label:"Cancel",type:"cancel","function":function(){w.html("");h.show();Q.show()}},{label:"Save "+i.itemLabel,type:"save",preSave:function(){$(this).closest(".input_container").find(".onlyshowhelp").closest("label").hide()},failedValidate:function(){$(this).closest(".input_container").find(".onlyshowhelp").closest("label").show()},"function":function(){var a=j.getval(),c=Object.assign({},i.saveas),d;for(d in c)null===c[d]&&delete c[d];"undefined"==
+-typeof b?a.push(c):a[b]=c;j.setval(a);w.html("");h.show();Q.show()}}]}]));w.html(c);h.hide();Q.hide()});var F=e;h.click(function(){F.data("build")({})});g.unshift({type:"str",label:"Human readable name",placeholder:"none",help:"A convenient name to describe this "+d.itemLabel+". It won't be used by MistServer.",pointer:{main:d.saveas,index:"x-LSP-name"}});e.data("savelist",[]);e.append(k).append(h);b.append(w);break;case "json":e=$("<textarea>").on("keydown",function(a){a.stopPropagation()}).on("keyup change",
+-function(){this.style.height="";this.style.height=(this.scrollHeight?this.scrollHeight+20:14*this.value.split("\n").length+20)+"px"}).css("min-height","3em");k=function(a,b){if(""!=$(b).val()&&null===a)return{msg:"Invalid json",classes:["red"]}};"validate"in d?d.validate.push(k):d.validate=[k];break;default:e=$("<input>").attr("type","text")}e.addClass("field").data("opts",d);"pointer"in d&&e.attr("name",d.pointer.index);f.append(e);if("classes"in d)for(l in d.classes)e.addClass(d.classes[l]);"placeholder"in
+-d&&e.attr("placeholder",d.placeholder);"default"in d&&e.attr("placeholder",d["default"]);"unit"in d&&f.append($("<span>").addClass("unit").html(d.unit));"readonly"in d&&(e.attr("readonly","readonly"),e.click(function(){$(this).select()}));"qrcode"in d&&f.append($("<span>").addClass("unit").html($("<button>").text("QR").on("keydown",function(a){a.stopPropagation()}).click(function(){var a=String($(this).closest(".field_container").find(".field").getval()),b=$("<div>").addClass("qrcode");UI.popup.show($("<span>").addClass("qr_container").append($("<p>").text(a)).append(b));
+-b.qrcode({text:a,size:Math.min(b.width(),b.height())})})));"clipboard"in d&&document.queryCommandSupported("copy")&&f.append($("<span>").addClass("unit").html($("<button>").text("Copy").on("keydown",function(a){a.stopPropagation()}).click(function(){var a=String($(this).closest(".field_container").find(".field").getval()),b=document.createElement("textarea");b.value=a;document.body.appendChild(b);b.select();var c=false;try{c=document.execCommand("copy")}catch(d){}if(c){$(this).text("Copied to clipboard!");
+-document.body.removeChild(b);var g=$(this);setTimeout(function(){g.text("Copy")},5E3)}else{document.body.removeChild(b);alert("Failed to copy:\n"+a)}})));"rows"in d&&e.attr("rows",d.rows);"LTSonly"in d&&!mist.data.LTS&&(f.addClass("LTSonly"),e.prop("disabled",!0));if("dependent"in d)for(c in d.dependent)m.attr("data-dependent-"+c,d.dependent[c]);switch(d.type){case "browse":k=$("<div>").addClass("grouper").append(m);b.append(k);k=$("<button>").text("Browse").on("keydown",function(a){a.stopPropagation()});
+-f.append(k);k.click(function(){function a(b){f.text("Loading..");mist.send(function(a){i.text(a.browse.path[0]);mist.data.LTS&&d.setval(a.browse.path[0]+"/");f.html(h.clone(true).text("..").attr("title","Folder up"));if(a.browse.subdirectories){a.browse.subdirectories.sort();for(var b in a.browse.subdirectories){var e=a.browse.subdirectories[b];f.append(h.clone(true).attr("title",i.text()+m+e).text(e))}}if(a.browse.files){a.browse.files.sort();for(b in a.browse.files){var e=a.browse.files[b],r=i.text()+
+-m+e,e=$("<a>").text(e).addClass("file").attr("title",r);f.append(e);if(j){var k=true,l;for(l in j)if(typeof j[l]!="undefined"&&mist.inputMatch(j[l],r)){k=false;break}k&&e.hide()}e.click(function(){var a=$(this).attr("title");d.setval(a).removeAttr("readonly").css("opacity",1);g.show();c.remove()})}}},{browse:b})}var b=$(this).closest(".grouper"),c=$("<div>").addClass("browse_container"),d=b.find(".field").attr("readonly","readonly").css("opacity",0.5),g=$(this),e=$("<button>").text("Stop browsing").click(function(){g.show();
+-c.remove();d.removeAttr("readonly").css("opacity",1)}),i=$("<span>").addClass("field"),f=$("<div>").addClass("browse_contents"),h=$("<a>").addClass("folder"),j=d.data("filetypes");b.append(c);c.append($("<label>").addClass("UIelement").append($("<span>").addClass("label").text("Current folder:")).append($("<span>").addClass("field_container").append(i).append(e))).append(f);var m="/";mist.data.config.version.indexOf("indows")>-1&&(m="\\");h.click(function(){var b=i.text()+m+$(this).text();a(b)});
+-b=d.getval();e=b.split("://");e.length>1&&(b=e[0]=="file"?e[1]:"");b=b.split(m);b.pop();b=b.join(m);g.hide();a(b)});break;case "geolimited":case "hostlimited":k={field:e};k.blackwhite=$("<select>").append($("<option>").val("-").text("Blacklist")).append($("<option>").val("+").text("Whitelist"));k.values=$("<span>").addClass("limit_value_list");switch(d.type){case "geolimited":k.prototype=$("<select>").append($("<option>").val("").text("[Select a country]"));for(c in UI.countrylist)k.prototype.append($("<option>").val(c).html(UI.countrylist[c]));
+-break;case "hostlimited":k.prototype=$("<input>").attr("type","text").attr("placeholder","type a host")}k.prototype.on("change keyup",function(){$(this).closest(".field_container").data("subUI").blackwhite.trigger("change")});k.blackwhite.change(function(){var a=$(this).closest(".field_container").data("subUI"),b=[],c=false;a.values.children().each(function(){c=$(this).val();c!=""?b.push(c):$(this).remove()});a.values.append(a.prototype.clone(true));b.length>0?a.field.val($(this).val()+b.join(" ")):
+-a.field.val("");a.field.trigger("change")});"LTSonly"in d&&!mist.data.LTS&&(k.blackwhite.prop("disabled",!0),k.prototype.prop("disabled",!0));k.values.append(k.prototype.clone(!0));f.data("subUI",k).addClass("limit_list").append(k.blackwhite).append(k.values)}"pointer"in d&&(e.data("pointer",d.pointer).addClass("isSetting"),d.pointer.main&&(k=d.pointer.main[d.pointer.index],"undefined"!=k&&e.setval(k)));(""==e.getval()||null==e.getval())&&"value"in d&&e.setval(d.value);if("datalist"in d)for(c in k=
+-"datalist_"+c+MD5(e[0].outerHTML),e.attr("list",k),k=$("<datalist>").attr("id",k),f.append(k),d.datalist)k.append($("<option>").val(d.datalist[c]));f=$("<span>").addClass("help_container");m.append(f);"help"in d&&(f.append($("<span>").addClass("ih_balloon").html(d.help)),e.on("focus mouseover",function(){$(this).closest("label").addClass("active")}).on("blur mouseout",function(){$(this).closest("label").removeClass("active")}));if("validate"in d){m=[];for(l in d.validate){k=d.validate[l];if("function"!=
+-typeof k)switch(k){case "required":k=function(a){return a==""||a==null?{msg:"This is a required field.",classes:["red"]}:false};break;case "int":k=function(a,b){var c=$(b).data("opts");if(!$(b)[0].validity.valid){var d=[];"min"in c&&d.push(" greater than or equal to "+c.min);"max"in c&&d.push(" smaller than or equal to "+c.max);return{msg:"Please enter an integer"+d.join(" and")+".",classes:["red"]}}};break;case "streamname":k=function(a,b){if(a!=""){if(!isNaN(a.charAt(0)))return{msg:"The first character may not be a number.",
+-classes:["red"]};if(a.toLowerCase()!=a)return{msg:"Uppercase letters are not allowed.",classes:["red"]};if(a.replace(/[^\da-z_]/g,"")!=a)return{msg:"Special characters (except for underscores) are not allowed.",classes:["red"]};if("streams"in mist.data&&a in mist.data.streams&&$(b).data("pointer").main.name!=a)return{msg:"This streamname already exists.<br>If you want to edit an existing stream, please click edit on the the streams tab.",classes:["red"]}}};break;case "streamname_with_wildcard":k=
+-function(a){if(a!=""){streampart=a.split("+");var b=streampart.slice(1).join("+");streampart=streampart[0];if(!isNaN(streampart.charAt(0)))return{msg:"The first character may not be a number.",classes:["red"]};if(streampart.toLowerCase()!=streampart)return{msg:"Uppercase letters are not allowed in a stream name.",classes:["red"]};if(streampart.replace(/[^\da-z_]/g,"")!=streampart)return{msg:"Special characters (except for underscores) are not allowed in a stream name.",classes:["red"]};if(streampart!=
+-a&&b.replace(/[\00|\0|\/]/g,"")!=b)return{msg:"Slashes or null bytes are not allowed in wildcards.",classes:["red"]}}};break;case "streamname_with_wildcard_and_variables":k=function(a){if(a!=""){streampart=a.split("+");var b=streampart.slice(1).join("+");streampart=streampart[0];if(!isNaN(streampart.charAt(0)))return{msg:"The first character may not be a number.",classes:["red"]};if(streampart.toLowerCase()!=streampart)return{msg:"Uppercase letters are not allowed in a stream name.",classes:["red"]};
+-if(streampart.replace(/[^\da-z_$]/g,"")!=streampart)return{msg:"Special characters (except for underscores) are not allowed in a stream name.",classes:["red"]};if(streampart!=a&&b.replace(/[\00|\0|\/]/g,"")!=b)return{msg:"Slashes or null bytes are not allowed in wildcards.",classes:["red"]}}};break;default:k=function(){}}m.push(k)}e.data("validate_functions",m).data("help_container",f).data("validate",function(a,b){if($(a).is(":visible")||$(a).is('input[type="hidden"]')){var c=$(a).getval(),d=$(a).data("validate_functions"),
+-g=$(a).data("help_container");g.find(".err_balloon").remove();for(var e in d){var i=d[e](c,a);if(i){$err=$("<span>").addClass("err_balloon").html(i.msg);for(var f in i.classes)$err.addClass(i.classes[f]);g.prepend($err);b&&$(a).focus();return typeof i=="object"&&"break"in i?i["break"]:true}}return false}}).addClass("hasValidate").on("change keyup",function(){$(this).data("validate")($(this))});""!=e.getval()&&e.trigger("change")}"function"in d&&(e.on("change keyup",d["function"]),e.trigger("change"))}}b.on("keydown",
+-function(a){var b=!1;switch(a.which){case 13:b=$(this).find("button.save").first();break;case 27:b=$(this).find("button.cancel").first()}b&&b.length&&(b.trigger("click"),a.stopPropagation())});return b},buildVheaderTable:function(a){var b=$("<table>").css("margin","0.2em"),c=$("<tr>").addClass("header").append($("<td>").addClass("vheader").attr("rowspan",a.labels.length+1).append($("<span>").text(a.vheader))),d=[];c.append($("<td>"));for(var e in a.labels)d.push($("<tr>").append($("<td>").html(""==
+-a.labels[e]?"&nbsp;":a.labels[e]+":")));for(var l in a.content)for(e in c.append($("<td>").html(a.content[l].header)),a.content[l].body)d[e].append($("<td>").html(a.content[l].body[e]));b.append($("<tbody>").append(c).append(d));return b},plot:{addGraph:function(a,b){var c={id:a.id,xaxis:a.xaxis,datasets:[],elements:{cont:$("<div>").addClass("graph"),plot:$("<div>").addClass("plot"),legend:$("<div>").addClass("legend").attr("draggable","true")}};UI.draggable(c.elements.legend);c.elements.cont.append(c.elements.plot).append(c.elements.legend);
++menu:[{Overview:{},General:{},Protocols:{},Streams:{hiddenmenu:{Edit:{},Preview:{},Embed:{}}},Push:{},Triggers:{},Logs:{},Statistics:{},"Server Stats":{}},{Disconnect:{classes:["red"]}},{Guides:{link:"http://mistserver.org/documentation#Userdocs"},Tools:{submenu:{"Release notes":{link:"http://mistserver.org/documentation#Devdocs"},"Mist Shop":{link:"http://mistserver.org/products"},"Email for Help":{}}}}],buildMenu:function(){function a(a,b){var c=$("<a>").addClass("button");c.html($("<span>").addClass("plain").text(a)).append($("<span>").addClass("highlighted").text(a));
++for(var d in b.classes)c.addClass(b.classes[d]);"link"in b?c.attr("href",b.link).attr("target","_blank"):"submenu"in b||c.click(function(b){$(this).closest(".menu").hasClass("hide")||(UI.navto(a),b.stopPropagation())});return c}var b=UI.elements.menu,c;for(c in UI.menu){0<c&&b.append($("<br>"));for(var d in UI.menu[c]){var e=UI.menu[c][d],l=a(d,e);b.append(l);if("submenu"in e){var n=$("<span>").addClass("submenu");l.addClass("arrowdown").append(n);for(var h in e.submenu)n.append(a(h,e.submenu[h]))}else if("hiddenmenu"in
++e)for(h in n=$("<span>").addClass("hiddenmenu"),l.append(n),e.hiddenmenu)n.append(a(h,e.hiddenmenu[h]))}}c=$("<div>").attr("id","ih_button").text("?").click(function(){$("body").toggleClass("helpme");UI.stored.saveOpt("helpme",$("body").hasClass("helpme"))}).attr("title","Click to toggle the display of integrated help");UI.stored.getOpts().helpme&&$("body").addClass("helpme");b.after(c).after($("<div>").addClass("separator"))},findInput:function(a){return this.findInOutput("inputs",a)},findOutput:function(a){return this.findInOutput("connectors",
++a)},findInOutput:function(a,b){if("capabilities"in mist.data){var c=!1,d=mist.data.capabilities[a];b in d&&(c=d[b]);b+".exe"in d&&(c=d[b+".exe"]);return c}throw"Request capabilities first";},buildUI:function(a){var b=$("<div>").addClass("input_container"),c;for(c in a){var d=a[c];if(d instanceof jQuery)b.append(d);else if("help"==d.type){var e=$("<span>").addClass("text_container").append($("<span>").addClass("description").append(d.help));b.append(e);if("classes"in d)for(var l in d.classes)e.addClass(d.classes[l])}else if("text"==
++d.type)b.append($("<span>").addClass("text_container").append($("<span>").addClass("text").append(d.text)));else if("custom"==d.type)b.append(d.custom);else if("buttons"==d.type)for(l in e=$("<span>").addClass("button_container").on("keydown",function(a){a.stopPropagation()}),"css"in d&&e.css(d.css),b.append(e),d.buttons){var n=d.buttons[l],h=$("<button>").text(n.label).data("opts",n);"css"in n&&h.css(n.css);if("classes"in n)for(var t in n.classes)h.addClass(n.classes[t]);e.append(h);switch(n.type){case "cancel":h.addClass("cancel").click(n["function"]);
++break;case "save":h.addClass("save").click(function(){var a=$(this).data("opts").preSave;a&&a.call(this);var b=$(this).closest(".input_container"),c=!1;b.find('.hasValidate:visible, input[type="hidden"].hasValidate').each(function(){if(c=$(this).data("validate")(this,!0))return!1});(a=$(this).data("opts").failedValidate)&&a.call(this);c||(b.find('.isSetting:visible, input[type="hidden"].isSetting').each(function(){var a=$(this).getval(),b=$(this).data("pointer");if(""===a)if("default"in $(this).data("opts"))a=
++$(this).data("opts")["default"];else return b.main[b.index]=null,!0;b.main[b.index]=a}),(a=$(this).data("opts")["function"])&&a(this))});break;default:h.click(n["function"])}}else{n=$("<label>").addClass("UIelement");b.append(n);"css"in d&&n.css(d.css);n.append($("<span>").addClass("label").html("label"in d?d.label+":":""));if("classes"in d)for(t in d.classes)n.addClass(d.classes[t]);h=$("<span>").addClass("field_container");n.append(h);switch(d.type){case "password":e=$("<input>").attr("type","password");
++break;case "int":e=$("<input>").attr("type","number");"min"in d&&e.attr("min",d.min);"max"in d&&e.attr("max",d.max);"step"in d&&e.attr("step",d.step);"validate"in d?d.validate.push("int"):d.validate=["int"];break;case "span":e=$("<span>");break;case "debug":d.select=[["","Default"],[0,"0 - All debugging messages disabled"],[1,"1 - Messages about failed operations"],[2,"2 - Previous level, and error messages"],[3,"3 - Previous level, and warning messages"],[4,"4 - Previous level, and status messages for development"],
++[5,"5 - Previous level, and more status messages for development"],[6,"6 - Previous level, and verbose debugging messages"],[7,"7 - Previous level, and very verbose debugging messages"],[8,"8 - Report everything in extreme detail"],[9,"9 - Report everything in insane detail"],[10,"10 - All messages enabled"]];case "select":e=$("<select>");for(l in d.select){var m=$("<option>");"string"==typeof d.select[l]?m.text(d.select[l]):m.val(d.select[l][0]).text(d.select[l][1]);e.append(m)}break;case "textarea":e=
++$("<textarea>").on("keydown",function(a){a.stopPropagation()});break;case "checkbox":e=$("<input>").attr("type","checkbox");break;case "hidden":e=$("<input>").attr("type","hidden");n.hide();break;case "email":e=$("<input>").attr("type","email").attr("autocomplete","on").attr("required","");break;case "browse":e=$("<input>").attr("type","text");"filetypes"in d&&e.data("filetypes",d.filetypes);break;case "geolimited":case "hostlimited":e=$("<input>").attr("type","hidden");break;case "radioselect":e=
++$("<div>").addClass("radioselect");for(c in d.radioselect){var k=$("<input>").attr("type","radio").val(d.radioselect[c][0]).attr("name",d.label);d.readonly&&k.prop("disabled",!0);m=$("<label>").append(k).append($("<span>").html(d.radioselect[c][1]));e.append(m);if(2<d.radioselect[c].length)for(l in k=$("<select>").change(function(){$(this).parent().find("input[type=radio]:enabled").prop("checked","true")}),m.append(k),d.readonly&&k.prop("disabled",!0),d.radioselect[c][2])m=$("<option>"),k.append(m),
++d.radioselect[c][2][l]instanceof Array?m.val(d.radioselect[c][2][l][0]).html(d.radioselect[c][2][l][1]):m.html(d.radioselect[c][2][l])}break;case "checklist":e=$("<div>").addClass("checkcontainer");$controls=$("<div>").addClass("controls");$checklist=$("<div>").addClass("checklist");e.append($checklist);for(c in d.checklist)"string"==typeof d.checklist[c]&&(d.checklist[c]=[d.checklist[c],d.checklist[c]]),$checklist.append($("<label>").text(d.checklist[c][1]).prepend($("<input>").attr("type","checkbox").attr("name",
++d.checklist[c][0])));break;case "DOMfield":e=d.DOMfield;break;case "unix":e=$("<input>").attr("type","datetime-local").attr("step",1);d.unit=$("<button>").text("Now").click(function(){$(this).closest(".field_container").find(".field").setval((new Date).getTime()/1E3)});break;case "selectinput":e=$("<div>").addClass("selectinput");k=$("<select>");e.append(k);k.data("input",!1);for(c in d.selectinput)m=$("<option>"),k.append(m),"string"==typeof d.selectinput[c]?m.text(d.selectinput[c]):(m.text(d.selectinput[c][1]),
++"string"==typeof d.selectinput[c][0]?m.val(d.selectinput[c][0]):(m.val("CUSTOM"),k.data("input")||k.data("input",UI.buildUI([d.selectinput[c][0]]).children())));k.data("input")&&e.append(k.data("input"));k.change(function(){"CUSTOM"==$(this).val()?$(this).data("input").css("display","flex"):$(this).data("input").hide()});k.trigger("change");break;case "inputlist":e=$("<div>").addClass("inputlist");e.data("newitem",function(){var a=$("<input>").attr("type","text").addClass("listitem");d.readonly&&
++a.prop("disabled",!0);var b=function(c){$(this).is(":last-child")?""!=$(this).val()?$(this).after(a.clone().keyup(b).val("")):8==c.which&&$(this).prev().focus():""==$(this).val()&&($(this).next().focus(),$(this).remove())};a.keyup(b);return a});e.append(e.data("newitem"));break;case "sublist":e=$("<div>").addClass("sublist");k=$("<div>").addClass("curvals");k.append($("<span>").text("None."));var w=$("<div>").addClass("itemsettings"),i=$("<button>").text("New "+d.itemLabel),g=d.sublist,f=d,j=e,r=
++n;e.data("build",function(a,b){for(var c in f.saveas)c in a||delete f.saveas[c];f.saveas=Object.assign(f.saveas,a);c="New";"undefined"!=typeof b&&(c="Edit");c=UI.buildUI([$("<h4>").text(c+" "+f.itemLabel)].concat(g).concat([{label:"Save first",type:"str",classes:["onlyshowhelp"],validate:[function(){return{msg:"Did you want to save this "+f.itemLabel+"?",classes:["red"]}}]},{type:"buttons",buttons:[{label:"Cancel",type:"cancel","function":function(){w.html("");i.show();r.show()}},{label:"Save "+f.itemLabel,
++type:"save",preSave:function(){$(this).closest(".input_container").find(".onlyshowhelp").closest("label").hide()},failedValidate:function(){$(this).closest(".input_container").find(".onlyshowhelp").closest("label").show()},"function":function(){var a=j.getval(),c=Object.assign({},f.saveas),d;for(d in c)null===c[d]&&delete c[d];"undefined"==typeof b?a.push(c):a[b]=c;j.setval(a);w.html("");i.show();r.show()}}]}]));w.html(c);i.hide();r.hide()});var F=e;i.click(function(){F.data("build")({})});g.unshift({type:"str",
++label:"Human readable name",placeholder:"none",help:"A convenient name to describe this "+d.itemLabel+". It won't be used by MistServer.",pointer:{main:d.saveas,index:"x-LSP-name"}});e.data("savelist",[]);e.append(k).append(i);b.append(w);break;case "json":e=$("<textarea>").on("keydown",function(a){a.stopPropagation()}).on("keyup change",function(){this.style.height="";this.style.height=(this.scrollHeight?this.scrollHeight+20:14*this.value.split("\n").length+20)+"px"}).css("min-height","3em");k=function(a,
++b){if(""!=$(b).val()&&null===a)return{msg:"Invalid json",classes:["red"]}};"validate"in d?d.validate.push(k):d.validate=[k];break;case "bitmask":e=$("<div>").addClass("bitmask");for(c in d.bitmask)e.append($("<label>").append($("<input>").attr("type","checkbox").attr("name","bitmask_"+("pointer"in d?d.pointer.index:"")).attr("value",d.bitmask[c][0]).addClass("field")).append($("<span>").text(d.bitmask[c][1])));n.attr("for","none");break;default:e=$("<input>").attr("type","text")}e.addClass("field").data("opts",
++d);"pointer"in d&&e.attr("name",d.pointer.index);h.append(e);if("classes"in d)for(l in d.classes)e.addClass(d.classes[l]);"placeholder"in d&&e.attr("placeholder",d.placeholder);"default"in d&&e.attr("placeholder",d["default"]);"unit"in d&&h.append($("<span>").addClass("unit").html(d.unit));"readonly"in d&&(e.attr("readonly","readonly"),e.click(function(){$(this).select()}));"qrcode"in d&&h.append($("<span>").addClass("unit").html($("<button>").text("QR").on("keydown",function(a){a.stopPropagation()}).click(function(){var a=
++String($(this).closest(".field_container").find(".field").getval()),b=$("<div>").addClass("qrcode");UI.popup.show($("<span>").addClass("qr_container").append($("<p>").text(a)).append(b));b.qrcode({text:a,size:Math.min(b.width(),b.height())})})));"clipboard"in d&&document.queryCommandSupported("copy")&&h.append($("<span>").addClass("unit").html($("<button>").text("Copy").on("keydown",function(a){a.stopPropagation()}).click(function(){var a=String($(this).closest(".field_container").find(".field").getval()),
++b=document.createElement("textarea");b.value=a;document.body.appendChild(b);b.select();var c=false;try{c=document.execCommand("copy")}catch(d){}if(c){$(this).text("Copied to clipboard!");document.body.removeChild(b);var g=$(this);setTimeout(function(){g.text("Copy")},5E3)}else{document.body.removeChild(b);alert("Failed to copy:\n"+a)}})));"rows"in d&&e.attr("rows",d.rows);if("dependent"in d)for(c in d.dependent)n.attr("data-dependent-"+c,d.dependent[c]);switch(d.type){case "browse":k=$("<div>").addClass("grouper").append(n);
++b.append(k);k=$("<button>").text("Browse").on("keydown",function(a){a.stopPropagation()});h.append(k);k.click(function(){function a(b){h.text("Loading..");mist.send(function(a){e.text(a.browse.path[0]);mist.data.LTS&&d.setval(a.browse.path[0]+"/");h.html(i.clone(true).text("..").attr("title","Folder up"));if(a.browse.subdirectories){a.browse.subdirectories.sort();for(var b in a.browse.subdirectories){var f=a.browse.subdirectories[b];h.append(i.clone(true).attr("title",e.text()+n+f).text(f))}}if(a.browse.files){a.browse.files.sort();
++for(b in a.browse.files){var f=a.browse.files[b],k=e.text()+n+f,f=$("<a>").text(f).addClass("file").attr("title",k);h.append(f);if(j){var l=true,m;for(m in j)if(typeof j[m]!="undefined"&&mist.inputMatch(j[m],k)){l=false;break}l&&f.hide()}f.click(function(){var a=$(this).attr("title");d.setval(a).removeAttr("readonly").css("opacity",1);g.show();c.remove()})}}},{browse:b})}var b=$(this).closest(".grouper"),c=$("<div>").addClass("browse_container"),d=b.find(".field").attr("readonly","readonly").css("opacity",
++0.5),g=$(this),f=$("<button>").text("Stop browsing").click(function(){g.show();c.remove();d.removeAttr("readonly").css("opacity",1)}),e=$("<span>").addClass("field"),h=$("<div>").addClass("browse_contents"),i=$("<a>").addClass("folder"),j=d.data("filetypes");b.append(c);c.append($("<label>").addClass("UIelement").append($("<span>").addClass("label").text("Current folder:")).append($("<span>").addClass("field_container").append(e).append(f))).append(h);var n="/";mist.data.config.version.indexOf("indows")>
++-1&&(n="\\");i.click(function(){var b=e.text()+n+$(this).text();a(b)});b=d.getval();f=b.split("://");f.length>1&&(b=f[0]=="file"?f[1]:"");b=b.split(n);b.pop();b=b.join(n);g.hide();a(b)});break;case "geolimited":case "hostlimited":k={field:e};k.blackwhite=$("<select>").append($("<option>").val("-").text("Blacklist")).append($("<option>").val("+").text("Whitelist"));k.values=$("<span>").addClass("limit_value_list");switch(d.type){case "geolimited":k.prototype=$("<select>").append($("<option>").val("").text("[Select a country]"));
++for(c in UI.countrylist)k.prototype.append($("<option>").val(c).html(UI.countrylist[c]));break;case "hostlimited":k.prototype=$("<input>").attr("type","text").attr("placeholder","type a host")}k.prototype.on("change keyup",function(){$(this).closest(".field_container").data("subUI").blackwhite.trigger("change")});k.blackwhite.change(function(){var a=$(this).closest(".field_container").data("subUI"),b=[],c=false;a.values.children().each(function(){c=$(this).val();c!=""?b.push(c):$(this).remove()});
++a.values.append(a.prototype.clone(true));b.length>0?a.field.val($(this).val()+b.join(" ")):a.field.val("");a.field.trigger("change")});k.values.append(k.prototype.clone(!0));h.data("subUI",k).addClass("limit_list").append(k.blackwhite).append(k.values)}"pointer"in d&&(e.data("pointer",d.pointer).addClass("isSetting"),d.pointer.main&&(k=d.pointer.main[d.pointer.index],"undefined"!=k&&e.setval(k)));(""==e.getval()||null==e.getval())&&"value"in d&&e.setval(d.value);if("datalist"in d)for(c in k="datalist_"+
++c+MD5(e[0].outerHTML),e.attr("list",k),k=$("<datalist>").attr("id",k),h.append(k),d.datalist)k.append($("<option>").val(d.datalist[c]));h=$("<span>").addClass("help_container");n.append(h);"help"in d&&(h.append($("<span>").addClass("ih_balloon").html(d.help)),e.on("focus mouseover",function(){$(this).closest("label").addClass("active")}).on("blur mouseout",function(){$(this).closest("label").removeClass("active")}));if("validate"in d){n=[];for(l in d.validate){k=d.validate[l];if("function"!=typeof k)switch(k){case "required":k=
++function(a){return a==""||a==null?{msg:"This is a required field.",classes:["red"]}:false};break;case "int":k=function(a,b){var c=$(b).data("opts");if(!$(b)[0].validity.valid){var d=[];"min"in c&&d.push(" greater than or equal to "+c.min);"max"in c&&d.push(" smaller than or equal to "+c.max);return{msg:"Please enter an integer"+d.join(" and")+".",classes:["red"]}}};break;case "streamname":k=function(a,b){if(a!=""){if(!isNaN(a.charAt(0)))return{msg:"The first character may not be a number.",classes:["red"]};
++if(a.toLowerCase()!=a)return{msg:"Uppercase letters are not allowed.",classes:["red"]};if(a.replace(/[^\da-z_]/g,"")!=a)return{msg:"Special characters (except for underscores) are not allowed.",classes:["red"]};if("streams"in mist.data&&a in mist.data.streams&&$(b).data("pointer").main.name!=a)return{msg:"This streamname already exists.<br>If you want to edit an existing stream, please click edit on the the streams tab.",classes:["red"]}}};break;case "streamname_with_wildcard":k=function(a){if(a!=
++""){streampart=a.split("+");var b=streampart.slice(1).join("+");streampart=streampart[0];if(!isNaN(streampart.charAt(0)))return{msg:"The first character may not be a number.",classes:["red"]};if(streampart.toLowerCase()!=streampart)return{msg:"Uppercase letters are not allowed in a stream name.",classes:["red"]};if(streampart.replace(/[^\da-z_]/g,"")!=streampart)return{msg:"Special characters (except for underscores) are not allowed in a stream name.",classes:["red"]};if(streampart!=a&&b.replace(/[\00|\0|\/]/g,
++"")!=b)return{msg:"Slashes or null bytes are not allowed in wildcards.",classes:["red"]}}};break;case "streamname_with_wildcard_and_variables":k=function(a){if(a!=""){streampart=a.split("+");var b=streampart.slice(1).join("+");streampart=streampart[0];if(!isNaN(streampart.charAt(0)))return{msg:"The first character may not be a number.",classes:["red"]};if(streampart.toLowerCase()!=streampart)return{msg:"Uppercase letters are not allowed in a stream name.",classes:["red"]};if(streampart.replace(/[^\da-z_$]/g,
++"")!=streampart)return{msg:"Special characters (except for underscores) are not allowed in a stream name.",classes:["red"]};if(streampart!=a&&b.replace(/[\00|\0|\/]/g,"")!=b)return{msg:"Slashes or null bytes are not allowed in wildcards.",classes:["red"]}}};break;default:k=function(){}}n.push(k)}e.data("validate_functions",n).data("help_container",h).data("validate",function(a,b){if($(a).is(":visible")||$(a).is('input[type="hidden"]')){var c=$(a).getval(),d=$(a).data("validate_functions"),g=$(a).data("help_container");
++g.find(".err_balloon").remove();for(var f in d){var e=d[f](c,a);if(e){$err=$("<span>").addClass("err_balloon").html(e.msg);for(var h in e.classes)$err.addClass(e.classes[h]);g.prepend($err);b&&$(a).focus();return typeof e=="object"&&"break"in e?e["break"]:true}}return false}}).addClass("hasValidate").on("change keyup",function(){$(this).data("validate")($(this))});""!=e.getval()&&e.trigger("change")}"function"in d&&(e.on("change keyup",d["function"]),e.trigger("change"))}}b.on("keydown",function(a){var b=
++!1;switch(a.which){case 13:b=$(this).find("button.save").first();break;case 27:b=$(this).find("button.cancel").first()}b&&b.length&&(b.trigger("click"),a.stopPropagation())});return b},buildVheaderTable:function(a){var b=$("<table>").css("margin","0.2em"),c=$("<tr>").addClass("header").append($("<td>").addClass("vheader").attr("rowspan",a.labels.length+1).append($("<span>").text(a.vheader))),d=[];c.append($("<td>"));for(var e in a.labels)d.push($("<tr>").append($("<td>").html(""==a.labels[e]?"&nbsp;":
++a.labels[e]+":")));for(var l in a.content)for(e in c.append($("<td>").html(a.content[l].header)),a.content[l].body)d[e].append($("<td>").html(a.content[l].body[e]));b.append($("<tbody>").append(c).append(d));return b},plot:{addGraph:function(a,b){var c={id:a.id,xaxis:a.xaxis,datasets:[],elements:{cont:$("<div>").addClass("graph"),plot:$("<div>").addClass("plot"),legend:$("<div>").addClass("legend").attr("draggable","true")}};UI.draggable(c.elements.legend);c.elements.cont.append(c.elements.plot).append(c.elements.legend);
+ b.append(c.elements.cont);return c},go:function(a){if(!(1>Object.keys(a).length)){var b={totals:[],clients:[]},c;for(c in a)for(var d in a[c].datasets){var e=a[c].datasets[d];switch(e.datatype){case "clients":case "upbps":case "downbps":case "perc_lost":case "perc_retrans":switch(e.origin[0]){case "total":b.totals.push({fields:[e.datatype],end:-15});break;case "stream":b.totals.push({fields:[e.datatype],streams:[e.origin[1]],end:-15});break;case "protocol":b.totals.push({fields:[e.datatype],protocols:[e.origin[1]],
+-end:-15})}break;case "cpuload":case "memload":b.capabilities={}}}0==b.totals.length&&delete b.totals;0==b.clients.length&&delete b.clients;mist.send(function(){for(var b in a){var c=a[b];if(1>c.datasets.length){c.elements.plot.html("");c.elements.legend.html("");break}switch(c.xaxis){case "time":var d=[];c.yaxes={};var e=[],o;for(o in c.datasets){var k=c.datasets[o];k.display&&(k.getdata(),k.yaxistype in c.yaxes||(d.push(UI.plot.yaxes[k.yaxistype]),c.yaxes[k.yaxistype]=d.length),k.yaxis=c.yaxes[k.yaxistype],
++end:-15})}break;case "cpuload":case "memload":b.capabilities={}}}0==b.totals.length&&delete b.totals;0==b.clients.length&&delete b.clients;mist.send(function(){for(var b in a){var c=a[b];if(1>c.datasets.length){c.elements.plot.html("");c.elements.legend.html("");break}switch(c.xaxis){case "time":var d=[];c.yaxes={};var e=[],m;for(m in c.datasets){var k=c.datasets[m];k.display&&(k.getdata(),k.yaxistype in c.yaxes||(d.push(UI.plot.yaxes[k.yaxistype]),c.yaxes[k.yaxistype]=d.length),k.yaxis=c.yaxes[k.yaxistype],
+ e.push(k))}d[0]&&(d[0].color=0);c.plot=$.plot(c.elements.plot,e,{legend:{show:!1},xaxis:UI.plot.xaxes[c.xaxis],yaxes:d,grid:{hoverable:!0,borderWidth:{top:0,right:0,bottom:1,left:1},color:"black",backgroundColor:{colors:["rgba(0,0,0,0)","rgba(0,0,0,0.025)"]}},crosshair:{mode:"x"}});d=$("<table>").addClass("legend-list").addClass("nolay").html($("<tr>").html($("<td>").html($("<h3>").text(c.id))).append($("<td>").css("padding-right","2em").css("text-align","right").html($("<span>").addClass("value")).append($("<button>").data("opts",
+ c).text("X").addClass("close").click(function(){var b=$(this).data("opts");if(confirm("Are you sure you want to remove "+b.id+"?")){b.elements.cont.remove();var c=$(".graph_ids option:contains("+b.id+")"),d=c.parent();c.remove();UI.plot.del(b.id);delete a[b.id];d.trigger("change");UI.plot.go(a)}}))));c.elements.legend.html(d);var w=function(a){var b=c.elements.legend.find(".value"),d=1;if(typeof a=="undefined")b.eq(0).html("Latest:");else{var e=c.plot.getXAxes()[0],a=Math.min(e.max,a),a=Math.max(e.min,
+-a);b.eq(0).html(UI.format.time(a/1E3))}for(var f in c.datasets){var h="&nbsp;";if(c.datasets[f].display){var e=UI.plot.yaxes[c.datasets[f].yaxistype].tickFormatter,k=c.datasets[f].data;if(a)for(var l in k){if(k[l][0]==a){h=e(k[l][1]);break}if(k[l][0]>a){if(l!=0){h=k[l];k=k[l-1];h=e(h[1]+(a-h[0])*(k[1]-h[1])/(k[0]-h[0]))}break}}else h=e(c.datasets[f].data[c.datasets[f].data.length-1][1])}b.eq(d).html(h);d++}};c.plot.getOptions();for(o in c.datasets)e=$("<input>").attr("type","checkbox").data("index",
+-o).data("graph",c).click(function(){var a=$(this).data("graph");$(this).is(":checked")?a.datasets[$(this).data("index")].display=true:a.datasets[$(this).data("index")].display=false;var b={};b[a.id]=a;UI.plot.go(b)}),c.datasets[o].display&&e.attr("checked","checked"),d.append($("<tr>").html($("<td>").html($("<label>").html(e).append($("<div>").addClass("series-color").css("background-color",c.datasets[o].color)).append(c.datasets[o].label))).append($("<td>").css("padding-right","2em").css("text-align",
+-"right").html($("<span>").addClass("value")).append($("<button>").text("X").addClass("close").data("index",o).data("graph",c).click(function(){var b=$(this).data("index"),c=$(this).data("graph");if(confirm("Are you sure you want to remove "+c.datasets[b].label+" from "+c.id+"?")){c.datasets.splice(b,1);if(c.datasets.length==0){c.elements.cont.remove();var b=$(".graph_ids option:contains("+c.id+")"),d=b.parent();b.remove();d.trigger("change");UI.plot.del(c.id);delete a[c.id];UI.plot.go(a)}else{UI.plot.save(c);
+-b={};b[c.id]=c;UI.plot.go(b)}}}))));w();var h=!1;c.elements.plot.on("plothover",function(a,b,c){if(b.x!=h){w(b.x);h=b.x}if(c){a=$("<span>").append($("<h3>").text(c.series.label).prepend($("<div>").addClass("series-color").css("background-color",c.series.color))).append($("<table>").addClass("nolay").html($("<tr>").html($("<td>").text("Time:")).append($("<td>").html(UI.format.dateTime(c.datapoint[0]/1E3,"long")))).append($("<tr>").html($("<td>").text("Value:")).append($("<td>").html(c.series.yaxis.tickFormatter(c.datapoint[1],
++a);b.eq(0).html(UI.format.time(a/1E3))}for(var h in c.datasets){var i="&nbsp;";if(c.datasets[h].display){var e=UI.plot.yaxes[c.datasets[h].yaxistype].tickFormatter,k=c.datasets[h].data;if(a)for(var l in k){if(k[l][0]==a){i=e(k[l][1]);break}if(k[l][0]>a){if(l!=0){i=k[l];k=k[l-1];i=e(i[1]+(a-i[0])*(k[1]-i[1])/(k[0]-i[0]))}break}}else i=e(c.datasets[h].data[c.datasets[h].data.length-1][1])}b.eq(d).html(i);d++}};c.plot.getOptions();for(m in c.datasets)e=$("<input>").attr("type","checkbox").data("index",
++m).data("graph",c).click(function(){var a=$(this).data("graph");$(this).is(":checked")?a.datasets[$(this).data("index")].display=true:a.datasets[$(this).data("index")].display=false;var b={};b[a.id]=a;UI.plot.go(b)}),c.datasets[m].display&&e.attr("checked","checked"),d.append($("<tr>").html($("<td>").html($("<label>").html(e).append($("<div>").addClass("series-color").css("background-color",c.datasets[m].color)).append(c.datasets[m].label))).append($("<td>").css("padding-right","2em").css("text-align",
++"right").html($("<span>").addClass("value")).append($("<button>").text("X").addClass("close").data("index",m).data("graph",c).click(function(){var b=$(this).data("index"),c=$(this).data("graph");if(confirm("Are you sure you want to remove "+c.datasets[b].label+" from "+c.id+"?")){c.datasets.splice(b,1);if(c.datasets.length==0){c.elements.cont.remove();var b=$(".graph_ids option:contains("+c.id+")"),d=b.parent();b.remove();d.trigger("change");UI.plot.del(c.id);delete a[c.id];UI.plot.go(a)}else{UI.plot.save(c);
++b={};b[c.id]=c;UI.plot.go(b)}}}))));w();var i=!1;c.elements.plot.on("plothover",function(a,b,c){if(b.x!=i){w(b.x);i=b.x}if(c){a=$("<span>").append($("<h3>").text(c.series.label).prepend($("<div>").addClass("series-color").css("background-color",c.series.color))).append($("<table>").addClass("nolay").html($("<tr>").html($("<td>").text("Time:")).append($("<td>").html(UI.format.dateTime(c.datapoint[0]/1E3,"long")))).append($("<tr>").html($("<td>").text("Value:")).append($("<td>").html(c.series.yaxis.tickFormatter(c.datapoint[1],
+ c.series.yaxis)))));UI.tooltip.show(b,a.children())}else UI.tooltip.hide()}).on("mouseout",function(){w()})}}},b)}},save:function(a){var b={id:a.id,xaxis:a.xaxis,datasets:[]},c;for(c in a.datasets)b.datasets.push({origin:a.datasets[c].origin,datatype:a.datasets[c].datatype});a=mist.stored.get().graphs||{};a[b.id]=b;mist.stored.set("graphs",a)},del:function(a){var b=mist.stored.get().graphs||{};delete b[a];mist.stored.set("graphs",b)},datatype:{getOptions:function(a){var b=$.extend(!0,{},UI.plot.datatype.templates.general),
+ c=$.extend(!0,{},UI.plot.datatype.templates[a.datatype]),a=$.extend(!0,c,a),a=$.extend(!0,b,a);switch(a.origin[0]){case "total":switch(a.datatype){case "cpuload":case "memload":break;default:a.label+=" (total)"}break;case "stream":case "protocol":a.label+=" ("+a.origin[1]+")"}var b=[],d;for(d in a.basecolor)c=a.basecolor[d],c+=50*(0.5-Math.random()),c=Math.round(c),c=Math.min(255,Math.max(0,c)),b.push(c);a.color="rgb("+b.join(",")+")";return a},templates:{general:{display:!0,datatype:"general",label:"",
+ yaxistype:"amount",data:[],lines:{show:!0},points:{show:!1},getdata:function(){var a=mist.data.totals["stream"==this.origin[0]?this.origin[1]:"all_streams"]["protocol"==this.origin[0]?this.origin[1]:"all_protocols"][this.datatype];return this.data=a}},cpuload:{label:"CPU use",yaxistype:"percentage",basecolor:[237,194,64],cores:1,getdata:function(){var a=!1,b;for(b in this.data)this.data[b][0]<1E3*(mist.data.config.time-600)&&(a=b);!1!==a&&this.data.splice(0,Number(a)+1);this.data.push([1E3*mist.data.config.time,
+ mist.data.capabilities.cpu_use/10]);return this.data}},memload:{label:"Memory load",yaxistype:"percentage",basecolor:[175,216,248],getdata:function(){var a=!1,b;for(b in this.data)this.data[b][0]<1E3*(mist.data.config.time-600)&&(a=b);!1!==a&&this.data.splice(0,Number(a)+1);this.data.push([1E3*mist.data.config.time,mist.data.capabilities.load.memory]);return this.data}},clients:{label:"Connections",basecolor:[203,75,75]},upbps:{label:"Bandwidth up",yaxistype:"bytespersec",basecolor:[77,167,77]},downbps:{label:"Bandwidth down",
+ yaxistype:"bytespersec",basecolor:[148,64,237]},perc_lost:{label:"Lost packages",yaxistype:"percentage",basecolor:[255,33,234]},perc_retrans:{label:"Re-transmitted packages",yaxistype:"percentage",basecolor:[0,0,255]}}},yaxes:{percentage:{name:"percentage",color:"black",tickColor:0,tickDecimals:0,tickFormatter:function(a){return UI.format.addUnit(UI.format.number(a),"%")},tickLength:0,min:0,max:100},amount:{name:"amount",color:"black",tickColor:0,tickDecimals:0,tickFormatter:function(a){return UI.format.number(a)},
+-tickLength:0,min:0},bytespersec:{name:"bytespersec",color:"black",tickColor:0,tickDecimals:1,tickFormatter:function(a){return UI.format.bytes(a,!0)},tickLength:0,ticks:function(a){var b=0.3*Math.sqrt($(".graph").first().height()),b=(a.max-a.min)/b,c=Math.floor(Math.log(Math.abs(b))/Math.log(1024)),d=b/Math.pow(1024,c),e=-Math.floor(Math.log(d)/Math.LN10),l=a.tickDecimals;null!=l&&e>l&&(e=l);var m=Math.pow(10,-e),d=d/m,f;if(1.5>d)f=1;else if(3>d){if(f=2,2.25<d&&(null==l||e+1<=l))f=2.5,++e}else f=7.5>
+-d?5:10;f=f*m*Math.pow(1024,c);null!=a.minTickSize&&f<a.minTickSize&&(f=a.minTickSize);a.delta=b;a.tickDecimals=Math.max(0,null!=l?l:e);a.tickSize=f;b=[];c=a.tickSize*Math.floor(a.min/a.tickSize);e=0;l=Number.NaN;do m=l,l=c+e*a.tickSize,b.push(l),++e;while(l<a.max&&l!=m);return b},min:0}},xaxes:{time:{name:"time",mode:"time",timezone:"browser",ticks:5}}},draggable:function(a){a.attr("draggable",!0);a.on("dragstart",function(a){$(this).css("opacity",0.4).data("dragstart",{click:{x:a.originalEvent.pageX,
++tickLength:0,min:0},bytespersec:{name:"bytespersec",color:"black",tickColor:0,tickDecimals:1,tickFormatter:function(a){return UI.format.bytes(a,!0)},tickLength:0,ticks:function(a){var b=0.3*Math.sqrt($(".graph").first().height()),b=(a.max-a.min)/b,c=Math.floor(Math.log(Math.abs(b))/Math.log(1024)),d=b/Math.pow(1024,c),e=-Math.floor(Math.log(d)/Math.LN10),l=a.tickDecimals;null!=l&&e>l&&(e=l);var n=Math.pow(10,-e),d=d/n,h;if(1.5>d)h=1;else if(3>d){if(h=2,2.25<d&&(null==l||e+1<=l))h=2.5,++e}else h=7.5>
++d?5:10;h=h*n*Math.pow(1024,c);null!=a.minTickSize&&h<a.minTickSize&&(h=a.minTickSize);a.delta=b;a.tickDecimals=Math.max(0,null!=l?l:e);a.tickSize=h;b=[];c=a.tickSize*Math.floor(a.min/a.tickSize);e=0;l=Number.NaN;do n=l,l=c+e*a.tickSize,b.push(l),++e;while(l<a.max&&l!=n);return b},min:0}},xaxes:{time:{name:"time",mode:"time",timezone:"browser",ticks:5}}},draggable:function(a){a.attr("draggable",!0);a.on("dragstart",function(a){$(this).css("opacity",0.4).data("dragstart",{click:{x:a.originalEvent.pageX,
+ y:a.originalEvent.pageY},ele:{x:this.offsetLeft,y:this.offsetTop}})}).on("dragend",function(a){var c=$(this).data("dragstart"),d=c.ele.x-c.click.x+a.originalEvent.pageX,a=c.ele.y-c.click.y+a.originalEvent.pageY;$(this).css({opacity:1,top:a,left:d,right:"auto",bottom:"auto"})});a.parent().on("dragleave",function(){})},format:{time:function(a,b){var c=new Date(1E3*a),d=[];d.push(("0"+c.getHours()).slice(-2));d.push(("0"+c.getMinutes()).slice(-2));"short"!=b&&d.push(("0"+c.getSeconds()).slice(-2));return d.join(":")},
+ date:function(a,b){var c=new Date(1E3*a),d="Sun Mon Tue Wed Thu Fri Sat".split(" "),e=[];"long"==b&&e.push(d[c.getDay()]);e.push(("0"+c.getDate()).slice(-2));e.push("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ")[c.getMonth()]);"short"!=b&&e.push(c.getFullYear());return e.join(" ")},dateTime:function(a,b){return UI.format.date(a,b)+", "+UI.format.time(a,b)},duration:function(a){var b=[0.001,1E3,60,60,24,7,52,1E9],c="ms sec min hr day week year".split(" "),d={},e;for(e in c){var a=a/b[e],
+-l=Math.round(a%b[Number(e)+1]);d[c[e]]=l;a-=l}var m;for(e=c.length-1;0<=e;e--)if(0<d[c[e]]){m=c[e];break}b=$("<span>");switch(m){case "year":b.append(UI.format.addUnit(d.year,"years, ")).append(UI.format.addUnit(d.week,"wks"));break;case "week":b.append(UI.format.addUnit(d.week,"wks, ")).append(UI.format.addUnit(d.day,"days"));break;case "day":b.append(UI.format.addUnit(d.day,"days, ")).append(UI.format.addUnit(d.hr,"hrs"));break;default:b.append([("0"+d.hr).slice(-2),("0"+d.min).slice(-2),("0"+d.sec).slice(-2)+
++l=Math.round(a%b[Number(e)+1]);d[c[e]]=l;a-=l}var n;for(e=c.length-1;0<=e;e--)if(0<d[c[e]]){n=c[e];break}b=$("<span>");switch(n){case "year":b.append(UI.format.addUnit(d.year,"years, ")).append(UI.format.addUnit(d.week,"wks"));break;case "week":b.append(UI.format.addUnit(d.week,"wks, ")).append(UI.format.addUnit(d.day,"days"));break;case "day":b.append(UI.format.addUnit(d.day,"days, ")).append(UI.format.addUnit(d.hr,"hrs"));break;default:b.append([("0"+d.hr).slice(-2),("0"+d.min).slice(-2),("0"+d.sec).slice(-2)+
+ (d.ms?"."+d.ms:"")].join(":"))}return b[0].innerHTML},number:function(a){if(isNaN(Number(a))||0==a)return a;var b=Math.pow(10,3-Math.floor(Math.log(a)/Math.LN10)-1),a=Math.round(a*b)/b;if(1E4<=a){number=a.toString().split(".");for(a=/(\d+)(\d{3})/;a.test(number[0]);)number[0]=number[0].replace(a,"$1 $2");a=number.join(".")}return a},status:function(a){var b=$("<span>");if("undefined"==typeof a.online)return b.text("Unknown, checking.."),"undefined"!=typeof a.error&&b.text(a.error),b;switch(a.online){case -1:b.text("Enabling");
+ break;case 0:b.text("Unavailable").addClass("red");break;case 1:b.text("Active").addClass("green");break;case 2:b.text("Standby").addClass("orange");break;default:b.text(a.online)}"error"in a&&b.text(a.error);return b},capital:function(a){return a.charAt(0).toUpperCase()+a.substring(1)},addUnit:function(a,b){var c=$("<span>").html(a);c.append($("<span>").addClass("unit").html(b));return c[0].innerHTML},bytes:function(a,b){var c="bytes KiB MiB GiB TiB PiB".split(" ");if(0==a)unit=c[0];else{var d=Math.floor(Math.log(Math.abs(a))/
+ Math.log(1024));0>d?unit=c[0]:(a/=Math.pow(1024,d),unit=c[d])}return UI.format.addUnit(UI.format.number(a),unit+(b?"/s":""))}},navto:function(a,b){var c=location.hash,d=c.split("@");d[0]=[mist.user.name,mist.user.host].join("&");d[1]=[a,b].join("&");"undefined"!=typeof screenlog&&screenlog.navto(d[1]);location.hash=d.join("@");location.hash==c&&$(window).trigger("hashchange")},showTab:function(a,b){var c=UI.elements.main;if(mist.user.loggedin){if(!("ui_settings"in mist.data)){c.html("Loading..");
+@@ -86,165 +86,171 @@ pointer:{main:mist.user,index:"name"}},{label:"Desired password",type:"password"
+ classes:["red"]}:false}],help:"Repeat your desired password.",classes:["match_password"]},{type:"buttons",buttons:[{type:"save",label:"Create new account","function":function(){mist.send(function(){UI.navto("Account created")},{authorize:{new_username:mist.user.name,new_password:mist.user.rawpassword}});mist.user.password=MD5(mist.user.rawpassword);delete mist.user.rawpassword}}]}]));break;case "Account created":UI.elements.menu.addClass("hide");c.append($("<p>").text("Your account has been created succesfully.")).append(UI.buildUI([{type:"text",
+ text:"Would you like to enable all (currently) available protocols with their default settings?"},{type:"buttons",buttons:[{label:"Enable protocols",type:"save","function":function(){if(mist.data.config.protocols)c.append("Unable to enable all protocols as protocol settings already exist.<br>");else{c.append("Retrieving available protocols..<br>");mist.send(function(a){var b=[],d;for(d in a.capabilities.connectors)if(a.capabilities.connectors[d].required)c.append('Could not enable protocol "'+d+'" because it has required settings.<br>');
+ else{b.push({connector:d});c.append('Enabled protocol "'+d+'".<br>')}c.append("Saving protocol settings..<br>");mist.send(function(){c.append("Protocols enabled. Redirecting..");setTimeout(function(){UI.navto("Overview")},5E3)},{config:{protocols:b}})},{capabilities:true})}}},{label:"Skip",type:"cancel","function":function(){UI.navto("Overview")}}]}]));break;case "Overview":if("undefined"==typeof mist.data.bandwidth){mist.send(function(){UI.navto(a)},{bandwidth:!0});c.append("Loading..");return}var e=
+-$("<span>").text("Loading.."),l=$("<span>"),m=$("<span>").addClass("logs"),f=$("<span>"),t=$("<span>"),o=$("<span>").text("Unknown"),k=$("<span>"),w=$("<span>"),h={serverid:mist.data.config.serverid,debug:mist.data.config.debug,accesslog:mist.data.config.accesslog,prometheus:mist.data.config.prometheus,defaultStream:mist.data.config.defaultStream,trustedproxy:mist.data.config.trustedproxy,location:"location"in mist.data.config?mist.data.config.location:{}},g={};"bandwidth"in mist.data&&(g=mist.data.bandwidth,
+-null==g&&(g={}),g.limit||(g.limit=""));var i=$("<select>").html($("<option>").val(1).text("bytes/s")).append($("<option>").val(1024).text("KiB/s")).append($("<option>").val(1048576).text("MiB/s")).append($("<option>").val(1073741824).text("GiB/s")),j=parseURL(mist.user.host),j=j.protocol+j.host+j.port;c.append(UI.buildUI([{type:"help",help:"You can find most basic information about your MistServer here.<br>You can also set the debug level and force a save to the config.json file that MistServer uses to save your settings. "},
+-{type:"span",label:"Version",pointer:{main:mist.data.config,index:"version"}},{type:"span",label:"Version check",value:e,LTSonly:!0},{type:"span",label:"Server time",value:t},{type:"span",label:"Licensed to",value:"license"in mist.data.config?mist.data.config.license.user:"",LTSonly:!0},{type:"span",label:"Active licenses",value:o,LTSonly:!0},{type:"span",label:"Configured streams",value:mist.data.streams?Object.keys(mist.data.streams).length:0},{type:"span",label:"Active streams",value:l},{type:"span",
+-label:"Current connections",value:f},{type:"span",label:"Enabled protocols",value:k},{type:"span",label:"Disabled protocols",value:w},{type:"span",label:"Recent problems",value:m},$("<br>"),{type:"str",label:"Human readable name",pointer:{main:h,index:"serverid"},help:"You can name your MistServer here for personal use. You'll still need to set host name within your network yourself."},{type:"debug",label:"Debug level",pointer:{main:h,index:"debug"},help:"You can set the amount of debug information MistServer saves in the log. A full reboot of MistServer is required before some components of MistServer can post debug information."},
+-{type:"selectinput",label:"Access log",selectinput:[["","Do not track"],["LOG","Log to MistServer log"],[{type:"str",label:"Path",LTSonly:!0},"Log to file"]],pointer:{main:h,index:"accesslog"},help:"Enable access logs.",LTSonly:!0},{type:"selectinput",label:"Prometheus stats output",selectinput:[["","Disabled"],[{type:"str",label:"Passphrase",LTSonly:!0},"Enabled"]],pointer:{main:h,index:"prometheus"},help:"Make stats available in Prometheus format. These can be accessed via "+j+"/PASSPHRASE or "+
+-j+"/PASSPHRASE.json.",LTSonly:!0},{type:"inputlist",label:"Trusted proxies",help:"List of proxy server addresses that are allowed to override the viewer IP address to arbitrary values.<br>You may use a hostname or IP address.",LTSonly:!0,pointer:{main:h,index:"trustedproxy"}},{type:"selectinput",label:"Load balancer bandwidth limit",selectinput:[["","Default (1 gbps)"],[{label:"Custom",type:"int",min:0,unit:i},"Custom"]],pointer:{main:g,index:"limit"},help:"This setting only applies when MistServer is combined with a load balancer. This is the amount of traffic this server is willing to handle.",
+-LTSonly:!0},{type:"inputlist",label:"Load balancer bandwidth exceptions",pointer:{main:g,index:"exceptions"},help:"This setting only applies when MistServer is combined with a load balancer. Data sent to the hosts and subnets listed here will not count towards reported bandwidth usage.<br>Examples:<ul><li>192.168.0.0/16</li><li>localhost</li><li>10.0.0.0/8</li><li>fe80::/16</li></ul>",LTSonly:!0},{type:"int",step:1E-8,label:"Server latitude",pointer:{main:h.location,index:"lat"},help:"This setting is only useful when MistServer is combined with a load balancer. When this is set, the balancer can send users to a server close to them.",
+-LTSonly:!0},{type:"int",step:1E-8,label:"Server longitude",pointer:{main:h.location,index:"lon"},help:"This setting is only useful when MistServer is combined with a load balancer. When this is set, the balancer can send users to a server close to them.",LTSonly:!0},{type:"str",label:"Server location name",pointer:{main:h.location,index:"name"},help:"This setting is only useful when MistServer is combined with a load balancer. This will be displayed as the server's location.",LTSonly:!0},{type:"str",
+-validate:["streamname_with_wildcard_and_variables"],label:"Fallback stream",pointer:{main:h,index:"defaultStream"},help:"When this is set, if someone attempts to view a stream that does not exist, or is offline, they will be redirected to this stream instead. $stream may be used to refer to the original stream name.",LTSonly:!0},{type:"checkbox",label:"Force configurations save",pointer:{main:h,index:"save"},help:"Tick the box in order to force an immediate save to the config.json MistServer uses to save your settings. Saving will otherwise happen upon closing MistServer. Don't forget to press save after ticking the box."},
+-{type:"buttons",buttons:[{type:"save",label:"Save","function":function(){var a={config:h},b={};b.limit=g.limit?i.val()*g.limit:0;b.exceptions=g.exceptions;if(b.exceptions===null)b.exceptions=[];a.bandwidth=b;if(h.save)a.save=h.save;delete h.save;mist.send(function(){UI.navto("Overview")},a)}}]}]));if(mist.data.LTS){var Q=function(a){function b(a){if(a.update){var d="";"progress"in a.update&&(d=" ("+a.update.progress+"%)");e.text("Updating.."+d);c(a.log);setTimeout(function(){mist.send(function(a){b(a)},
+-{update:true})},1E3)}else UI.showTab("Overview")}function c(a){a=a.filter(function(a){return a[1]=="UPDR"});if(a.length){var b=$("<div>");e.append(b);for(var d in a)b.append($("<div>").text(a[d][2]))}}if(!a.update||!("uptodate"in a.update)){e.text("Unknown, checking..");setTimeout(function(){mist.send(function(a){"update"in a&&Q(a)},{checkupdate:true})},5E3)}else if(a.update.error)e.addClass("red").text(a.update.error);else if(a.update.uptodate)e.text("Your version is up to date.").addClass("green");
+-else{if(a.update.progress){e.addClass("orange").removeClass("red").text("Updating..");b(a)}else{e.text("");e.append($("<span>").addClass("red").text("On "+(new Date(a.update.date)).toLocaleDateString()+" version "+a.update.version+" became available."));(!a.update.url||a.update.url.slice(-4)!=".zip")&&e.append($("<button>").text("Rolling update").css({"font-size":"1em","margin-left":"1em"}).click(function(){if(confirm("Are you sure you want to execute a rolling update?")){e.addClass("orange").removeClass("red").text("Rolling update command sent..");
+-mist.send(function(a){b(a)},{autoupdate:true})}}));var d=$("<a>").attr("href",a.update.url).attr("target","_blank").text("Manual download");d[0].protocol="https:";e.append($("<div>").append(d))}c(a.log)}};Q(mist.data);if("license"in mist.data.config){if("active_products"in mist.data.config.license&&Object.keys(mist.data.config.license.active_products).length){var F=$("<table>").css("text-indent","0");o.html(F);F.append($("<tr>").append($("<th>").append("Product")).append($("<th>").append("Updates until")).append($("<th>").append("Use until")).append($("<th>").append("Max. simul. instances")));
+-for(var r in mist.data.config.license.active_products){var J=mist.data.config.license.active_products[r];F.append($("<tr>").append($("<td>").append(J.name)).append($("<td>").append(J.updates_final?J.updates_final:"&infin;")).append($("<td>").append(J.use_final?J.use_final:"&infin;")).append($("<td>").append(J.amount?J.amount:"&infin;")))}}else o.text("None. ");o.append($("<a>").text("More details").attr("href","https://shop.mistserver.org/myinvoices").attr("target","_blank"))}}else e.text("");var za=
+-function(){var a={totals:{fields:["clients"],start:-10},active_streams:true};if(!("cabailities"in mist.data))a.capabilities=true;mist.send(function(){Aa()},a)},Aa=function(){l.text("active_streams"in mist.data?mist.data.active_streams?mist.data.active_streams.length:0:"?");if("totals"in mist.data&&"all_streams"in mist.data.totals)var a=mist.data.totals.all_streams.all_protocols.clients,a=a.length?UI.format.number(a[a.length-1][1]):0;else a="Loading..";f.text(a);t.text(UI.format.dateTime(mist.data.config.time,
+-"long"));m.html("");a=0;"license"in mist.data.config&&"user_msg"in mist.data.config.license&&mist.data.log.unshift([mist.data.config.license.time,"ERROR",mist.data.config.license.user_msg]);for(var b in mist.data.log){var c=mist.data.log[b];if(["FAIL","ERROR"].indexOf(c[1])>-1){a++;var d=$("<span>").addClass("content").addClass("red"),e=c[2].split("|");for(b in e)d.append($("<span>").text(e[b]));m.append($("<div>").append($("<span>").append(UI.format.time(c[0]))).append(d));if(a==5)break}}a==0&&m.html("None.");
+-a=[];c=[];for(b in mist.data.config.protocols){d=mist.data.config.protocols[b];a.indexOf(d.connector)>-1||a.push(d.connector)}k.text(a.length?a.join(", "):"None.");if("capabilities"in mist.data){for(b in mist.data.capabilities.connectors)a.indexOf(b)==-1&&c.push(b);w.text(c.length?c.join(", "):"None.")}else w.text("Loading..")};za();Aa();UI.interval.set(za,3E4);break;case "Protocols":if("undefined"==typeof mist.data.capabilities){mist.send(function(){UI.navto(a)},{capabilities:!0});c.append("Loading..");
+-return}var C=$("<tbody>");c.append(UI.buildUI([{type:"help",help:"You can find an overview of all the protocols and their relevant information here. You can add, edit or delete protocols."}])).append($("<button>").text("Delete all protocols").click(function(){if(confirm("Are you sure you want to delete all currently configured protocols?")){mist.data.config.protocols=[];mist.send(function(){UI.navto("Protocols")},{config:mist.data.config})}})).append($("<button>").text("Enable default protocols").click(function(){var a=
+-Object.keys(mist.data.capabilities.connectors),b;for(b in mist.data.config.protocols){var c=a.indexOf(mist.data.config.protocols[b].connector);c>-1&&a.splice(c,1)}var d=[];for(b in a)(!("required"in mist.data.capabilities.connectors[a[b]])||Object.keys(mist.data.capabilities.connectors[a[b]].required).length==0)&&d.push(a[b]);c="Click OK to enable disabled protocols with their default settings:\n ";c=d.length?c+d.join(", "):c+"None.";if(d.length!=a.length){a=a.filter(function(a){return d.indexOf(a)<
+-0});c=c+("\n\nThe following protocols can only be set manually:\n "+a.join(", "))}if(confirm(c)&&d.length){if(mist.data.config.protocols===null)mist.data.config.protocols=[];for(b in d)mist.data.config.protocols.push({connector:d[b]});mist.send(function(){UI.navto("Protocols")},{config:mist.data.config})}})).append("<br>").append($("<button>").text("New protocol").click(function(){UI.navto("Edit Protocol")}).css("clear","both")).append($("<table>").html($("<thead>").html($("<tr>").html($("<th>").text("Protocol")).append($("<th>").text("Status")).append($("<th>").text("Settings")).append($("<th>")))).append(C));
+-var Ba=function(){function a(b){var c=mist.data.capabilities.connectors[b.connector];if(!c)return"";var d=[],e=["required","optional"],M;for(M in e)for(var g in c[e[M]])b[g]&&b[g]!=""?d.push(g+": "+b[g]):c[e[M]][g]["default"]&&d.push(g+": "+c[e[M]][g]["default"]);return $("<span>").addClass("description").text(d.join(", "))}C.html("");for(var b in mist.data.config.protocols){var c=mist.data.config.protocols[b],d=mist.data.capabilities.connectors[c.connector];C.append($("<tr>").data("index",b).append($("<td>").text(d&&
+-d.friendly?d.friendly:c.connector)).append($("<td>").html(UI.format.status(c))).append($("<td>").html(a(c))).append($("<td>").css("text-align","right").html($("<button>").text("Edit").click(function(){UI.navto("Edit Protocol",$(this).closest("tr").data("index"))})).append($("<button>").text("Delete").click(function(){var a=$(this).closest("tr").data("index");if(confirm('Are you sure you want to delete the protocol "'+mist.data.config.protocols[a].connector+'"?')){mist.send(function(){UI.navto("Protocols")},
+-{deleteprotocol:mist.data.config.protocols[a]});mist.data.config.protocols.splice(a,1)}}))))}};Ba();UI.interval.set(function(){mist.send(function(){Ba()})},1E4);break;case "Edit Protocol":if("undefined"==typeof mist.data.capabilities){mist.send(function(){UI.navto(a,b)},{capabilities:!0});c.append("Loading..");return}var K=!1;""!=b&&0<=b&&(K=!0);var U={};for(r in mist.data.config.protocols)U[mist.data.config.protocols[r].connector]=1;var Ca=function(a){var b=mist.data.capabilities.connectors[a],c=
+-mist.convertBuildOptions(b,p);if(K)var d=$.extend({},p);c.push({type:"hidden",pointer:{main:p,index:"connector"},value:a});c.push({type:"buttons",buttons:[{type:"save",label:"Save","function":function(){var a={};K?a.updateprotocol=[d,p]:a.addprotocol=p;mist.send(function(){UI.navto("Protocols")},a)}},{type:"cancel",label:"Cancel","function":function(){UI.navto("Protocols")}}]});if("deps"in b&&b.deps!=""){F=$("<span>").text("Dependencies:");$ul=$("<ul>");F.append($ul);if(typeof b.deps=="string")b.deps=
+-b.deps.split(", ");for(var e in b.deps){a=$("<li>").text(b.deps[e]+" ");$ul.append(a);typeof U[b.deps[e]]!="undefined"||typeof U[b.deps[e]+".exe"]!="undefined"?a.append($("<span>").addClass("green").text("(Configured)")):a.append($("<span>").addClass("red").text("(Not yet configured)"))}c.unshift({type:"text",text:F[0].innerHTML})}return UI.buildUI(c)},U={};for(r in mist.data.config.protocols)U[mist.data.config.protocols[r].connector]=1;if(K){var s=mist.data.config.protocols[b],p=s;c.find("h2").append(' "'+
+-s.connector+'"');c.append(Ca(s.connector))}else{c.html($("<h2>").text("New Protocol"));var p={},u=[["",""]];for(r in mist.data.capabilities.connectors)u.push([r,mist.data.capabilities.connectors[r].friendly?mist.data.capabilities.connectors[r].friendly:r]);var R=$("<span>");c.append(UI.buildUI([{label:"Protocol",type:"select",select:u,"function":function(){$(this).getval()!=""&&R.html(Ca($(this).getval()))}}])).append(R)}break;case "Streams":if(!("capabilities"in mist.data)){c.html("Loading..");mist.send(function(){UI.navto(a)},
+-{capabilities:!0});return}var Da=$("<button>"),L=$("<span>").text("Loading..");c.append(UI.buildUI([{type:"help",help:"Here you can create, edit or delete new and existing streams. Go to stream preview or embed a video player on your website."},$("<div>").css({width:"45.25em",display:"flex","justify-content":"flex-end"}).append(Da).append($("<button>").text("Create a new stream").click(function(){UI.navto("Edit")}))])).append(L);""==b&&(h=mist.stored.get(),"viewmode"in h&&(b=h.viewmode));Da.text("Switch to "+
+-("thumbnails"==b?"list":"thumbnail")+" view").click(function(){mist.stored.set("viewmode",b=="thumbnails"?"list":"thumbnails");UI.navto("Streams",b=="thumbnails"?"list":"thumbnails")});var A=$.extend(!0,{},mist.data.streams),ka=function(a,b){var c=$.extend({},b);delete c.meta;delete c.error;c.online=2;c.name=a;c.ischild=true;return c},la=function(b,d,e){L.remove();switch(b){case "thumbnails":var g=$("<div>").addClass("preview_icons"),f;f=e||[];d.sort();d.unshift("");L.remove();c.append($("<h2>").text(a)).append(UI.buildUI([{label:"Filter the streams",
+-type:"datalist",datalist:d,pointer:{main:{},index:"stream"},help:"If you type something here, the box below will only show streams with names that contain your text.","function":function(){var a=$(this).val();g.children().each(function(){$(this).hide();$(this).attr("data-stream").indexOf(a)>-1&&$(this).show()})}}]));d.shift();c.append($("<span>").addClass("description").text("Choose a stream below.")).append(g);for(var i in d){var b=d[i],h="",j=$("<button>").text("Delete").click(function(){var a=
+-$(this).closest("div").attr("data-stream");if(confirm('Are you sure you want to delete the stream "'+a+'"?')){delete mist.data.streams[a];var b={};b.deletestream=[a];mist.send(function(){UI.navto("Streams")},b)}}),k=$("<button>").text("Settings").click(function(){UI.navto("Edit",$(this).closest("div").attr("data-stream"))}),e=$("<button>").text("Preview").click(function(){UI.navto("Preview",$(this).closest("div").attr("data-stream"))}),l=$("<button>").text("Embed").click(function(){UI.navto("Embed",
+-$(this).closest("div").attr("data-stream"))}),q=$("<span>").addClass("image");if(b.indexOf("+")>-1){h=b.split("+");h=mist.data.streams[h[0]].source+h[1];k=j="";q.addClass("wildcard")}else{h=mist.data.streams[b].source;if(f.indexOf(b)>-1){l=e="";q.addClass("folder")}}g.append($("<div>").append($("<span>").addClass("streamname").text(b)).append(q).append($("<span>").addClass("description").text(h)).append($("<span>").addClass("button_container").append(k).append(j).append(e).append(l)).attr("title",
+-b).attr("data-stream",b))}break;default:var m=$("<tbody>").append($("<tr>").append("<td>").attr("colspan",6).text("Loading.."));i=$("<table>").html($("<thead>").html($("<tr>").html($("<th>").text("Stream name").attr("data-sort-type","string").addClass("sorting-asc")).append($("<th>").text("Source").attr("data-sort-type","string")).append($("<th>").text("Status").attr("data-sort-type","int")).append($("<th>").css("text-align","right").text("Connections").attr("data-sort-type","int")).append($("<th>")).append($("<th>")))).append(m);
+-c.append(i);i.stupidtable();var n=function(){var a=[],b;for(b in mist.data.active_streams)a.push({streams:[mist.data.active_streams[b]],fields:["clients"],start:-2});mist.send(function(){$.extend(true,A,mist.data.streams);var a=0;m.html("");d.sort();for(var b in d){var c=d[b],e;e=c in mist.data.streams?mist.data.streams[c]:A[c];var g=$("<td>").css("text-align","right").html($("<span>").addClass("description").text("Loading..")),f=0;if(typeof mist.data.totals!="undefined"&&typeof mist.data.totals[c]!=
+-"undefined"){var i=mist.data.totals[c].all_protocols.clients,f=0;if(i.length){for(a in i)f=f+i[a][1];f=Math.round(f/i.length)}}g.html(UI.format.number(f));if(f==0&&e.online==1)e.online=2;f=$("<td>").css("text-align","right").css("white-space","nowrap");(!("ischild"in e)||!e.ischild)&&f.html($("<button>").text("Settings").click(function(){UI.navto("Edit",$(this).closest("tr").data("index"))})).append($("<button>").text("Delete").click(function(){var a=$(this).closest("tr").data("index");if(confirm('Are you sure you want to delete the stream "'+
+-a+'"?')){delete mist.data.streams[a];var b={};mist.data.LTS?b.deletestream=[a]:b.streams=mist.data.streams;mist.send(function(){UI.navto("Streams")},b)}}));i=$("<span>").text(e.name);e.ischild&&i.css("padding-left","1em");var h=UI.format.status(e),M=$("<button>").text("Preview").click(function(){UI.navto("Preview",$(this).closest("tr").data("index"))}),q=$("<button>").text("Embed").click(function(){UI.navto("Embed",$(this).closest("tr").data("index"))});if("filesfound"in A[c]||e.online<0){h.html("");
+-M="";g.html("");q=""}m.append($("<tr>").data("index",c).html($("<td>").html(i).attr("title",e.name=="..."?"The results were truncated":e.name).addClass("overflow_ellipsis")).append($("<td>").text(e.source).attr("title",e.source).addClass("description").addClass("overflow_ellipsis").css("max-width","20em")).append($("<td>").data("sort-value",e.online).html(h)).append(g).append($("<td>").css("white-space","nowrap").html(M).append(q)).append(f));a++}},{totals:a,active_streams:true})};if(mist.data.LTS){var o=
+-0,p=0;for(f in mist.data.streams){i=mist.data.capabilities.inputs.Folder||mist.data.capabilities.inputs["Folder.exe"];if(!i)break;if(mist.inputMatch(i.source_match,mist.data.streams[f].source)){A[f].source=A[f].source+"*";A[f].filesfound=null;mist.send(function(a,b){var c=b.stream,d=0,e;a:for(e in a.browse.files){var f;for(f in mist.data.capabilities.inputs)if(!(f.indexOf("Buffer")>=0||f.indexOf("Buffer.exe")>=0||f.indexOf("Folder")>=0||f.indexOf("Folder.exe")>=0)&&mist.inputMatch(mist.data.capabilities.inputs[f].source_match,
+-"/"+a.browse.files[e])){var g=c+"+"+a.browse.files[e];A[g]=ka(g,mist.data.streams[c]);A[g].source=mist.data.streams[c].source+a.browse.files[e];d++;if(d>=500){A[c+"+zzzzzzzzz"]={ischild:true,name:"...",online:-1};break a}}}"files"in a.browse&&a.browse.files.length?A[c].filesfound=true:mist.data.streams[c].filesfound=false;p++;if(o==p){mist.send(function(){n()},{active_streams:true});UI.interval.set(function(){n()},5E3)}},{browse:mist.data.streams[f].source},{stream:f});o++}}if(o==0){mist.send(function(){n()},
+-{active_streams:true});UI.interval.set(function(){n()},5E3)}}else{mist.send(function(){n()},{active_streams:true});UI.interval.set(function(){n()},5E3)}}};if(mist.data.LTS){var ma=0,Ea=0,u={},Fa=[];for(h in mist.data.streams)if(mist.inputMatch((mist.data.capabilities.inputs.Folder||mist.data.capabilities.inputs["Folder.exe"]).source_match,mist.data.streams[h].source))Fa.push(h),mist.send(function(a,c){var d=c.stream,e=0,f;a:for(f in a.browse.files){var g;for(g in mist.data.capabilities.inputs)if(!(g.indexOf("Buffer")>=
+-0||g.indexOf("Folder")>=0)&&mist.inputMatch(mist.data.capabilities.inputs[g].source_match,"/"+a.browse.files[f])){u[d+"+"+a.browse.files[f]]=true;e++;if(e>=500){u[d+"+zzzzzzzzz"]=true;break a}}}Ea++;ma==Ea&&mist.send(function(){for(var a in mist.data.active_streams){var c=mist.data.active_streams[a].split("+");if(c.length>1&&c[0]in mist.data.streams){u[mist.data.active_streams[a]]=true;A[mist.data.active_streams[a]]=ka(mist.data.active_streams[a],mist.data.streams[c[0]])}}u=Object.keys(u);u=u.concat(Object.keys(mist.data.streams));
+-u.sort();la(b,u,Fa)},{active_streams:true})},{browse:mist.data.streams[h].source},{stream:h}),ma++;0==ma&&mist.send(function(){for(var a in mist.data.active_streams){var c=mist.data.active_streams[a].split("+");if(c.length>1&&c[0]in mist.data.streams){u[mist.data.active_streams[a]]=true;A[mist.data.active_streams[a]]=ka(mist.data.active_streams[a],mist.data.streams[c[0]])}}u=Object.keys(u);mist.data.streams&&(u=u.concat(Object.keys(mist.data.streams)));u.sort();la(b,u)},{active_streams:!0})}else la(b,
+-Object.keys(mist.data.streams));break;case "Edit":if("undefined"==typeof mist.data.capabilities){mist.send(function(){UI.navto(a,b)},{capabilities:!0});c.append("Loading..");return}K=!1;""!=b&&(K=!0);if(K){var Ga=b,p=mist.data.streams[Ga];c.find("h2").append(' "'+Ga+'"')}else c.html($("<h2>").text("New Stream")),p={};var Ha=[];for(r in mist.data.capabilities.inputs)Ha.push(mist.data.capabilities.inputs[r].source_match);var da=$("<div>"),Ia=function(a){var c={};if(!mist.data.streams)mist.data.streams=
+-{};mist.data.streams[p.name]=p;b!=p.name&&delete mist.data.streams[b];c.addstream={};c.addstream[p.name]=p;if(b!=p.name)c.deletestream=[b];if(p.stop_sessions&&b!=""){c.stop_sessions=b;delete p.stop_sessions}mist.send(function(){delete mist.data.streams[p.name].online;delete mist.data.streams[p.name].error;UI.navto(a,a=="Preview"?p.name:"")},c)},Ja=$("<style>").text("button.saveandpreview { display: none; }"),N=$("<span>"),na=function(){var a=c.find("[name=name]").val();if(a){var b=parseURL(mist.user.host),
+-d=c.find("[name=source]").val(),e=d.match(/@.*/);e&&(e=e[0].substring(1));var f=d.replace(/(?:.+?):\/\//,""),f=f.split("/"),f=f[0],f=f.split(":"),f=f[0];(d=d.match(/:\d+/))&&(d=d[0]);var g={},i=["RTMP","RTSP","RTMP.exe","RTSP.exe"],h;for(h in i)i[h]in mist.data.capabilities.connectors&&(g[i[h]]=mist.data.capabilities.connectors[i[h]].optional.port["default"]);var i={RTMP:1935,"RTMP.exe":1935,RTSP:554,"RTSP.exe":554,TS:-1,"TS.exe":-1},j;for(j in g){for(h in mist.data.config.protocols){var k=mist.data.config.protocols[h];
+-if(k.connector==j){if("port"in k)g[j]=k.port;break}}g[j]=g[j]==i[j]?"":":"+g[j]}g.TS="";g["TS.exe"]="";N.find(".field").closest("label").hide();for(h in g){var q;j=d?d:g[h];switch(h){case "RTMP":case "RTMP.exe":q="rtmp://"+b.host+j+"/"+(e?e:"live")+"/";N.find(".field.RTMPurl").setval(q).closest("label").show();N.find(".field.RTMPkey").setval(a==""?"STREAMNAME":a).closest("label").show();q=q+(a==""?"STREAMNAME":a);break;case "RTSP":case "RTSP.exe":q="rtsp://"+b.host+j+"/"+(a==""?"STREAMNAME":a)+(e?
+-"?pass="+e:"");break;case "TS":case "TS.exe":q="udp://"+(f==""?b.host:f)+j+"/"}N.find(".field."+h.replace(".exe","")).setval(q).closest("label").show()}}},Ka=$("<div>"),oa={},u=[],La=$("<div>");for(r in mist.data.capabilities.processes)u.push([r,mist.data.capabilities.processes[r].hrn?mist.data.capabilities.processes[r].hrn:mist.data.capabilities.processes[r].name]);if(u.length){var ab=[{label:"New process",type:"select",select:u,value:u[0][0],pointer:{main:oa,index:"process"},"function":function(){var a=
+-$(this).getval();if(a!=null){var a=mist.data.capabilities.processes[a],b=[$("<h4>").text(a.name+" Process options")];La.html(UI.buildUI(b.concat(mist.convertBuildOptions(a,oa))))}}},La];Ka.append(UI.buildUI([$("<br>"),$("<h3>").text("Stream processes"),{label:"Stream processes",itemLabel:"stream process",type:"sublist",sublist:ab,saveas:oa,pointer:{main:p,index:"processes"}}]))}c.append(UI.buildUI([{label:"Stream name",type:"str",validate:["required","streamname"],pointer:{main:p,index:"name"},help:"Set the name this stream will be recognised by for players and/or stream pushing."},
+-{label:"Source",type:"browse",filetypes:Ha,pointer:{main:p,index:"source"},help:"<p> Below is the explanation of the input methods for MistServer. Anything between brackets () will go to default settings if not specified. </p> <table class=valigntop> <tr> <th colspan=3><b>File inputs</b></th> </tr> <tr> <th>File</th> <td> Linux/MacOS:&nbsp;/PATH/FILE<br> Windows:&nbsp;/cygdrive/DRIVE/PATH/FILE </td> <td> For file input please specify the proper path and file.<br> Supported inputs are: DTSC, FLV, MP3. MistServer Pro has TS, MP4, ISMV added as input. </td> </tr> <th> Folder </th> <td> Linux/MacOS:&nbsp;/PATH/<br> Windows:&nbsp;/cygdrive/DRIVE/PATH/ </td> <td class=LTSonly> A folder stream makes all the recognised files in the selected folder available as a stream. </td> </tr> <tr><td colspan=3>&nbsp;</td></tr> <tr> <th colspan=3><b>Push inputs</b></th> </tr> <tr> <th>RTMP</th> <td>push://(IP)(@PASSWORD)</td> <td> IP is white listed IP for pushing towards MistServer, if left empty all are white listed.<br> PASSWORD is the application under which to push to MistServer, if it doesn't match the stream will be rejected. PASSWORD is MistServer Pro only. </td> </tr> <tr> <th>RTSP</th> <td>push://(IP)(@PASSWORD)</td> <td class=LTSonly>IP is white listed IP for pushing towards MistServer, if left empty all are white listed.</td> </tr> <tr> <th>TS</th> <td>tsudp://(IP):PORT(/INTERFACE)</td> <td class=LTSonly> IP is the IP address used to listen for this stream, multi-cast IP range is: 224.0.0.0 - 239.255.255.255. If IP is not set all addresses will listened to.<br> PORT is the port you reserve for this stream on the chosen IP.<br> INTERFACE is the interface used, if left all interfaces will be used. </td> </tr> <tr><td colspan=3>&nbsp;</td></tr> <tr> <th colspan=3><b>Pull inputs</b></th> </tr> <tr> <th>DTSC</th> <td>dtsc://MISTSERVER_IP:PORT/(STREAMNAME)</td> <td>MISTSERVER_IP is the IP of another MistServer to pull from.<br> PORT is the DTSC port of the other MistServer. (default is 4200)<br> STREAMNAME is the name of the target stream on the other MistServer. If left empty, the name of this stream will be used. </td> </tr> <tr> <th>HLS</th> <td>http://URL/TO/STREAM.m3u8</td> <td class=LTSonly>The URL where the HLS stream is available to MistServer.</td> </tr> <tr> <th>RTSP</th> <td>rtsp://(USER:PASSWORD@)IP(:PORT)(/path)</td> <td class=LTSonly> USER:PASSWORD is the account used if authorization is required.<br> IP is the IP address used to pull this stream from.<br> PORT is the port used to connect through.<br> PATH is the path to be used to identify the correct stream. </td> </tr> </table>".replace(/LTSonly/g,
+-mist.data.LTS?'""':"LTSonly"),"function":function(){var a=$(this).val();Ja.remove();N.html("");if(a!=""){var b=null,d;for(d in mist.data.capabilities.inputs)if(typeof mist.data.capabilities.inputs[d].source_match!="undefined"&&mist.inputMatch(mist.data.capabilities.inputs[d].source_match,a)){b=d;break}if(b===null)da.html($("<h3>").text("Unrecognized input").addClass("red")).append($("<span>").text("Please edit the stream source.").addClass("red"));else{b=mist.data.capabilities.inputs[b];da.html($("<h3>").text(b.name+
+-" Input options"));var e=mist.convertBuildOptions(b,p);"always_match"in mist.data.capabilities.inputs[d]&&mist.inputMatch(mist.data.capabilities.inputs[d].always_match,a)&&e.push({label:"Always on",type:"checkbox",help:"Keep this input available at all times, even when there are no active viewers.",pointer:{main:p,index:"always_on"}});da.append(UI.buildUI(e));if(b.name=="Folder")c.append(Ja);else if(["Buffer","Buffer.exe","TS","TS.exe"].indexOf(b.name)>-1){d=[$("<br>"),$("<span>").text("Configure your source to push to:")];
++$("<span>").text("Loading.."),l=$("<span>"),n=$("<span>").addClass("logs"),h=$("<span>"),t=$("<span>"),m=$("<span>").text("Unknown"),k=$("<span>"),w=$("<span>"),i=parseURL(mist.user.host),i=i.protocol+i.host+i.port,g={};c.append(UI.buildUI([{type:"help",help:"You can find most basic information about your MistServer here.<br>You can also set the debug level and force a save to the config.json file that MistServer uses to save your settings. "},{type:"span",label:"Version",pointer:{main:mist.data.config,
++index:"version"}},{type:"span",label:"Version check",value:e},{type:"span",label:"Server time",value:t},{type:"span",label:"Licensed to",value:"license"in mist.data.config?mist.data.config.license.user:""},{type:"span",label:"Active licenses",value:m},{type:"span",label:"Configured streams",value:mist.data.streams?Object.keys(mist.data.streams).length:0},{type:"span",label:"Active streams",value:l},{type:"span",label:"Current connections",value:h},{type:"span",label:"Enabled protocols",value:k},{type:"span",
++label:"Disabled protocols",value:w},{type:"span",label:"Recent problems",value:n},$("<br>"),$("<h3>").text("Write config now"),{type:"help",help:"Tick the box in order to force an immediate save to the config.json MistServer uses to save your settings. Saving will otherwise happen upon closing MistServer. Don't forget to press save after ticking the box."},{type:"checkbox",label:"Force configurations save",pointer:{main:g,index:"save"}},{type:"buttons",buttons:[{type:"save",label:"Save","function":function(){var a=
++{};if(g.save)a.save=g.save;delete g.save;mist.send(function(){UI.navto("Overview")},a)}}]}]));if(mist.data.LTS){var f=function(a){function b(a){if(a.update){var d="";"progress"in a.update&&(d=" ("+a.update.progress+"%)");e.text("Updating.."+d);c(a.log);setTimeout(function(){mist.send(function(a){b(a)},{update:true})},1E3)}else UI.showTab("Overview")}function c(a){a=a.filter(function(a){return a[1]=="UPDR"});if(a.length){var b=$("<div>");e.append(b);for(var d in a)b.append($("<div>").text(a[d][2]))}}
++if(!a.update||!("uptodate"in a.update)){e.text("Unknown, checking..");setTimeout(function(){mist.send(function(a){"update"in a&&f(a)},{checkupdate:true})},5E3)}else if(a.update.error)e.addClass("red").text(a.update.error);else if(a.update.uptodate)e.text("Your version is up to date.").addClass("green");else{if(a.update.progress){e.addClass("orange").removeClass("red").text("Updating..");b(a)}else{e.text("");e.append($("<span>").addClass("red").text("On "+(new Date(a.update.date)).toLocaleDateString()+
++" version "+a.update.version+" became available."));(!a.update.url||a.update.url.slice(-4)!=".zip")&&e.append($("<button>").text("Rolling update").css({"font-size":"1em","margin-left":"1em"}).click(function(){if(confirm("Are you sure you want to execute a rolling update?")){e.addClass("orange").removeClass("red").text("Rolling update command sent..");mist.send(function(a){b(a)},{autoupdate:true})}}));var d=$("<a>").attr("href",a.update.url).attr("target","_blank").text("Manual download");d[0].protocol=
++"https:";e.append($("<div>").append(d))}c(a.log)}};f(mist.data);if("license"in mist.data.config){if("active_products"in mist.data.config.license&&Object.keys(mist.data.config.license.active_products).length){var j=$("<table>").css("text-indent","0");m.html(j);j.append($("<tr>").append($("<th>").append("Product")).append($("<th>").append("Updates until")).append($("<th>").append("Use until")).append($("<th>").append("Max. simul. instances")));for(var r in mist.data.config.license.active_products){var F=
++mist.data.config.license.active_products[r];j.append($("<tr>").append($("<td>").append(F.name)).append($("<td>").append(F.updates_final?F.updates_final:"&infin;")).append($("<td>").append(F.use_final?F.use_final:"&infin;")).append($("<td>").append(F.amount?F.amount:"&infin;")))}}else m.text("None. ");m.append($("<a>").text("More details").attr("href","https://shop.mistserver.org/myinvoices").attr("target","_blank"))}}else e.text("");var ya=function(){var a={totals:{fields:["clients"],start:-10},active_streams:true};
++if(!("cabailities"in mist.data))a.capabilities=true;mist.send(function(){za()},a)},za=function(){l.text("active_streams"in mist.data?mist.data.active_streams?mist.data.active_streams.length:0:"?");if("totals"in mist.data&&"all_streams"in mist.data.totals)var a=mist.data.totals.all_streams.all_protocols.clients,a=a.length?UI.format.number(a[a.length-1][1]):0;else a="Loading..";h.text(a);t.text(UI.format.dateTime(mist.data.config.time,"long"));n.html("");a=0;"license"in mist.data.config&&"user_msg"in
++mist.data.config.license&&mist.data.log.unshift([mist.data.config.license.time,"ERROR",mist.data.config.license.user_msg]);for(var b in mist.data.log){var c=mist.data.log[b];if(["FAIL","ERROR"].indexOf(c[1])>-1){a++;var d=$("<span>").addClass("content").addClass("red"),e=c[2].split("|");for(b in e)d.append($("<span>").text(e[b]));n.append($("<div>").append($("<span>").append(UI.format.time(c[0]))).append(d));if(a==5)break}}a==0&&n.html("None.");a=[];c=[];for(b in mist.data.config.protocols){d=mist.data.config.protocols[b];
++a.indexOf(d.connector)>-1||a.push(d.connector)}k.text(a.length?a.join(", "):"None.");if("capabilities"in mist.data){for(b in mist.data.capabilities.connectors)a.indexOf(b)==-1&&c.push(b);w.text(c.length?c.join(", "):"None.")}else w.text("Loading..")};ya();za();UI.interval.set(ya,3E4);break;case "General":var g={serverid:mist.data.config.serverid,debug:mist.data.config.debug,accesslog:mist.data.config.accesslog,prometheus:mist.data.config.prometheus,sessionViewerMode:mist.data.config.sessionViewerMode,
++sessionInputMode:mist.data.config.sessionInputMode,sessionOutputMode:mist.data.config.sessionOutputMode,sessionUnspecifiedMode:mist.data.config.sessionUnspecifiedMode,tknMode:mist.data.config.tknMode,sessionStreamInfoMode:mist.data.config.sessionStreamInfoMode,defaultStream:mist.data.config.defaultStream,trustedproxy:mist.data.config.trustedproxy,location:"location"in mist.data.config?mist.data.config.location:{}},C={};"bandwidth"in mist.data&&(C=mist.data.bandwidth,null==C&&(C={}),C.limit||(C.limit=
++""));var Aa=$("<select>").html($("<option>").val(1).text("bytes/s")).append($("<option>").val(1024).text("KiB/s")).append($("<option>").val(1048576).text("MiB/s")).append($("<option>").val(1073741824).text("GiB/s"));c.html(UI.buildUI([$("<h2>").text("General settings"),{type:"help",help:"These are settings that apply to your MistServer instance in general."},{type:"str",label:"Human readable name",pointer:{main:g,index:"serverid"},help:"You can name your MistServer here for personal use. You'll still need to set host name within your network yourself."},
++{type:"debug",label:"Debug level",pointer:{main:g,index:"debug"},help:"You can set the amount of debug information MistServer saves in the log. A full reboot of MistServer is required before some components of MistServer can post debug information."},{type:"selectinput",label:"Access log",selectinput:[["","Do not track"],["LOG","Log to MistServer log"],[{type:"str",label:"Path"},"Log to file"]],pointer:{main:g,index:"accesslog"},help:"Enable access logs."},{type:"selectinput",label:"Prometheus stats output",
++selectinput:[["","Disabled"],[{type:"str",label:"Passphrase"},"Enabled"]],pointer:{main:g,index:"prometheus"},help:"Make stats available in Prometheus format. These can be accessed via "+i+"/PASSPHRASE or "+i+"/PASSPHRASE.json."},{type:"inputlist",label:"Trusted proxies",help:"List of proxy server addresses that are allowed to override the viewer IP address to arbitrary values.<br>You may use a hostname or IP address.",pointer:{main:g,index:"trustedproxy"}},{type:"str",validate:["streamname_with_wildcard_and_variables"],
++label:"Fallback stream",pointer:{main:g,index:"defaultStream"},help:"When this is set, if someone attempts to view a stream that does not exist, or is offline, they will be redirected to this stream instead. $stream may be used to refer to the original stream name."},$("<h3>").text("Sessions"),{type:"bitmask",label:"Bundle viewer sessions by",bitmask:[[8,"Stream name"],[4,"IP address"],[2,"Token"],[1,"Protocol"]],pointer:{main:g,index:"sessionViewerMode"},help:"Change the way viewer connections are bundled into sessions.<br>Default: stream name, viewer IP and token"},
++{type:"bitmask",label:"Bundle input sessions by",bitmask:[[8,"Stream name"],[4,"IP address"],[2,"Token"],[1,"Protocol"]],pointer:{main:g,index:"sessionInputMode"},help:"Change the way input connections are bundled into sessions.<br>Default: stream name, input IP, token and protocol"},{type:"bitmask",label:"Bundle output sessions by",bitmask:[[8,"Stream name"],[4,"IP address"],[2,"Token"],[1,"Protocol"]],pointer:{main:g,index:"sessionOutputMode"},help:"Change the way output connections are bundled into sessions.<br>Default: stream name, output IP, token and protocol"},
++{type:"bitmask",label:"Bundle unspecified sessions by",bitmask:[[8,"Stream name"],[4,"IP address"],[2,"Token"],[1,"Protocol"]],pointer:{main:g,index:"sessionUnspecifiedMode"},help:"Change the way unspecified connections are bundled into sessions.<br>Default: none"},{type:"select",label:"Treat HTTP-only sessions as",select:[[1,"A viewer session"],[2,"An output session: skip executing the USER_NEW and USER_END triggers"],[4,"A separate 'unspecified' session: skip executing the USER_NEW and USER_END triggers"],
++[3,"Do not start a session: skip executing the USER_NEW and USER_END triggers and do not count for statistics"]],pointer:{main:g,index:"sessionStreamInfoMode"},help:"Change the way the stream info connection gets treated.<br>Default: as a viewer session"},{type:"bitmask",label:"Communicate session token",bitmask:[[8,"Write to cookie"],[4,"Write to URL parameter"],[2,"Read from cookie"],[1,"Read from URL parameter"]],pointer:{main:g,index:"tknMode"},help:"Change the way the session token gets passed to and from MistServer, which can be set as a cookie or URL parameter named `tkn`. Reading the session token as a URL parameter takes precedence over reading from the cookie.<br>Default: all"},
++$("<h3>").text("Load balancer"),{type:"help",help:"If you're using MistServer's load balancer, the information below is passed to it so that it can make informed decisions."},{type:"selectinput",label:"Server's bandwidth limit",selectinput:[["","Default (1 gbps)"],[{label:"Custom",type:"int",min:0,unit:Aa},"Custom"]],pointer:{main:C,index:"limit"},help:"This is the amount of traffic this server is willing to handle."},{type:"inputlist",label:"Bandwidth exceptions",pointer:{main:C,index:"exceptions"},
++help:"Data sent to the hosts and subnets listed here will not count towards reported bandwidth usage.<br>Examples:<ul><li>192.168.0.0/16</li><li>localhost</li><li>10.0.0.0/8</li><li>fe80::/16</li></ul>"},{type:"int",step:1E-8,label:"Server latitude",pointer:{main:g.location,index:"lat"},help:"This setting is only useful when MistServer is combined with a load balancer. When this is set, the balancer can send users to a server close to them."},{type:"int",step:1E-8,label:"Server longitude",pointer:{main:g.location,
++index:"lon"},help:"This setting is only useful when MistServer is combined with a load balancer. When this is set, the balancer can send users to a server close to them."},{type:"str",label:"Server location name",pointer:{main:g.location,index:"name"},help:"This setting is only useful when MistServer is combined with a load balancer. This will be displayed as the server's location."},{type:"buttons",buttons:[{type:"save",label:"Save","function":function(a){$(a).text("Saving..");var a={config:g},b=
++{};b.limit=C.limit?Aa.val()*C.limit:0;b.exceptions=C.exceptions;if(b.exceptions===null)b.exceptions=[];a.bandwidth=b;mist.send(function(){UI.navto("Overview")},a)}}]}]));break;case "Protocols":if("undefined"==typeof mist.data.capabilities){mist.send(function(){UI.navto(a)},{capabilities:!0});c.append("Loading..");return}var D=$("<tbody>");c.append(UI.buildUI([{type:"help",help:"You can find an overview of all the protocols and their relevant information here. You can add, edit or delete protocols."}])).append($("<button>").text("Delete all protocols").click(function(){if(confirm("Are you sure you want to delete all currently configured protocols?")){mist.data.config.protocols=
++[];mist.send(function(){UI.navto("Protocols")},{config:mist.data.config})}})).append($("<button>").text("Enable default protocols").click(function(){var a=Object.keys(mist.data.capabilities.connectors),b;for(b in mist.data.config.protocols){var c=a.indexOf(mist.data.config.protocols[b].connector);c>-1&&a.splice(c,1)}var d=[];for(b in a)(!("required"in mist.data.capabilities.connectors[a[b]])||Object.keys(mist.data.capabilities.connectors[a[b]].required).length==0)&&d.push(a[b]);c="Click OK to enable disabled protocols with their default settings:\n ";
++c=d.length?c+d.join(", "):c+"None.";if(d.length!=a.length){a=a.filter(function(a){return d.indexOf(a)<0});c=c+("\n\nThe following protocols can only be set manually:\n "+a.join(", "))}if(confirm(c)&&d.length){if(mist.data.config.protocols===null)mist.data.config.protocols=[];for(b in d)mist.data.config.protocols.push({connector:d[b]});mist.send(function(){UI.navto("Protocols")},{config:mist.data.config})}})).append("<br>").append($("<button>").text("New protocol").click(function(){UI.navto("Edit Protocol")}).css("clear",
++"both")).append($("<table>").html($("<thead>").html($("<tr>").html($("<th>").text("Protocol")).append($("<th>").text("Status")).append($("<th>").text("Settings")).append($("<th>")))).append(D));var Ba=function(){function a(b){var c=mist.data.capabilities.connectors[b.connector];if(!c)return"";var d=[],e=["required","optional"],g;for(g in e)for(var E in c[e[g]])b[E]&&b[E]!=""?d.push(E+": "+b[E]):c[e[g]][E]["default"]&&d.push(E+": "+c[e[g]][E]["default"]);return $("<span>").addClass("description").text(d.join(", "))}
++D.html("");for(var b in mist.data.config.protocols){var c=mist.data.config.protocols[b],d=mist.data.capabilities.connectors[c.connector];D.append($("<tr>").data("index",b).append($("<td>").text(d&&d.friendly?d.friendly:c.connector)).append($("<td>").html(UI.format.status(c))).append($("<td>").html(a(c))).append($("<td>").css("text-align","right").html($("<button>").text("Edit").click(function(){UI.navto("Edit Protocol",$(this).closest("tr").data("index"))})).append($("<button>").text("Delete").click(function(){var a=
++$(this).closest("tr").data("index");if(confirm('Are you sure you want to delete the protocol "'+mist.data.config.protocols[a].connector+'"?')){mist.send(function(){UI.navto("Protocols")},{deleteprotocol:mist.data.config.protocols[a]});mist.data.config.protocols.splice(a,1)}}))))}};Ba();UI.interval.set(function(){mist.send(function(){Ba()})},1E4);break;case "Edit Protocol":if("undefined"==typeof mist.data.capabilities){mist.send(function(){UI.navto(a,b)},{capabilities:!0});c.append("Loading..");return}var L=
++!1;""!=b&&0<=b&&(L=!0);var T={};for(r in mist.data.config.protocols)T[mist.data.config.protocols[r].connector]=1;var Ca=function(a){var b=mist.data.capabilities.connectors[a],c=mist.convertBuildOptions(b,p);if(L)var d=$.extend({},p);c.push({type:"hidden",pointer:{main:p,index:"connector"},value:a});c.push({type:"buttons",buttons:[{type:"save",label:"Save","function":function(){var a={};L?a.updateprotocol=[d,p]:a.addprotocol=p;mist.send(function(){UI.navto("Protocols")},a)}},{type:"cancel",label:"Cancel",
++"function":function(){UI.navto("Protocols")}}]});if("deps"in b&&b.deps!=""){j=$("<span>").text("Dependencies:");$ul=$("<ul>");j.append($ul);if(typeof b.deps=="string")b.deps=b.deps.split(", ");for(var e in b.deps){a=$("<li>").text(b.deps[e]+" ");$ul.append(a);typeof T[b.deps[e]]!="undefined"||typeof T[b.deps[e]+".exe"]!="undefined"?a.append($("<span>").addClass("green").text("(Configured)")):a.append($("<span>").addClass("red").text("(Not yet configured)"))}c.unshift({type:"text",text:j[0].innerHTML})}return UI.buildUI(c)},
++T={};for(r in mist.data.config.protocols)T[mist.data.config.protocols[r].connector]=1;if(L){var s=mist.data.config.protocols[b],p=s;c.find("h2").append(' "'+s.connector+'"');c.append(Ca(s.connector))}else{c.html($("<h2>").text("New Protocol"));var p={},u=[["",""]];for(r in mist.data.capabilities.connectors)u.push([r,mist.data.capabilities.connectors[r].friendly?mist.data.capabilities.connectors[r].friendly:r]);var Q=$("<span>");c.append(UI.buildUI([{label:"Protocol",type:"select",select:u,"function":function(){$(this).getval()!=
++""&&Q.html(Ca($(this).getval()))}}])).append(Q)}break;case "Streams":if(!("capabilities"in mist.data)){c.html("Loading..");mist.send(function(){UI.navto(a)},{capabilities:!0});return}var Da=$("<button>"),M=$("<span>").text("Loading..");c.append(UI.buildUI([{type:"help",help:"Here you can create, edit or delete new and existing streams. Go to stream preview or embed a video player on your website."},$("<div>").css({width:"45.25em",display:"flex","justify-content":"flex-end"}).append(Da).append($("<button>").text("Create a new stream").click(function(){UI.navto("Edit")}))])).append(M);
++""==b&&(g=mist.stored.get(),"viewmode"in g&&(b=g.viewmode));Da.text("Switch to "+("thumbnails"==b?"list":"thumbnail")+" view").click(function(){mist.stored.set("viewmode",b=="thumbnails"?"list":"thumbnails");UI.navto("Streams",b=="thumbnails"?"list":"thumbnails")});var A=$.extend(!0,{},mist.data.streams),ja=function(a,b){var c=$.extend({},b);delete c.meta;delete c.error;c.online=2;c.name=a;c.ischild=true;return c},ka=function(b,d,e){M.remove();switch(b){case "thumbnails":var g=$("<div>").addClass("preview_icons"),
++f;f=e||[];d.sort();d.unshift("");M.remove();c.append($("<h2>").text(a)).append(UI.buildUI([{label:"Filter the streams",type:"datalist",datalist:d,pointer:{main:{},index:"stream"},help:"If you type something here, the box below will only show streams with names that contain your text.","function":function(){var a=$(this).val();g.children().each(function(){$(this).hide();$(this).attr("data-stream").indexOf(a)>-1&&$(this).show()})}}]));d.shift();c.append($("<span>").addClass("description").text("Choose a stream below.")).append(g);
++for(var h in d){var b=d[h],i="",j=$("<button>").text("Delete").click(function(){var a=$(this).closest("div").attr("data-stream");if(confirm('Are you sure you want to delete the stream "'+a+'"?')){delete mist.data.streams[a];var b={};b.deletestream=[a];mist.send(function(){UI.navto("Streams")},b)}}),k=$("<button>").text("Settings").click(function(){UI.navto("Edit",$(this).closest("div").attr("data-stream"))}),e=$("<button>").text("Preview").click(function(){UI.navto("Preview",$(this).closest("div").attr("data-stream"))}),
++l=$("<button>").text("Embed").click(function(){UI.navto("Embed",$(this).closest("div").attr("data-stream"))}),q=$("<span>").addClass("image");if(b.indexOf("+")>-1){i=b.split("+");i=mist.data.streams[i[0]].source+i[1];k=j="";q.addClass("wildcard")}else{i=mist.data.streams[b].source;if(f.indexOf(b)>-1){l=e="";q.addClass("folder")}}g.append($("<div>").append($("<span>").addClass("streamname").text(b)).append(q).append($("<span>").addClass("description").text(i)).append($("<span>").addClass("button_container").append(k).append(j).append(e).append(l)).attr("title",
++b).attr("data-stream",b))}break;default:var n=$("<tbody>").append($("<tr>").append("<td>").attr("colspan",6).text("Loading.."));h=$("<table>").html($("<thead>").html($("<tr>").html($("<th>").text("Stream name").attr("data-sort-type","string").addClass("sorting-asc")).append($("<th>").text("Source").attr("data-sort-type","string")).append($("<th>").text("Status").attr("data-sort-type","int")).append($("<th>").css("text-align","right").text("Connections").attr("data-sort-type","int")).append($("<th>")).append($("<th>")))).append(n);
++c.append(h);h.stupidtable();var m=function(){var a=[],b;for(b in mist.data.active_streams)a.push({streams:[mist.data.active_streams[b]],fields:["clients"],start:-2});mist.send(function(){$.extend(true,A,mist.data.streams);var a=0;n.html("");d.sort();for(var b in d){var c=d[b],e;e=c in mist.data.streams?mist.data.streams[c]:A[c];var g=$("<td>").css("text-align","right").html($("<span>").addClass("description").text("Loading..")),f=0;if(typeof mist.data.totals!="undefined"&&typeof mist.data.totals[c]!=
++"undefined"){var h=mist.data.totals[c].all_protocols.clients,f=0;if(h.length){for(a in h)f=f+h[a][1];f=Math.round(f/h.length)}}g.html(UI.format.number(f));if(f==0&&e.online==1)e.online=2;f=$("<td>").css("text-align","right").css("white-space","nowrap");(!("ischild"in e)||!e.ischild)&&f.html($("<button>").text("Settings").click(function(){UI.navto("Edit",$(this).closest("tr").data("index"))})).append($("<button>").text("Delete").click(function(){var a=$(this).closest("tr").data("index");if(confirm('Are you sure you want to delete the stream "'+
++a+'"?')){delete mist.data.streams[a];var b={};mist.data.LTS?b.deletestream=[a]:b.streams=mist.data.streams;mist.send(function(){UI.navto("Streams")},b)}}));h=$("<span>").text(e.name);e.ischild&&h.css("padding-left","1em");var E=UI.format.status(e),i=$("<button>").text("Preview").click(function(){UI.navto("Preview",$(this).closest("tr").data("index"))}),q=$("<button>").text("Embed").click(function(){UI.navto("Embed",$(this).closest("tr").data("index"))});if("filesfound"in A[c]||e.online<0){E.html("");
++i="";g.html("");q=""}n.append($("<tr>").data("index",c).html($("<td>").html(h).attr("title",e.name=="..."?"The results were truncated":e.name).addClass("overflow_ellipsis")).append($("<td>").text(e.source).attr("title",e.source).addClass("description").addClass("overflow_ellipsis").css("max-width","20em")).append($("<td>").data("sort-value",e.online).html(E)).append(g).append($("<td>").css("white-space","nowrap").html(i).append(q)).append(f));a++}},{totals:a,active_streams:true})};if(mist.data.LTS){var o=
++0,p=0;for(f in mist.data.streams){h=mist.data.capabilities.inputs.Folder||mist.data.capabilities.inputs["Folder.exe"];if(!h)break;if(mist.inputMatch(h.source_match,mist.data.streams[f].source)){A[f].source=A[f].source+"*";A[f].filesfound=null;mist.send(function(a,b){var c=b.stream,d=0,e;a:for(e in a.browse.files){var g;for(g in mist.data.capabilities.inputs)if(!(g.indexOf("Buffer")>=0||g.indexOf("Buffer.exe")>=0||g.indexOf("Folder")>=0||g.indexOf("Folder.exe")>=0)&&mist.inputMatch(mist.data.capabilities.inputs[g].source_match,
++"/"+a.browse.files[e])){var f=c+"+"+a.browse.files[e];A[f]=ja(f,mist.data.streams[c]);A[f].source=mist.data.streams[c].source+a.browse.files[e];d++;if(d>=500){A[c+"+zzzzzzzzz"]={ischild:true,name:"...",online:-1};break a}}}"files"in a.browse&&a.browse.files.length?A[c].filesfound=true:mist.data.streams[c].filesfound=false;p++;if(o==p){mist.send(function(){m()},{active_streams:true});UI.interval.set(function(){m()},5E3)}},{browse:mist.data.streams[f].source},{stream:f});o++}}if(o==0){mist.send(function(){m()},
++{active_streams:true});UI.interval.set(function(){m()},5E3)}}else{mist.send(function(){m()},{active_streams:true});UI.interval.set(function(){m()},5E3)}}};if(mist.data.LTS){var la=0,Ea=0,u={},Fa=[];for(g in mist.data.streams)if(mist.inputMatch((mist.data.capabilities.inputs.Folder||mist.data.capabilities.inputs["Folder.exe"]).source_match,mist.data.streams[g].source))Fa.push(g),mist.send(function(a,c){var d=c.stream,e=0,g;a:for(g in a.browse.files){var f;for(f in mist.data.capabilities.inputs)if(!(f.indexOf("Buffer")>=
++0||f.indexOf("Folder")>=0)&&mist.inputMatch(mist.data.capabilities.inputs[f].source_match,"/"+a.browse.files[g])){u[d+"+"+a.browse.files[g]]=true;e++;if(e>=500){u[d+"+zzzzzzzzz"]=true;break a}}}Ea++;la==Ea&&mist.send(function(){for(var a in mist.data.active_streams){var c=mist.data.active_streams[a].split("+");if(c.length>1&&c[0]in mist.data.streams){u[mist.data.active_streams[a]]=true;A[mist.data.active_streams[a]]=ja(mist.data.active_streams[a],mist.data.streams[c[0]])}}u=Object.keys(u);u=u.concat(Object.keys(mist.data.streams));
++u.sort();ka(b,u,Fa)},{active_streams:true})},{browse:mist.data.streams[g].source},{stream:g}),la++;0==la&&mist.send(function(){for(var a in mist.data.active_streams){var c=mist.data.active_streams[a].split("+");if(c.length>1&&c[0]in mist.data.streams){u[mist.data.active_streams[a]]=true;A[mist.data.active_streams[a]]=ja(mist.data.active_streams[a],mist.data.streams[c[0]])}}u=Object.keys(u);mist.data.streams&&(u=u.concat(Object.keys(mist.data.streams)));u.sort();ka(b,u)},{active_streams:!0})}else ka(b,
++Object.keys(mist.data.streams));break;case "Edit":if("undefined"==typeof mist.data.capabilities){mist.send(function(){UI.navto(a,b)},{capabilities:!0});c.append("Loading..");return}L=!1;""!=b&&(L=!0);if(L){var Ga=b,p=mist.data.streams[Ga];c.find("h2").append(' "'+Ga+'"')}else c.html($("<h2>").text("New Stream")),p={};var Ha=[];for(r in mist.data.capabilities.inputs)Ha.push(mist.data.capabilities.inputs[r].source_match);var ca=$("<div>"),Ia=function(a){var c={};if(!mist.data.streams)mist.data.streams=
++{};mist.data.streams[p.name]=p;b!=p.name&&delete mist.data.streams[b];c.addstream={};c.addstream[p.name]=p;if(b!=p.name)c.deletestream=[b];if(p.stop_sessions&&b!=""){c.stop_sessions=b;delete p.stop_sessions}mist.send(function(){delete mist.data.streams[p.name].online;delete mist.data.streams[p.name].error;UI.navto(a,a=="Preview"?p.name:"")},c)},Ja=$("<style>").text("button.saveandpreview { display: none; }"),N=$("<span>"),ma=function(){var a=c.find("[name=name]").val();if(a){var b=parseURL(mist.user.host),
++d=c.find("[name=source]").val(),e=d.match(/@.*/);e&&(e=e[0].substring(1));var g=d.replace(/(?:.+?):\/\//,""),g=g.split("/"),g=g[0],g=g.split(":"),g=g[0];(d=d.match(/:\d+/))&&(d=d[0]);var f={},h=["RTMP","RTSP","RTMP.exe","RTSP.exe"],i;for(i in h)h[i]in mist.data.capabilities.connectors&&(f[h[i]]=mist.data.capabilities.connectors[h[i]].optional.port["default"]);var h={RTMP:1935,"RTMP.exe":1935,RTSP:554,"RTSP.exe":554,TS:-1,"TS.exe":-1},j;for(j in f){for(i in mist.data.config.protocols){var k=mist.data.config.protocols[i];
++if(k.connector==j){if("port"in k)f[j]=k.port;break}}f[j]=f[j]==h[j]?"":":"+f[j]}f.TS="";f["TS.exe"]="";N.find(".field").closest("label").hide();for(i in f){var q;j=d?d:f[i];switch(i){case "RTMP":case "RTMP.exe":q="rtmp://"+b.host+j+"/"+(e?e:"live")+"/";N.find(".field.RTMPurl").setval(q).closest("label").show();N.find(".field.RTMPkey").setval(a==""?"STREAMNAME":a).closest("label").show();q=q+(a==""?"STREAMNAME":a);break;case "RTSP":case "RTSP.exe":q="rtsp://"+b.host+j+"/"+(a==""?"STREAMNAME":a)+(e?
++"?pass="+e:"");break;case "TS":case "TS.exe":q="udp://"+(g==""?b.host:g)+j+"/"}N.find(".field."+i.replace(".exe","")).setval(q).closest("label").show()}}},Ka=$("<div>"),na={},u=[],La=$("<div>");for(r in mist.data.capabilities.processes)u.push([r,mist.data.capabilities.processes[r].hrn?mist.data.capabilities.processes[r].hrn:mist.data.capabilities.processes[r].name]);if(u.length){var ab=[{label:"New process",type:"select",select:u,value:u[0][0],pointer:{main:na,index:"process"},"function":function(){var a=
++$(this).getval();if(a!=null){var a=mist.data.capabilities.processes[a],b=[$("<h4>").text(a.name+" Process options")];La.html(UI.buildUI(b.concat(mist.convertBuildOptions(a,na))))}}},La];Ka.append(UI.buildUI([$("<br>"),$("<h3>").text("Stream processes"),{label:"Stream processes",itemLabel:"stream process",type:"sublist",sublist:ab,saveas:na,pointer:{main:p,index:"processes"}}]))}c.append(UI.buildUI([{label:"Stream name",type:"str",validate:["required","streamname"],pointer:{main:p,index:"name"},help:"Set the name this stream will be recognised by for players and/or stream pushing."},
++{label:"Source",type:"browse",filetypes:Ha,pointer:{main:p,index:"source"},help:"<p> Below is the explanation of the input methods for MistServer. Anything between brackets () will go to default settings if not specified. </p> <table class=valigntop> <tr> <th colspan=3><b>File inputs</b></th> </tr> <tr> <th>File</th> <td> Linux/MacOS:&nbsp;/PATH/FILE<br> Windows:&nbsp;/cygdrive/DRIVE/PATH/FILE </td> <td> For file input please specify the proper path and file.<br> Supported inputs are: DTSC, FLV, MP3. MistServer Pro has TS, MP4, ISMV added as input. </td> </tr> <th> Folder </th> <td> Linux/MacOS:&nbsp;/PATH/<br> Windows:&nbsp;/cygdrive/DRIVE/PATH/ </td> <td> A folder stream makes all the recognised files in the selected folder available as a stream. </td> </tr> <tr><td colspan=3>&nbsp;</td></tr> <tr> <th colspan=3><b>Push inputs</b></th> </tr> <tr> <th>RTMP</th> <td>push://(IP)(@PASSWORD)</td> <td> IP is white listed IP for pushing towards MistServer, if left empty all are white listed.<br> PASSWORD is the application under which to push to MistServer, if it doesn't match the stream will be rejected. PASSWORD is MistServer Pro only. </td> </tr> <tr> <th>RTSP</th> <td>push://(IP)(@PASSWORD)</td> <td>IP is white listed IP for pushing towards MistServer, if left empty all are white listed.</td> </tr> <tr> <th>TS</th> <td>tsudp://(IP):PORT(/INTERFACE)</td> <td> IP is the IP address used to listen for this stream, multi-cast IP range is: 224.0.0.0 - 239.255.255.255. If IP is not set all addresses will listened to.<br> PORT is the port you reserve for this stream on the chosen IP.<br> INTERFACE is the interface used, if left all interfaces will be used. </td> </tr> <tr><td colspan=3>&nbsp;</td></tr> <tr> <th colspan=3><b>Pull inputs</b></th> </tr> <tr> <th>DTSC</th> <td>dtsc://MISTSERVER_IP:PORT/(STREAMNAME)</td> <td>MISTSERVER_IP is the IP of another MistServer to pull from.<br> PORT is the DTSC port of the other MistServer. (default is 4200)<br> STREAMNAME is the name of the target stream on the other MistServer. If left empty, the name of this stream will be used. </td> </tr> <tr> <th>HLS</th> <td>http://URL/TO/STREAM.m3u8</td> <td>The URL where the HLS stream is available to MistServer.</td> </tr> <tr> <th>RTSP</th> <td>rtsp://(USER:PASSWORD@)IP(:PORT)(/path)</td> <td> USER:PASSWORD is the account used if authorization is required.<br> IP is the IP address used to pull this stream from.<br> PORT is the port used to connect through.<br> PATH is the path to be used to identify the correct stream. </td> </tr> </table>",
++"function":function(){var a=$(this).val();Ja.remove();N.html("");if(a!=""){var b=null,d;for(d in mist.data.capabilities.inputs)if(typeof mist.data.capabilities.inputs[d].source_match!="undefined"&&mist.inputMatch(mist.data.capabilities.inputs[d].source_match,a)){b=d;break}if(b===null)ca.html($("<h3>").text("Unrecognized input").addClass("red")).append($("<span>").text("Please edit the stream source.").addClass("red"));else{b=mist.data.capabilities.inputs[b];ca.html($("<h3>").text(b.name+" Input options"));
++var e=mist.convertBuildOptions(b,p);"always_match"in mist.data.capabilities.inputs[d]&&mist.inputMatch(mist.data.capabilities.inputs[d].always_match,a)&&e.push({label:"Always on",type:"checkbox",help:"Keep this input available at all times, even when there are no active viewers.",pointer:{main:p,index:"always_on"}});ca.append(UI.buildUI(e));if(b.name=="Folder")c.append(Ja);else if(["Buffer","Buffer.exe","TS","TS.exe"].indexOf(b.name)>-1){d=[$("<br>"),$("<span>").text("Configure your source to push to:")];
+ switch(b.name){case "Buffer":case "Buffer.exe":d.push({label:"RTMP full url",type:"span",clipboard:true,readonly:true,classes:["RTMP"],help:"Use this RTMP url if your client doesn't ask for a stream key"});d.push({label:"RTMP url",type:"span",clipboard:true,readonly:true,classes:["RTMPurl"],help:"Use this RTMP url if your client also asks for a stream key"});d.push({label:"RTMP stream key",type:"span",clipboard:true,readonly:true,classes:["RTMPkey"],help:"Use this key if your client asks for a stream key"});
+-d.push({label:"RTSP",type:"span",clipboard:true,readonly:true,classes:["RTSP"]});break;case "TS":case "TS.exe":a.charAt(0)=="/"?d=[]:d.push({label:"TS",type:"span",clipboard:true,readonly:true,classes:["TS"]})}N.html(UI.buildUI(d));na()}}}}},{label:"Stop sessions",type:"checkbox",help:"When saving these stream settings, kill this stream's current connections.",LTSonly:!0,pointer:{main:p,index:"stop_sessions"}},N,$("<br>"),{type:"custom",custom:da},Ka,{type:"buttons",buttons:[{type:"cancel",label:"Cancel",
+-"function":function(){UI.navto("Streams")}},{type:"save",label:"Save","function":function(){Ia("Streams")}},{type:"save",label:"Save and Preview","function":function(){Ia("Preview")},classes:["saveandpreview"]}]}]));c.find("[name=name]").keyup(function(){na()});na();break;case "Preview":""==b&&UI.navto("Streams");var O=parseURL(mist.user.host),V=O.protocol,S=O.host,G=":8080",v=V+S+G+"/";for(r in mist.data.config.protocols)if(s=mist.data.config.protocols[r],"HTTP"==s.connector||"HTTP.exe"==s.connector){s.pubaddr&&
+-s.pubaddr.length?"string"==typeof s.pubaddr?v=s.pubaddr.replace(/\/$/,"")+"/":s.pubaddr.length&&(v=s.pubaddr[0].replace(/\/$/,"")+"/"):(G=s.port?":"+s.port:":8080",v=V+S+G+"/");break}var R=$("<div>").css({display:"flex","flex-flow":"row wrap","flex-shrink":1,"min-width":"auto"}),W="";-1==b.indexOf("+")&&(W=$("<button>").text("Settings").addClass("settings").click(function(){UI.navto("Edit",b)}));c.html($("<div>").addClass("bigbuttons").append(W).append($("<button>").text("Embed").addClass("embed").click(function(){UI.navto("Embed",
+-b)})).append($("<button>").addClass("cancel").addClass("return").text("Return").click(function(){UI.navto("Streams")}))).append($("<h2>").text('Preview of "'+b+'"')).append(R);var H=encodeURIComponent(b),Ma=$("<div>").css("flex-shrink","1").css("min-width","auto").css("max-width","100%");R.append(Ma);var Na=$("<div>"),X=$("<div>").text("Loading player..").css("max-width","100%").css("flex-shrink","1").css("min-width","auto"),pa=$("<div>").addClass("controls");Ma.append(X).append(Na).append(pa);$("link#devcss").length||
+-c.append($("<link>").attr("rel","stylesheet").attr("type","text/css").attr("href",v+"skins/dev.css").attr("id","devcss"));var Oa=function(){Na.text("");var d=document.createElement("script");c.append(d);d.src=v+"player.js";d.onerror=function(){X.html($("<p>").append('Failed to load <a href="'+v+'player.js">'+v+"player.js</a>.")).append($("<p>").append("Please check if you've activated the HTTP protocol, if your http port is blocked, or if you're trying to load HTTPS on an HTTP page.")).append($("<button>").text("Reload").css("display",
+-"block").click(function(){Oa()}))};d.onload=function(){var e=b,f=function(){var a=MistVideoObject.reference;pa.html("");pa.append(a.UI.buildStructure({type:"container",classes:["mistvideo-column"],style:{flexShrink:1},children:[{"if":function(){return this.playerName&&this.source},then:{type:"container",classes:["mistvideo-description"],style:{display:"block"},children:[{type:"playername",style:{display:"inline"}},{type:"text",text:"is playing",style:{margin:"0 0.2em"}},{type:"mimetype"}]}},{type:"decodingIssues",
++d.push({label:"RTSP",type:"span",clipboard:true,readonly:true,classes:["RTSP"]});break;case "TS":case "TS.exe":a.charAt(0)=="/"?d=[]:d.push({label:"TS",type:"span",clipboard:true,readonly:true,classes:["TS"]})}N.html(UI.buildUI(d));ma()}}}}},{label:"Stop sessions",type:"checkbox",help:"When saving these stream settings, kill this stream's current connections.",pointer:{main:p,index:"stop_sessions"}},N,$("<br>"),{type:"custom",custom:ca},Ka,{type:"buttons",buttons:[{type:"cancel",label:"Cancel","function":function(){UI.navto("Streams")}},
++{type:"save",label:"Save","function":function(){Ia("Streams")}},{type:"save",label:"Save and Preview","function":function(){Ia("Preview")},classes:["saveandpreview"]}]}]));c.find("[name=name]").keyup(function(){ma()});ma();break;case "Preview":""==b&&UI.navto("Streams");var O=parseURL(mist.user.host),U=O.protocol,R=O.host,I=":8080",v=U+R+I+"/";for(r in mist.data.config.protocols)if(s=mist.data.config.protocols[r],"HTTP"==s.connector||"HTTP.exe"==s.connector){s.pubaddr&&s.pubaddr.length?"string"==
++typeof s.pubaddr?v=s.pubaddr.replace(/\/$/,"")+"/":s.pubaddr.length&&(v=s.pubaddr[0].replace(/\/$/,"")+"/"):(I=s.port?":"+s.port:":8080",v=U+R+I+"/");break}var Q=$("<div>").css({display:"flex","flex-flow":"row wrap","flex-shrink":1,"min-width":"auto"}),V="";-1==b.indexOf("+")&&(V=$("<button>").text("Settings").addClass("settings").click(function(){UI.navto("Edit",b)}));c.html($("<div>").addClass("bigbuttons").append(V).append($("<button>").text("Embed").addClass("embed").click(function(){UI.navto("Embed",
++b)})).append($("<button>").addClass("cancel").addClass("return").text("Return").click(function(){UI.navto("Streams")}))).append($("<h2>").text('Preview of "'+b+'"')).append(Q);var J=encodeURIComponent(b),Ma=$("<div>").css("flex-shrink","1").css("min-width","auto").css("max-width","100%");Q.append(Ma);var Na=$("<div>"),W=$("<div>").text("Loading player..").css("max-width","100%").css("flex-shrink","1").css("min-width","auto"),oa=$("<div>").addClass("controls");Ma.append(W).append(Na).append(oa);$("link#devcss").length||
++c.append($("<link>").attr("rel","stylesheet").attr("type","text/css").attr("href",v+"skins/dev.css").attr("id","devcss"));var Oa=function(){Na.text("");var d=document.createElement("script");c.append(d);d.src=v+"player.js";d.onerror=function(){W.html($("<p>").append('Failed to load <a href="'+v+'player.js">'+v+"player.js</a>.")).append($("<p>").append("Please check if you've activated the HTTP protocol, if your http port is blocked, or if you're trying to load HTTPS on an HTTP page.")).append($("<button>").text("Reload").css("display",
++"block").click(function(){Oa()}))};d.onload=function(){var e=b,g=function(){var a=MistVideoObject.reference;oa.html("");oa.append(a.UI.buildStructure({type:"container",classes:["mistvideo-column"],style:{flexShrink:1},children:[{"if":function(){return this.playerName&&this.source},then:{type:"container",classes:["mistvideo-description"],style:{display:"block"},children:[{type:"playername",style:{display:"inline"}},{type:"text",text:"is playing",style:{margin:"0 0.2em"}},{type:"mimetype"}]}},{type:"decodingIssues",
+ style:{"max-width":"30em","flex-flow":"column nowrap"}},{type:"container",classes:["mistvideo-column","mistvideo-devcontrols"],children:[{type:"text",text:"Player control"},{type:"container",classes:["mistvideo-devbuttons"],style:{"flex-wrap":"wrap"},children:[{"if":function(){return!(!this.player||!this.player.api)},then:{type:"button",title:"Reload the video source",label:"Reload video",onclick:function(){this.player.api.load()}}},{type:"button",title:"Build MistVideo again",label:"Reload player",
+-onclick:function(){this.reload()}},{type:"button",title:"Switch to the next available player and source combination",label:"Try next combination",onclick:function(){this.nextCombo()}}]},{type:"forcePlayer"},{type:"forceType"},{type:"forceSource"}]},{type:"log"}]}))};if(!(a!="Preview"||!b||b==""||e!=b)){X[0].addEventListener("initialized",f);X[0].addEventListener("initializeFailed",f);MistVideoObject.reference=mistPlay(e,{target:X[0],host:v,skin:"dev",loop:true,MistVideoObject:MistVideoObject})}c[0].removeChild(d)};
+-MistVideoObject.reference={unload:function(){d.onload=function(){this.parentElement&&this.parentElement.removeChild(this)}}}};Oa();var qa=$("<div>").append($("<h3>").text("Meta information")),ea=$("<span>").text("Loading..");qa.append(ea);var Pa=$("<div>").addClass("process_info");qa.append(Pa);R.append(qa);if(""!=H){$.ajax({type:"GET",url:v+"json_"+H+".js",success:function(a){var b=function(a,b){return"maxbps"in a?UI.format.bytes(a[b],1):b=="maxbps"?UI.format.bytes(a.bps,1):"unknown"},c=a.meta;if(!c||
+-!c.tracks)ea.html("No meta information available.");else{a=[];a.push({label:"Type",type:"span",value:c.live?"Live":"Pre-recorded (VoD)"});"format"in c&&a.push({label:"Format",type:"span",value:c.format});c.live&&a.push({label:"Buffer window",type:"span",value:UI.format.addUnit(c.buffer_window,"ms")});var d={audio:{vheader:"Audio",labels:["Codec","Duration","Avg bitrate","Peak bitrate","Channels","Samplerate","Language","Track index"],content:[]},video:{vheader:"Video",labels:["Codec","Duration","Avg bitrate",
+-"Peak bitrate","Size","Framerate","Language","Track index","Has B-Frames"],content:[]},subtitle:{vheader:"Subtitles",labels:["Codec","Duration","Avg bitrate","Peak bitrate","Language","Track index"],content:[]}},e=Object.keys(c.tracks);e.sort(function(a,b){a=a.split("_").pop();b=b.split("_").pop();return a-b});var f=1,g=1,i=1,h;for(h in e){var j=e[h],q=c.tracks[j];switch(q.type){case "audio":d.audio.content.push({header:"Track "+j.split("_").pop(),body:[q.codec,UI.format.duration((q.lastms-q.firstms)/
+-1E3)+"<br><span class=description>"+UI.format.duration(q.firstms/1E3)+" to "+UI.format.duration(q.lastms/1E3)+"</span>",b(q,"bps"),b(q,"maxbps"),q.channels,UI.format.addUnit(UI.format.number(q.rate),"Hz"),"language"in q?q.language:"unknown",f]});f++;break;case "video":d.video.content.push({header:"Track "+j.split("_").pop(),body:[q.codec,UI.format.duration((q.lastms-q.firstms)/1E3)+"<br><span class=description>"+UI.format.duration(q.firstms/1E3)+" to "+UI.format.duration(q.lastms/1E3)+"</span>",b(q,
+-"bps"),b(q,"maxbps"),UI.format.addUnit(q.width,"x ")+UI.format.addUnit(q.height,"px"),UI.format.addUnit(UI.format.number(q.fpks/1E3),"fps"),"language"in q?q.language:"unknown",g,"bframes"in q?"yes":"no"]});g++;break;case "meta":case "subtitle":if(q.codec=="subtitle"||q.type=="subtitle"){d.subtitle.content.push({header:"Track "+j.split("_").pop(),body:[q.codec,UI.format.duration((q.lastms-q.firstms)/1E3)+"<br><span class=description>"+UI.format.duration(q.firstms/1E3)+" to "+UI.format.duration(q.lastms/
+-1E3)+"</span>",b(q,"bps"),b(q,"maxbps"),"language"in q?q.language:"unknown",i]});i++}}}b=["audio","video","subtitle"];h=$("<div>").css({display:"flex","flex-flow":"row wrap","font-size":"0.9em"});for(j in b)d[b[j]].content.length&&h.append(UI.buildVheaderTable(d[b[j]]).css("width","auto"));a.push($("<span>").text("Tracks:"));a.push(h);ea.html(UI.buildUI(a))}},error:function(){ea.html("Error while retrieving stream info.")}});var Qa=function(){var a={proc_list:b};if(!mist.data.capabilities)a.capabilities=
++onclick:function(){this.reload()}},{type:"button",title:"Switch to the next available player and source combination",label:"Try next combination",onclick:function(){this.nextCombo()}}]},{type:"forcePlayer"},{type:"forceType"},{type:"forceSource"}]},{type:"log"}]}))};if(!(a!="Preview"||!b||b==""||e!=b)){W[0].addEventListener("initialized",g);W[0].addEventListener("initializeFailed",g);MistVideoObject.reference=mistPlay(e,{target:W[0],host:v,skin:"dev",loop:true,MistVideoObject:MistVideoObject})}c[0].removeChild(d)};
++MistVideoObject.reference={unload:function(){d.onload=function(){this.parentElement&&this.parentElement.removeChild(this)}}}};Oa();var pa=$("<div>").append($("<h3>").text("Meta information")),da=$("<span>").text("Loading..");pa.append(da);var Pa=$("<div>").addClass("process_info");pa.append(Pa);Q.append(pa);if(""!=J){$.ajax({type:"GET",url:v+"json_"+J+".js",success:function(a){var b=function(a,b){return"maxbps"in a?UI.format.bytes(a[b],1):b=="maxbps"?UI.format.bytes(a.bps,1):"unknown"},c=a.meta;if(!c||
++!c.tracks)da.html("No meta information available.");else{a=[];a.push({label:"Type",type:"span",value:c.live?"Live":"Pre-recorded (VoD)"});"format"in c&&a.push({label:"Format",type:"span",value:c.format});c.live&&a.push({label:"Buffer window",type:"span",value:UI.format.addUnit(c.buffer_window,"ms")});var d={audio:{vheader:"Audio",labels:["Codec","Duration","Avg bitrate","Peak bitrate","Channels","Samplerate","Language","Track index"],content:[]},video:{vheader:"Video",labels:["Codec","Duration","Avg bitrate",
++"Peak bitrate","Size","Framerate","Language","Track index","Has B-Frames"],content:[]},subtitle:{vheader:"Subtitles",labels:["Codec","Duration","Avg bitrate","Peak bitrate","Language","Track index"],content:[]}},e=Object.keys(c.tracks);e.sort(function(a,b){a=a.split("_").pop();b=b.split("_").pop();return a-b});var g=1,f=1,h=1,i;for(i in e){var j=e[i],q=c.tracks[j];switch(q.type){case "audio":d.audio.content.push({header:"Track "+j.split("_").pop(),body:[q.codec,UI.format.duration((q.lastms-q.firstms)/
++1E3)+"<br><span class=description>"+UI.format.duration(q.firstms/1E3)+" to "+UI.format.duration(q.lastms/1E3)+"</span>",b(q,"bps"),b(q,"maxbps"),q.channels,UI.format.addUnit(UI.format.number(q.rate),"Hz"),"language"in q?q.language:"unknown",g]});g++;break;case "video":d.video.content.push({header:"Track "+j.split("_").pop(),body:[q.codec,UI.format.duration((q.lastms-q.firstms)/1E3)+"<br><span class=description>"+UI.format.duration(q.firstms/1E3)+" to "+UI.format.duration(q.lastms/1E3)+"</span>",b(q,
++"bps"),b(q,"maxbps"),UI.format.addUnit(q.width,"x ")+UI.format.addUnit(q.height,"px"),UI.format.addUnit(UI.format.number(q.fpks/1E3),"fps"),"language"in q?q.language:"unknown",f,"bframes"in q?"yes":"no"]});f++;break;case "meta":case "subtitle":if(q.codec=="subtitle"||q.type=="subtitle"){d.subtitle.content.push({header:"Track "+j.split("_").pop(),body:[q.codec,UI.format.duration((q.lastms-q.firstms)/1E3)+"<br><span class=description>"+UI.format.duration(q.firstms/1E3)+" to "+UI.format.duration(q.lastms/
++1E3)+"</span>",b(q,"bps"),b(q,"maxbps"),"language"in q?q.language:"unknown",h]});h++}}}b=["audio","video","subtitle"];i=$("<div>").css({display:"flex","flex-flow":"row wrap","font-size":"0.9em"});for(j in b)d[b[j]].content.length&&i.append(UI.buildVheaderTable(d[b[j]]).css("width","auto"));a.push($("<span>").text("Tracks:"));a.push(i);da.html(UI.buildUI(a))}},error:function(){da.html("Error while retrieving stream info.")}});var Qa=function(){var a={proc_list:b};if(!mist.data.capabilities)a.capabilities=
+ true;mist.send(function(a){if(a.proc_list){var b=$("<table>").css("width","auto"),c={"Process type:":function(a){return $("<b>").text(a.process)},"Source:":function(a){var b=$("<span>").text(a.source);a.source_tracks&&a.source_tracks.length&&b.append($("<span>").addClass("description").text(" track "+a.source_tracks.slice(0,-2).concat(a.source_tracks.slice(-2).join(" and ")).join(", ")));return b},"Sink:":function(a){var b=$("<span>").text(a.sink);a.sink_tracks&&a.sink_tracks.length&&b.append($("<span>").addClass("description").text(" track "+
+ a.sink_tracks.slice(0,-2).concat(a.sink_tracks.slice(-2).join(" and ")).join(", ")));return b},"Active for:":function(a){var b=(new Date).setSeconds((new Date).getSeconds()-a.active_seconds);return $("<span>").append($("<span>").text(UI.format.duration(a.active_seconds))).append($("<span>").addClass("description").text(" since "+UI.format.time(b/1E3)))},"Pid:":function(a,b){return b},"Logs:":function(a){var b=$("<div>").text("None.");if(a.logs&&a.logs.length){b.html("").addClass("description").css({overflow:"auto",
+ maxHeight:"6em",display:"flex",flexFlow:"column-reverse nowrap"});for(var c in a.logs){var d=a.logs[c];b.prepend($("<div>").append(UI.format.time(d[0])+" ["+d[1]+"] "+d[2]))}}return b},"Additional info:":function(a){var b;if(a.ainfo&&Object.keys(a.ainfo).length){b=$("<table>");for(var c in a.ainfo){var d=mist.data.capabilities.processes[a.process].ainfo[c];d||(d={name:c});b.append($("<tr>").append($("<th>").text(d.name+":")).append($("<td>").html(a.ainfo[c]).append(d.unit?$("<span>").addClass("unit").text(d.unit):
+-"")))}}else b=$("<span>").addClass("description").text("N/A");return b}};Pa.html($("<h4>").text("Stream processes")).append(b);for(var d in c){var e=$("<tr>");b.append(e);e.append($("<th>").text(d).css("vertical-align","top"));for(var f in a.proc_list){$out=c[d](a.proc_list[f],f);e.append($("<td>").html($out).css("vertical-align","top"))}}}},a)};UI.interval.set(function(){Qa()},5E3);Qa()}break;case "Embed":""==b&&UI.navTo("Streams");W="";-1==b.indexOf("+")&&(W=$("<button>").addClass("settings").text("Settings").click(function(){UI.navto("Edit",
+-b)}));c.html($("<div>").addClass("bigbuttons").append(W).append($("<button>").text("Preview").addClass("preview").click(function(){UI.navto("Preview",b)})).append($("<button>").addClass("cancel").addClass("return").text("Return").click(function(){UI.navto("Streams")}))).append($("<h2>").text('Embed "'+b+'"'));var Y=$("<span>");c.append(Y);var H=encodeURIComponent(b),O=parseURL(mist.user.host),V=O.protocol,S=O.host,G=":8080",Z,fa={},v={http:V+S+G+"/"};for(r in mist.data.config.protocols)if(s=mist.data.config.protocols[r],
+-"HTTP"==s.connector||"HTTP.exe"==s.connector)s.pubaddr?("string"==typeof s.pubaddr?v.http=s.pubaddr.replace(/\/$/,"")+"/":s.pubaddr.length&&(v.http=s.pubaddr[0].replace(/\/$/,"")+"/"),fa.http=s.pubaddr):(G=s.port?":"+s.port:":8080",v.http=V+S+G+"/");else if("HTTPS"==s.connector||"HTTPS.exe"==s.connector)s.pubaddr&&s.pubaddr.length?("string"==typeof s.pubaddr?v.https=s.pubaddr.replace(/\/$/,"")+"/":s.pubaddr.length&&(v.https=s.pubaddr[0].replace(/\/$/,"")+"/"),fa.https=s.pubaddr):(Z=s.port?":"+s.port:
+-":4433",v.https="https://"+S+Z+"/");var P=v.http,B={http:v.http};"https"in v&&(B.https=v.https);if(otherhost.host||otherhost.https){P=(otherhost.https&&Z?"https://":"http://")+(otherhost.host?otherhost.host:O.host)+(otherhost.https&&Z?Z:G)+"/";if(otherhost.host&&("http"in fa||(B.http=parseURL(B.http,{hostname:otherhost.host}).full),"https"in B&&!("https"in fa)))B.https=parseURL(B.https,{hostname:otherhost.host}).full;P=otherhost.https?B.https:B.http}var aa=!1,ra={forcePlayer:"",forceType:"",controls:!0,
+-autoplay:!0,loop:!1,muted:!1,fillSpace:!1,poster:"",urlappend:"",setTracks:{}},n=$.extend({},ra),Ra=UI.stored.getOpts();"embedoptions"in Ra&&(n=$.extend(n,Ra.embedoptions,!0),"object"!=typeof n.setTracks&&(n.setTracks={}));var ga={};switch(n.controls){case "stock":ga.controls="stock";break;case !0:ga.controls=1;break;case !1:ga.controls=0}var y=function(){function a(b){switch(typeof b){case "string":return $.isNumeric(b)?b:'"'+b+'"';case "object":return JSON.stringify(b);default:return b}}aa&&UI.stored.saveOpt("embedoptions",
+-n);for(var c=b+"_",d=12,e="";d--;){var f;f=Math.floor(Math.random()*62);f=f<10?f:f<36?String.fromCharCode(f+55):String.fromCharCode(f+61);e=e+f}var c=c+e,d=['target: document.getElementById("'+c+'")'],g;for(g in n)g=="prioritize_type"?n[g]&&n[g]!=""&&d.push("forcePriority: "+JSON.stringify({source:[["type",[n[g]]]]})):g=="monitor_action"?n[g]&&n[g]!=""&&n[g]=="nextCombo"&&d.push('monitor: {\n action: function(){\n this.MistVideo.log("Switching to nextCombo because of poor playback in "+this.MistVideo.source.type+" ("+Math.round(this.vars.score*1000)/10+"%)");\n this.MistVideo.nextCombo();\n }\n }'):
+-n[g]!=ra[g]&&(n[g]!=null&&(typeof n[g]!="object"||JSON.stringify(n[g])!=JSON.stringify(ra[g])))&&d.push(g+": "+a(n[g]));g=[];g.push('<div class="mistvideo" id="'+c+'">');g.push(" <noscript>");g.push(' <a href="'+(otherhost.https?B.https:B.http)+H+'.html" target="_blank">');g.push(" Click here to play this video");g.push(" </a>");g.push(" </noscript>");g.push(" <script>");g.push(" var a = function(){");g.push(' mistPlay("'+b+'",{');g.push(" "+d.join(",\n "));g.push(" });");
+-g.push(" };");g.push(" if (!window.mistplayers) {");g.push(' var p = document.createElement("script");');if("https"in v&&parseURL(v.http).protocol!="https://"){g.push(' if (location.protocol == "https:") { p.src = "'+B.https+'player.js" } ');g.push(' else { p.src = "'+B.http+'player.js" } ')}else g.push(' p.src = "'+P+'player.js"');g.push(" document.head.appendChild(p);");g.push(" p.onload = a;");g.push(" }");g.push(" else { a(); }");g.push(" <\/script>");
+-g.push("</div>");return g.join("\n")},sa=$("<span>").text("Loading.."),Sa=y(n),T=$("<div>").text("Loading..").css("display","flex").css("flex-flow","column nowrap"),Ta="";"https"in v&&(Ta=UI.buildUI([{label:"Use HTTPS",type:"checkbox","function":function(){if($(this).getval()!=otherhost.https){otherhost.https=$(this).getval();UI.navto("Embed",b)}},value:otherhost.https}]).find("label"));Y.append($("<span>").addClass("input_container").append($("<label>").addClass("UIelement").append($("<span>").addClass("label").text("Use a different host:")).append($("<span>").addClass("field_container").append($("<input>").attr("type",
+-"text").addClass("field").val(otherhost.host?otherhost.host:O.host)).append($("<span>").addClass("unit").append($("<button>").text("Apply").click(function(){otherhost.host=$(this).closest("label").find("input").val();UI.navto("Embed",b)}))))).append(Ta)).append(UI.buildUI([$("<h3>").text("Urls"),{label:"Stream info json",type:"str",value:P+"json_"+H+".js",readonly:!0,clipboard:!0,help:"Information about this stream as a json page."},{label:"Stream info script",type:"str",value:P+"info_"+H+".js",readonly:!0,
+-clipboard:!0,help:"This script loads information about this stream into a mistvideo javascript object."},{label:"HTML page",type:"str",value:P+H+".html",readonly:!0,qrcode:!0,clipboard:!0,help:"A basic html containing the embedded stream."},$("<h3>").text("Embed code"),{label:"Embed code",type:"textarea",value:Sa,rows:Sa.split("\n").length+3,readonly:!0,classes:["embed_code"],clipboard:!0,help:"Include this code on your webpage to embed the stream. The options below can be used to configure how your content is displayed."},
+-$("<h4>").text("Embed code options (optional)").css("margin-top",0),{type:"help",help:"Use these controls to customise what this embedded video will look like.<br>Not all players have all of these options."},{label:"Prioritize type",type:"select",select:[["","Automatic"]],pointer:{main:n,index:"prioritize_type"},classes:["prioritize_type"],"function":function(){if(aa){n.prioritize_type=$(this).getval();$(".embed_code").setval(y(n))}},help:"Try to use this source type first, but full back to something else if it is not available."},
+-{label:"Force type",type:"select",select:[["","Automatic"]],pointer:{main:n,index:"forceType"},classes:["forceType"],"function":function(){if(aa){n.forceType=$(this).getval();$(".embed_code").setval(y(n))}},help:"Only use this particular source."},{label:"Force player",type:"select",select:[["","Automatic"]],pointer:{main:n,index:"forcePlayer"},classes:["forcePlayer"],"function":function(){if(aa){n.forcePlayer=$(this).getval();$(".embed_code").setval(y(n))}},help:"Only use this particular player."},
+-{label:"Controls",type:"select",select:[["1","MistServer Controls"],["stock","Player controls"],["0","None"]],pointer:{main:ga,index:"controls"},"function":function(){n.controls=$(this).getval()==1;switch($(this).getval()){case 0:n.controls=false;break;case 1:n.controls=true;break;case "stock":n.controls="stock"}$(".embed_code").setval(y(n))},help:"The type of controls that should be shown."},{label:"Autoplay",type:"checkbox",pointer:{main:n,index:"autoplay"},"function":function(){n.autoplay=$(this).getval();
+-$(".embed_code").setval(y(n))},help:"Whether or not the video should play as the page is loaded."},{label:"Loop",type:"checkbox",pointer:{main:n,index:"loop"},"function":function(){n.loop=$(this).getval();$(".embed_code").setval(y(n))},help:"If the video should restart when the end is reached."},{label:"Start muted",type:"checkbox",pointer:{main:n,index:"muted"},"function":function(){n.muted=$(this).getval();$(".embed_code").setval(y(n))},help:"If the video should restart when the end is reached."},
+-{label:"Fill available space",type:"checkbox",pointer:{main:n,index:"fillSpace"},"function":function(){n.fillSpace=$(this).getval();$(".embed_code").setval(y(n))},help:"The video will fit the available space in its container, even if the video stream has a smaller resolution."},{label:"Poster",type:"str",pointer:{main:n,index:"poster"},"function":function(){n.poster=$(this).getval();$(".embed_code").setval(y(n))},help:"URL to an image that is displayed when the video is not playing."},{label:"Video URL addition",
+-type:"str",pointer:{main:n,index:"urlappend"},help:"The embed script will append this string to the video url, useful for sending through params.",classes:["embed_code_forceprotocol"],"function":function(){n.urlappend=$(this).getval();$(".embed_code").setval(y(n))}},{label:"Preselect tracks",type:"DOMfield",DOMfield:T,help:"Pre-select these tracks."},{label:"Monitoring action",type:"select",select:[["","Ask the viewer what to do"],["nextCombo","Try the next source / player combination"]],pointer:{main:n,
+-index:"monitor_action"},"function":function(){n.monitor_action=$(this).getval();$(".embed_code").setval(y(n))},help:"What the player should do when playback is poor."},$("<h3>").text("Protocol stream urls"),sa]));$.ajax({type:"GET",url:P+"json_"+H+".js",success:function(a){var b=[],c=Y.find(".field.forceType"),d=Y.find(".field.prioritize_type"),e;for(e in a.source){var g=a.source[e],f=UI.humanMime(g.type);b.push({label:f?f+" <span class=description>("+g.type+")</span>":UI.format.capital(g.type),type:"str",
+-value:g.url,readonly:true,qrcode:true,clipboard:true});f=UI.humanMime(g.type);if(c.children('option[value="'+g.type+'"]').length==0){c.append($("<option>").text(f?f+" ("+g.type+")":UI.format.capital(g.type)).val(g.type));d.append($("<option>").text(f?f+" ("+g.type+")":UI.format.capital(g.type)).val(g.type))}}c.val(n.forceType);d.val(n.prioritize_type);sa.html(UI.buildUI(b));T.html("");b={};for(e in a.meta.tracks){c=a.meta.tracks[e];if(c.codec=="subtitle")c.type="subtitle";if(!(c.type!="audio"&&c.type!=
+-"video"&&c.type!="subtitle")){c.type in b||(b[c.type]=c.type=="subtitle"?[]:[["","Autoselect "+c.type]]);b[c.type].push([c.trackid,UI.format.capital(c.type)+" track "+(b[c.type].length+(c.type=="subtitle"?1:0))])}}if(Object.keys(b).length){T.closest("label").show();var a=["audio","video","subtitle"],h;for(h in a){e=a[h];if(b[e]&&b[e].length){c=$("<select>").attr("data-type",e).css("flex-grow","1").change(function(){$(this).val()==""?delete n.setTracks[$(this).attr("data-type")]:n.setTracks[$(this).attr("data-type")]=
+-$(this).val();$(".embed_code").setval(y(n))});T.append(c);e=="subtitle"?b[e].unshift(["","No "+e]):b[e].push([-1,"No "+e]);for(var i in b[e])c.append($("<option>").val(b[e][i][0]).text(b[e][i][1]));if(e in n.setTracks){c.val(n.setTracks[e]);if(c.val()==null){c.val("");delete n.setTracks[e];$(".embed_code").setval(y(n))}}}}}else T.closest("label").hide();aa=true},error:function(){sa.html("Error while retrieving stream info.");T.closest("label").hide();n.setTracks={}}});var ha=document.createElement("script");
+-ha.src=v.http+"player.js";document.head.appendChild(ha);ha.onload=function(){var a=Y.find(".field.forcePlayer"),b;for(b in mistplayers)a.append($("<option>").text(mistplayers[b].name).val(b));document.head.removeChild(this)};ha.onerror=function(){document.head.removeChild(this)};break;case "Push":var I=$("<div>").text("Loading..");c.append(I);mist.send(function(a){function b(a){setTimeout(function(){mist.send(function(c){var d=false;if("push_list"in c&&c.push_list&&c.push_list.length){var d=true,
+-g;for(g in c.push_list)if(a.indexOf(c.push_list[g][0])>-1){d=false;break}}else d=true;if(d)for(g in a)e.find("tr[data-pushid="+a[g]+"]").remove();else b()},{push_list:1})},1E3)}function c(g,f){var h=$("<span>"),i=$("<span>");if(f=="Automatic"&&g.length>=4){h.append($("<span>").text(g[2]));g[3]&&h.append($("<span>").text(", schedule on "+(new Date(g[3]*1E3)).toLocaleString()));g.length>=5&&g[4]&&h.append($("<span>").text(", complete on "+(new Date(g[4]*1E3)).toLocaleString()))}else g.length>=4&&g[2]!=
+-g[3]?h.append($("<span>").text(g[2])).append($("<span>").html("&#187").addClass("unit").css("margin","0 0.5em")).append($("<span>").text(g[3])):h.append($("<span>").text(g[2]));var j=$("<td>").append($("<button>").text(f=="Automatic"?"Remove":"Stop").click(function(){if(confirm("Are you sure you want to "+$(this).text().toLowerCase()+" this push?\n"+g[1]+" to "+g[2])){var a=$(this).closest("tr");a.html($("<td colspan=99>").html($("<span>").addClass("red").text(f=="Automatic"?"Removing..":"Stopping..")));
+-if(f=="Automatic"){var c=g.slice(1);mist.send(function(){a.remove()},{push_auto_remove:[c]})}else mist.send(function(){b([g[0]])},{push_stop:[g[0]]})}}));if(f=="Automatic"){j.prepend($("<button>").text("Edit").click(function(){UI.navto("Start Push","auto_"+($(this).closest("tr").index()-1))}));j.append($("<button>").text("Stop pushes").click(function(){if(confirm('Are you sure you want to stop all pushes matching \n"'+g[1]+" to "+g[2]+'"?'+(d.wait!=0?"\n\nRetrying is enabled. You'll probably want to set that to 0.":
+-""))){var c=$(this);c.text("Stopping pushes..");var f=[],h;for(h in a.push_list)if(g[1]==a.push_list[h][1]&&g[2]==a.push_list[h][2]){f.push(a.push_list[h][0]);e.find("tr[data-pushid="+a.push_list[h][0]+"]").html($("<td colspan=99>").html($("<span>").addClass("red").text("Stopping..")))}mist.send(function(){c.text("Stop pushes");b(f)},{push_stop:f,push_settings:{wait:0}})}}))}else{if(g.length>=6){var k=g[5];i.append($("<div>").append("Active for: "+UI.format.duration(k.active_seconds))).append($("<div>").append("Data transfered: "+
+-UI.format.bytes(k.bytes))).append($("<div>").append("Media time transfered: "+UI.format.duration(k.mediatime*0.001)));"pkt_retrans_count"in k&&i.append($("<div>").append("Packets retransmitted: "+UI.format.number(k.pkt_retrans_count||0)));"pkt_loss_count"in k&&i.append($("<div>").append("Packets lost: "+UI.format.number(k.pkt_loss_count||0)+" ("+UI.format.addUnit(UI.format.number(k.pkt_loss_perc||0),"%")+" over the last "+UI.format.addUnit(5,"s")+")"))}if(g.length>=5)for(var l in g[4]){k=g[4][l];
+-i.append($("<div>").append(UI.format.time(k[0])+" ["+k[1]+"] "+k[2]))}}return $("<tr>").css("vertical-align","top").attr("data-pushid",g[0]).append($("<td>").text(g[1])).append($("<td>").append(h.children())).append($("<td>").addClass("logs").append(i.children())).append(j)}I.html(UI.buildUI([{type:"help",help:"You can push streams to files or other servers, allowing them to broadcast your stream as well."}]));var d=a.push_settings;d||(d={});var e=$("<table>").append($("<tr>").append($("<th>").text("Stream")).append($("<th>").text("Target")).append($("<th>")).append($("<th>"))),
+-g=e.clone();if("push_list"in a)for(var f in a.push_list)e.append(c(a.push_list[f],"Manual"));if("push_auto_list"in a)for(f in a.push_auto_list){var h=a.push_auto_list[f].slice();h.unshift(-1);g.append(c(h,"Automatic"))}I.append($("<h3>").text("Automatic pushes")).append(UI.buildUI([{label:"Delay before retry",unit:"s",type:"int",min:0,help:"How long the delay should be before MistServer retries an automatic push.<br>If set to 0, it does not retry.","default":0,pointer:{main:d,index:"wait"},LTSonly:1},
+-{label:"Maximum retries",unit:"/s",type:"int",min:0,help:"The maximum amount of retries per second (for all automatic pushes).<br>If set to 0, there is no limit.","default":0,pointer:{main:d,index:"maxspeed"},LTSonly:1},{type:"buttons",buttons:[{type:"save",label:"Save","function":function(){mist.send(function(){UI.navto("Push")},{push_settings:d})}}]}])).append($("<button>").text("Add an automatic push").click(function(){UI.navto("Start Push","auto")}));g.find("tr").length==1?I.append($("<div>").text("No automatic pushes have been configured.").addClass("text").css("margin-top",
+-"0.5em")):I.append(g);I.append($("<h3>").text("Pushes")).append($("<button>").text("Start a push").click(function(){UI.navto("Start Push")}));if(e.find("tr").length==1)I.append($("<div>").text("No pushes are active.").addClass("text").css("margin-top","0.5em"));else{var g=[],h=[],i=$("<select>").css("margin-left","0.5em").append($("<option>").text("Any stream").val("")),j=$("<select>").css("margin-left","0.5em").append($("<option>").text("Any target").val(""));for(f in a.push_list){g.indexOf(a.push_list[f][1])==
+--1&&g.push(a.push_list[f][1]);h.indexOf(a.push_list[f][2])==-1&&h.push(a.push_list[f][2])}g.sort();h.sort();for(f in g)i.append($("<option>").text(g[f]));for(f in h)j.append($("<option>").text(h[f]));I.append($("<button>").text("Stop all pushes").click(function(){var c=[],d;for(d in a.push_list)c.push(a.push_list[d][0]);if(c.length!=0&&confirm("Are you sure you want to stop all pushes?")){mist.send(function(){b(c)},{push_stop:c});e.find("tr:not(:first-child)").html($("<td colspan=99>").append($("<span>").addClass("red").text("Stopping..")));
+-$(this).remove()}})).append($("<label>").css("margin-left","1em").append($("<span>").text("Stop all pushes that match: ").css("font-size","0.9em")).append(i).append($("<span>").css("margin-left","0.5em").text("and").css("font-size","0.9em")).append(j).append($("<button>").css("margin-left","0.5em").text("Apply").click(function(){var c=i.val(),d=j.val();if(c==""&&d=="")return alert("Looks like you want to stop all pushes. Maybe you should use that button?");var g={},f;for(f in a.push_list)if((c==""||
+-a.push_list[f][1]==c)&&(d==""||a.push_list[f][2]==d))g[a.push_list[f][0]]=a.push_list[f];if(Object.keys(g).length==0)return alert("No matching pushes.");c="Are you sure you want to stop these pushes?\n\n";for(f in g)c=c+(g[f][1]+" to "+g[f][2]+"\n");if(confirm(c)){g=Object.keys(g);mist.send(function(){b(g)},{push_stop:g});for(f in g)e.find("tr[data-pushid="+g[f]+"]").html($("<td colspan=99>").html($("<span>").addClass("red").text("Stopping..")))}}))).append(e)}UI.interval.set(function(){mist.send(function(a){var b=
+-e.find("tr").first();e.empty();e.append(b);for(var d in a.push_list)e.append(c(a.push_list[d]))},{push_list:1})},5E3)},{push_settings:1,push_list:1,push_auto_list:1});break;case "Start Push":if(!("capabilities"in mist.data)){c.append("Loading Mist capabilities..");mist.send(function(){UI.navto("Start Push",b)},{capabilities:1});return}var x,ia=function(a){var d=false,g=b.split("_");b=g[0];g.length==2&&(d=g[1]);if(d!==false&&typeof a=="undefined")mist.send(function(a){ia(a.push_auto_list[d])},{push_auto_list:1});
+-else{var e=[],f={},h;for(h in mist.data.capabilities.connectors){g=mist.data.capabilities.connectors[h];if("push_urls"in g){e=e.concat(g.push_urls);f[h]=g.push_urls}}b=="auto"&&c.find("h2").text("Add automatic push");var i={params:{}};if(b=="auto"&&typeof a!="undefined"){i={stream:a[0],target:a[1],params:{}};g=i.target.split("?");if(g.length>1){params=g.pop();i.target=g.join("?");params=params.split("&");for(h in params){g=params[h].split("=");i.params[g.shift()]=g.join("=")}}}var j=$("<div>").css("margin",
++"")))}}else b=$("<span>").addClass("description").text("N/A");return b}};Pa.html($("<h4>").text("Stream processes")).append(b);for(var d in c){var e=$("<tr>");b.append(e);e.append($("<th>").text(d).css("vertical-align","top"));for(var g in a.proc_list){$out=c[d](a.proc_list[g],g);e.append($("<td>").html($out).css("vertical-align","top"))}}}},a)};UI.interval.set(function(){Qa()},5E3);Qa()}break;case "Embed":""==b&&UI.navTo("Streams");V="";-1==b.indexOf("+")&&(V=$("<button>").addClass("settings").text("Settings").click(function(){UI.navto("Edit",
++b)}));c.html($("<div>").addClass("bigbuttons").append(V).append($("<button>").text("Preview").addClass("preview").click(function(){UI.navto("Preview",b)})).append($("<button>").addClass("cancel").addClass("return").text("Return").click(function(){UI.navto("Streams")}))).append($("<h2>").text('Embed "'+b+'"'));var X=$("<span>");c.append(X);var J=encodeURIComponent(b),O=parseURL(mist.user.host),U=O.protocol,R=O.host,I=":8080",Y,ea={},v={http:U+R+I+"/"};for(r in mist.data.config.protocols)if(s=mist.data.config.protocols[r],
++"HTTP"==s.connector||"HTTP.exe"==s.connector)s.pubaddr?("string"==typeof s.pubaddr?v.http=s.pubaddr.replace(/\/$/,"")+"/":s.pubaddr.length&&(v.http=s.pubaddr[0].replace(/\/$/,"")+"/"),ea.http=s.pubaddr):(I=s.port?":"+s.port:":8080",v.http=U+R+I+"/");else if("HTTPS"==s.connector||"HTTPS.exe"==s.connector)s.pubaddr&&s.pubaddr.length?("string"==typeof s.pubaddr?v.https=s.pubaddr.replace(/\/$/,"")+"/":s.pubaddr.length&&(v.https=s.pubaddr[0].replace(/\/$/,"")+"/"),ea.https=s.pubaddr):(Y=s.port?":"+s.port:
++":4433",v.https="https://"+R+Y+"/");var P=v.http,B={http:v.http};"https"in v&&(B.https=v.https);if(otherhost.host||otherhost.https){P=(otherhost.https&&Y?"https://":"http://")+(otherhost.host?otherhost.host:O.host)+(otherhost.https&&Y?Y:I)+"/";if(otherhost.host&&("http"in ea||(B.http=parseURL(B.http,{hostname:otherhost.host}).full),"https"in B&&!("https"in ea)))B.https=parseURL(B.https,{hostname:otherhost.host}).full;P=otherhost.https?B.https:B.http}var Z=!1,qa={forcePlayer:"",forceType:"",controls:!0,
++autoplay:!0,loop:!1,muted:!1,fillSpace:!1,poster:"",urlappend:"",setTracks:{}},o=$.extend({},qa),Ra=UI.stored.getOpts();"embedoptions"in Ra&&(o=$.extend(o,Ra.embedoptions,!0),"object"!=typeof o.setTracks&&(o.setTracks={}));var fa={};switch(o.controls){case "stock":fa.controls="stock";break;case !0:fa.controls=1;break;case !1:fa.controls=0}var y=function(){function a(b){switch(typeof b){case "string":return $.isNumeric(b)?b:'"'+b+'"';case "object":return JSON.stringify(b);default:return b}}Z&&UI.stored.saveOpt("embedoptions",
++o);for(var c=b+"_",d=12,e="";d--;){var g;g=Math.floor(Math.random()*62);g=g<10?g:g<36?String.fromCharCode(g+55):String.fromCharCode(g+61);e=e+g}var c=c+e,d=['target: document.getElementById("'+c+'")'],f;for(f in o)f=="prioritize_type"?o[f]&&o[f]!=""&&d.push("forcePriority: "+JSON.stringify({source:[["type",[o[f]]]]})):f=="monitor_action"?o[f]&&o[f]!=""&&o[f]=="nextCombo"&&d.push('monitor: {\n action: function(){\n this.MistVideo.log("Switching to nextCombo because of poor playback in "+this.MistVideo.source.type+" ("+Math.round(this.vars.score*1000)/10+"%)");\n this.MistVideo.nextCombo();\n }\n }'):
++o[f]!=qa[f]&&(o[f]!=null&&(typeof o[f]!="object"||JSON.stringify(o[f])!=JSON.stringify(qa[f])))&&d.push(f+": "+a(o[f]));f=[];f.push('<div class="mistvideo" id="'+c+'">');f.push(" <noscript>");f.push(' <a href="'+(otherhost.https?B.https:B.http)+J+'.html" target="_blank">');f.push(" Click here to play this video");f.push(" </a>");f.push(" </noscript>");f.push(" <script>");f.push(" var a = function(){");f.push(' mistPlay("'+b+'",{');f.push(" "+d.join(",\n "));f.push(" });");
++f.push(" };");f.push(" if (!window.mistplayers) {");f.push(' var p = document.createElement("script");');if("https"in v&&parseURL(v.http).protocol!="https://"){f.push(' if (location.protocol == "https:") { p.src = "'+B.https+'player.js" } ');f.push(' else { p.src = "'+B.http+'player.js" } ')}else f.push(' p.src = "'+P+'player.js"');f.push(" document.head.appendChild(p);");f.push(" p.onload = a;");f.push(" }");f.push(" else { a(); }");f.push(" <\/script>");
++f.push("</div>");return f.join("\n")},ra=$("<span>").text("Loading.."),Sa=y(o),S=$("<div>").text("Loading..").css("display","flex").css("flex-flow","column nowrap"),Ta="";"https"in v&&(Ta=UI.buildUI([{label:"Use HTTPS",type:"checkbox","function":function(){if($(this).getval()!=otherhost.https){otherhost.https=$(this).getval();UI.navto("Embed",b)}},value:otherhost.https}]).find("label"));X.append($("<span>").addClass("input_container").append($("<label>").addClass("UIelement").append($("<span>").addClass("label").text("Use a different host:")).append($("<span>").addClass("field_container").append($("<input>").attr("type",
++"text").addClass("field").val(otherhost.host?otherhost.host:O.host)).append($("<span>").addClass("unit").append($("<button>").text("Apply").click(function(){otherhost.host=$(this).closest("label").find("input").val();UI.navto("Embed",b)}))))).append(Ta)).append(UI.buildUI([$("<h3>").text("Urls"),{label:"Stream info json",type:"str",value:P+"json_"+J+".js",readonly:!0,clipboard:!0,help:"Information about this stream as a json page."},{label:"Stream info script",type:"str",value:P+"info_"+J+".js",readonly:!0,
++clipboard:!0,help:"This script loads information about this stream into a mistvideo javascript object."},{label:"HTML page",type:"str",value:P+J+".html",readonly:!0,qrcode:!0,clipboard:!0,help:"A basic html containing the embedded stream."},$("<h3>").text("Embed code"),{label:"Embed code",type:"textarea",value:Sa,rows:Sa.split("\n").length+3,readonly:!0,classes:["embed_code"],clipboard:!0,help:"Include this code on your webpage to embed the stream. The options below can be used to configure how your content is displayed."},
++$("<h4>").text("Embed code options (optional)").css("margin-top",0),{type:"help",help:"Use these controls to customise what this embedded video will look like.<br>Not all players have all of these options."},{label:"Prioritize type",type:"select",select:[["","Automatic"]],pointer:{main:o,index:"prioritize_type"},classes:["prioritize_type"],"function":function(){if(Z){o.prioritize_type=$(this).getval();$(".embed_code").setval(y(o))}},help:"Try to use this source type first, but full back to something else if it is not available."},
++{label:"Force type",type:"select",select:[["","Automatic"]],pointer:{main:o,index:"forceType"},classes:["forceType"],"function":function(){if(Z){o.forceType=$(this).getval();$(".embed_code").setval(y(o))}},help:"Only use this particular source."},{label:"Force player",type:"select",select:[["","Automatic"]],pointer:{main:o,index:"forcePlayer"},classes:["forcePlayer"],"function":function(){if(Z){o.forcePlayer=$(this).getval();$(".embed_code").setval(y(o))}},help:"Only use this particular player."},
++{label:"Controls",type:"select",select:[["1","MistServer Controls"],["stock","Player controls"],["0","None"]],pointer:{main:fa,index:"controls"},"function":function(){o.controls=$(this).getval()==1;switch($(this).getval()){case 0:o.controls=false;break;case 1:o.controls=true;break;case "stock":o.controls="stock"}$(".embed_code").setval(y(o))},help:"The type of controls that should be shown."},{label:"Autoplay",type:"checkbox",pointer:{main:o,index:"autoplay"},"function":function(){o.autoplay=$(this).getval();
++$(".embed_code").setval(y(o))},help:"Whether or not the video should play as the page is loaded."},{label:"Loop",type:"checkbox",pointer:{main:o,index:"loop"},"function":function(){o.loop=$(this).getval();$(".embed_code").setval(y(o))},help:"If the video should restart when the end is reached."},{label:"Start muted",type:"checkbox",pointer:{main:o,index:"muted"},"function":function(){o.muted=$(this).getval();$(".embed_code").setval(y(o))},help:"If the video should restart when the end is reached."},
++{label:"Fill available space",type:"checkbox",pointer:{main:o,index:"fillSpace"},"function":function(){o.fillSpace=$(this).getval();$(".embed_code").setval(y(o))},help:"The video will fit the available space in its container, even if the video stream has a smaller resolution."},{label:"Poster",type:"str",pointer:{main:o,index:"poster"},"function":function(){o.poster=$(this).getval();$(".embed_code").setval(y(o))},help:"URL to an image that is displayed when the video is not playing."},{label:"Video URL addition",
++type:"str",pointer:{main:o,index:"urlappend"},help:"The embed script will append this string to the video url, useful for sending through params.",classes:["embed_code_forceprotocol"],"function":function(){o.urlappend=$(this).getval();$(".embed_code").setval(y(o))}},{label:"Preselect tracks",type:"DOMfield",DOMfield:S,help:"Pre-select these tracks."},{label:"Monitoring action",type:"select",select:[["","Ask the viewer what to do"],["nextCombo","Try the next source / player combination"]],pointer:{main:o,
++index:"monitor_action"},"function":function(){o.monitor_action=$(this).getval();$(".embed_code").setval(y(o))},help:"What the player should do when playback is poor."},$("<h3>").text("Protocol stream urls"),ra]));$.ajax({type:"GET",url:P+"json_"+J+".js",success:function(a){var b=[],c=X.find(".field.forceType"),d=X.find(".field.prioritize_type"),e;for(e in a.source){var f=a.source[e],g=UI.humanMime(f.type);b.push({label:g?g+" <span class=description>("+f.type+")</span>":UI.format.capital(f.type),type:"str",
++value:f.url,readonly:true,qrcode:true,clipboard:true});g=UI.humanMime(f.type);if(c.children('option[value="'+f.type+'"]').length==0){c.append($("<option>").text(g?g+" ("+f.type+")":UI.format.capital(f.type)).val(f.type));d.append($("<option>").text(g?g+" ("+f.type+")":UI.format.capital(f.type)).val(f.type))}}c.val(o.forceType);d.val(o.prioritize_type);ra.html(UI.buildUI(b));S.html("");b={};for(e in a.meta.tracks){c=a.meta.tracks[e];if(c.codec=="subtitle")c.type="subtitle";if(!(c.type!="audio"&&c.type!=
++"video"&&c.type!="subtitle")){c.type in b||(b[c.type]=c.type=="subtitle"?[]:[["","Autoselect "+c.type]]);b[c.type].push([c.trackid,UI.format.capital(c.type)+" track "+(b[c.type].length+(c.type=="subtitle"?1:0))])}}if(Object.keys(b).length){S.closest("label").show();var a=["audio","video","subtitle"],h;for(h in a){e=a[h];if(b[e]&&b[e].length){c=$("<select>").attr("data-type",e).css("flex-grow","1").change(function(){$(this).val()==""?delete o.setTracks[$(this).attr("data-type")]:o.setTracks[$(this).attr("data-type")]=
++$(this).val();$(".embed_code").setval(y(o))});S.append(c);e=="subtitle"?b[e].unshift(["","No "+e]):b[e].push([-1,"No "+e]);for(var i in b[e])c.append($("<option>").val(b[e][i][0]).text(b[e][i][1]));if(e in o.setTracks){c.val(o.setTracks[e]);if(c.val()==null){c.val("");delete o.setTracks[e];$(".embed_code").setval(y(o))}}}}}else S.closest("label").hide();Z=true},error:function(){ra.html("Error while retrieving stream info.");S.closest("label").hide();o.setTracks={}}});var ga=document.createElement("script");
++ga.src=v.http+"player.js";document.head.appendChild(ga);ga.onload=function(){var a=X.find(".field.forcePlayer"),b;for(b in mistplayers)a.append($("<option>").text(mistplayers[b].name).val(b));document.head.removeChild(this)};ga.onerror=function(){document.head.removeChild(this)};break;case "Push":var K=$("<div>").text("Loading..");c.append(K);mist.send(function(a){function b(a){setTimeout(function(){mist.send(function(c){var d=false;if("push_list"in c&&c.push_list&&c.push_list.length){var d=true,
++f;for(f in c.push_list)if(a.indexOf(c.push_list[f][0])>-1){d=false;break}}else d=true;if(d)for(f in a)e.find("tr[data-pushid="+a[f]+"]").remove();else b()},{push_list:1})},1E3)}function c(f,g){var h=$("<span>"),i=$("<span>");if(g=="Automatic"&&f.length>=4){h.append($("<span>").text(f[2]));f[3]&&h.append($("<span>").text(", schedule on "+(new Date(f[3]*1E3)).toLocaleString()));f.length>=5&&f[4]&&h.append($("<span>").text(", complete on "+(new Date(f[4]*1E3)).toLocaleString()))}else f.length>=4&&f[2]!=
++f[3]?h.append($("<span>").text(f[2])).append($("<span>").html("&#187").addClass("unit").css("margin","0 0.5em")).append($("<span>").text(f[3])):h.append($("<span>").text(f[2]));var j=$("<td>").append($("<button>").text(g=="Automatic"?"Remove":"Stop").click(function(){if(confirm("Are you sure you want to "+$(this).text().toLowerCase()+" this push?\n"+f[1]+" to "+f[2])){var a=$(this).closest("tr");a.html($("<td colspan=99>").html($("<span>").addClass("red").text(g=="Automatic"?"Removing..":"Stopping..")));
++if(g=="Automatic"){var c=f.slice(1);mist.send(function(){a.remove()},{push_auto_remove:[c]})}else mist.send(function(){b([f[0]])},{push_stop:[f[0]]})}}));if(g=="Automatic"){j.prepend($("<button>").text("Edit").click(function(){UI.navto("Start Push","auto_"+($(this).closest("tr").index()-1))}));j.append($("<button>").text("Stop pushes").click(function(){if(confirm('Are you sure you want to stop all pushes matching \n"'+f[1]+" to "+f[2]+'"?'+(d.wait!=0?"\n\nRetrying is enabled. You'll probably want to set that to 0.":
++""))){var c=$(this);c.text("Stopping pushes..");var g=[],h;for(h in a.push_list)if(f[1]==a.push_list[h][1]&&f[2]==a.push_list[h][2]){g.push(a.push_list[h][0]);e.find("tr[data-pushid="+a.push_list[h][0]+"]").html($("<td colspan=99>").html($("<span>").addClass("red").text("Stopping..")))}mist.send(function(){c.text("Stop pushes");b(g)},{push_stop:g,push_settings:{wait:0}})}}))}else{if(f.length>=6){var k=f[5];i.append($("<div>").append("Active for: "+UI.format.duration(k.active_seconds))).append($("<div>").append("Data transfered: "+
++UI.format.bytes(k.bytes))).append($("<div>").append("Media time transfered: "+UI.format.duration(k.mediatime*0.001)));"pkt_retrans_count"in k&&i.append($("<div>").append("Packets retransmitted: "+UI.format.number(k.pkt_retrans_count||0)));"pkt_loss_count"in k&&i.append($("<div>").append("Packets lost: "+UI.format.number(k.pkt_loss_count||0)+" ("+UI.format.addUnit(UI.format.number(k.pkt_loss_perc||0),"%")+" over the last "+UI.format.addUnit(5,"s")+")"))}if(f.length>=5)for(var l in f[4]){k=f[4][l];
++i.append($("<div>").append(UI.format.time(k[0])+" ["+k[1]+"] "+k[2]))}}return $("<tr>").css("vertical-align","top").attr("data-pushid",f[0]).append($("<td>").text(f[1])).append($("<td>").append(h.children())).append($("<td>").addClass("logs").append(i.children())).append(j)}K.html(UI.buildUI([{type:"help",help:"You can push streams to files or other servers, allowing them to broadcast your stream as well."}]));var d=a.push_settings;d||(d={});var e=$("<table>").append($("<tr>").append($("<th>").text("Stream")).append($("<th>").text("Target")).append($("<th>")).append($("<th>"))),
++f=e.clone();if("push_list"in a)for(var g in a.push_list)e.append(c(a.push_list[g],"Manual"));if("push_auto_list"in a)for(g in a.push_auto_list){var h=a.push_auto_list[g].slice();h.unshift(-1);f.append(c(h,"Automatic"))}K.append($("<h3>").text("Automatic pushes")).append(UI.buildUI([{label:"Delay before retry",unit:"s",type:"int",min:0,help:"How long the delay should be before MistServer retries an automatic push.<br>If set to 0, it does not retry.","default":0,pointer:{main:d,index:"wait"}},{label:"Maximum retries",
++unit:"/s",type:"int",min:0,help:"The maximum amount of retries per second (for all automatic pushes).<br>If set to 0, there is no limit.","default":0,pointer:{main:d,index:"maxspeed"}},{type:"buttons",buttons:[{type:"save",label:"Save","function":function(){mist.send(function(){UI.navto("Push")},{push_settings:d})}}]}])).append($("<button>").text("Add an automatic push").click(function(){UI.navto("Start Push","auto")}));f.find("tr").length==1?K.append($("<div>").text("No automatic pushes have been configured.").addClass("text").css("margin-top",
++"0.5em")):K.append(f);K.append($("<h3>").text("Pushes")).append($("<button>").text("Start a push").click(function(){UI.navto("Start Push")}));if(e.find("tr").length==1)K.append($("<div>").text("No pushes are active.").addClass("text").css("margin-top","0.5em"));else{var f=[],h=[],i=$("<select>").css("margin-left","0.5em").append($("<option>").text("Any stream").val("")),j=$("<select>").css("margin-left","0.5em").append($("<option>").text("Any target").val(""));for(g in a.push_list){f.indexOf(a.push_list[g][1])==
++-1&&f.push(a.push_list[g][1]);h.indexOf(a.push_list[g][2])==-1&&h.push(a.push_list[g][2])}f.sort();h.sort();for(g in f)i.append($("<option>").text(f[g]));for(g in h)j.append($("<option>").text(h[g]));K.append($("<button>").text("Stop all pushes").click(function(){var c=[],d;for(d in a.push_list)c.push(a.push_list[d][0]);if(c.length!=0&&confirm("Are you sure you want to stop all pushes?")){mist.send(function(){b(c)},{push_stop:c});e.find("tr:not(:first-child)").html($("<td colspan=99>").append($("<span>").addClass("red").text("Stopping..")));
++$(this).remove()}})).append($("<label>").css("margin-left","1em").append($("<span>").text("Stop all pushes that match: ").css("font-size","0.9em")).append(i).append($("<span>").css("margin-left","0.5em").text("and").css("font-size","0.9em")).append(j).append($("<button>").css("margin-left","0.5em").text("Apply").click(function(){var c=i.val(),d=j.val();if(c==""&&d=="")return alert("Looks like you want to stop all pushes. Maybe you should use that button?");var f={},g;for(g in a.push_list)if((c==""||
++a.push_list[g][1]==c)&&(d==""||a.push_list[g][2]==d))f[a.push_list[g][0]]=a.push_list[g];if(Object.keys(f).length==0)return alert("No matching pushes.");c="Are you sure you want to stop these pushes?\n\n";for(g in f)c=c+(f[g][1]+" to "+f[g][2]+"\n");if(confirm(c)){f=Object.keys(f);mist.send(function(){b(f)},{push_stop:f});for(g in f)e.find("tr[data-pushid="+f[g]+"]").html($("<td colspan=99>").html($("<span>").addClass("red").text("Stopping..")))}}))).append(e)}UI.interval.set(function(){mist.send(function(a){var b=
++e.find("tr").first();e.empty();e.append(b);for(var d in a.push_list)e.append(c(a.push_list[d]))},{push_list:1})},5E3)},{push_settings:1,push_list:1,push_auto_list:1});break;case "Start Push":if(!("capabilities"in mist.data)){c.append("Loading Mist capabilities..");mist.send(function(){UI.navto("Start Push",b)},{capabilities:1});return}var x,ha=function(a){var d=false,f=b.split("_");b=f[0];f.length==2&&(d=f[1]);if(d!==false&&typeof a=="undefined")mist.send(function(a){ha(a.push_auto_list[d])},{push_auto_list:1});
++else{var e=[],g={},h;for(h in mist.data.capabilities.connectors){f=mist.data.capabilities.connectors[h];if("push_urls"in f){e=e.concat(f.push_urls);g[h]=f.push_urls}}b=="auto"&&c.find("h2").text("Add automatic push");var i={params:{}};if(b=="auto"&&typeof a!="undefined"){i={stream:a[0],target:a[1],params:{}};f=i.target.split("?");if(f.length>1){params=f.pop();i.target=f.join("?");params=params.split("&");for(h in params){f=params[h].split("=");i.params[f.shift()]=f.join("=")}}}var j=$("<div>").css("margin",
+ "1em 0");h=[{label:"Stream name",type:"str",help:"This may either be a full stream name, a partial wildcard stream name, or a full wildcard stream name.<br>For example, given the stream <i>a</i> you can use: <ul> <li><i>a</i>: the stream configured as <i>a</i></li> <li><i>a+</i>: all streams configured as <i>a</i> with a wildcard behind it, but not <i>a</i> itself</li> <li><i>a+b</i>: only the version of stream <i>a</i> that has wildcard <i>b</i></li> </ul>",
+-pointer:{main:i,index:"stream"},validate:["required",function(a){a=a.split("+");a=a[0];return a in mist.data.streams?false:{msg:"'"+a+"' is not a stream name.",classes:["orange"],"break":false}}],datalist:x,LTSonly:1},{label:"Target",type:"str",help:"Where the stream will be pushed to.<br> Valid formats: <ul> <li>"+e.join("</li><li>")+"</li> </ul> Valid text replacements: <ul> <li>$stream - inserts the stream name used to push to MistServer</li> <li>$day - inserts the current day number</li><li>$month - inserts the current month number</li> <li>$year - inserts the current year number</li><li>$hour - inserts the hour timestamp when stream was received</li> <li>$minute - inserts the minute timestamp the stream was received</li> <li>$seconds - inserts the seconds timestamp when the stream was received</li> <li>$datetime - inserts $year.$month.$day.$hour.$minute.$seconds timestamp when the stream was received</li> </ul> Valid URL parameters: <ul> <li>recstart=123 - media timestamp in milisseconds where the push should start</li> <li>recstop=456 - media timestamp in miliseconds where the push should stop</li> <li>recstartunix=150000000 - unix time in seconds where the push should start. This will override the recstart parameter.</li> <li>recstopunix=150000000 - unix time in seconds where the push should stop. This will override the recstop parameter.</li> </ul>",
+-pointer:{main:i,index:"target"},validate:["required",function(a){for(var b in e)if(mist.inputMatch(e[b],a))return false;return{msg:"Does not match a valid target.<br>Valid formats:<ul><li>"+e.join("</li><li>")+"</li></ul>",classes:["red"]}}],"function":function(){var a=false;for(connector in f)for(var b in f[connector])if(mist.inputMatch(f[connector][b],$(this).getval())){a=connector;break}if(a){j.html($("<h3>").text(mist.data.capabilities.connectors[a].friendly));j.append(UI.buildUI(mist.convertBuildOptions({desc:mist.data.capabilities.connectors[a].desc,
+-optional:mist.data.capabilities.connectors[a].push_parameters},i.params)))}else j.html($("<h4>").addClass("red").text("Unrecognized target.")).append($("<span>").text("Please edit the push target."))},LTSonly:1},j];h.push({type:"buttons",buttons:[{type:"cancel",label:"Cancel","function":function(){UI.navto("Push")}},{type:"save",label:"Save","function":function(){var c=i.params,d;for(d in c)c[d]===null&&delete c[d];if(Object.keys(c).length){var g="?",e=i.target.split("?");if(e.length>1){g="&";e=e[e.length-
+-1];e=e.split("&");for(d in e){var f=e[d].split("=")[0];f in c&&delete c[f]}}if(Object.keys(c).length){e=[];for(d in c)e.push(d+"="+c[d]);g=g+e.join("&");i.target=i.target+g}}c={};c[b=="auto"?"push_auto_add":"push_start"]=i;if(typeof a!="undefined"&&(a[0]!=i.stream||a[1]!=i.target))c.push_auto_remove=[a];mist.send(function(){UI.navto("Push")},c)}}]});c.append(UI.buildUI(h))}};mist.data.LTS?mist.send(function(a){(x=a.active_streams)||(x=[]);var a=[],b;for(b in x)x[b].indexOf("+")!=-1&&a.push(x[b].replace(/\+.*/,
+-"")+"+");x=x.concat(a);var c=0,d=0;for(b in mist.data.streams){x.push(b);if(mist.inputMatch(UI.findInput("Folder").source_match,mist.data.streams[b].source)){x.push(b+"+");mist.send(function(a,b){var g=b.stream,e;for(e in a.browse.files)for(var f in mist.data.capabilities.inputs)f.indexOf("Buffer")>=0||(f.indexOf("Folder")>=0||f.indexOf("Buffer.exe")>=0||f.indexOf("Folder.exe")>=0)||mist.inputMatch(mist.data.capabilities.inputs[f].source_match,"/"+a.browse.files[e])&&x.push(g+"+"+a.browse.files[e]);
+-d++;if(c==d){x=x.filter(function(a,b,c){return c.lastIndexOf(a)===b}).sort();ia()}},{browse:mist.data.streams[b].source},{stream:b});c++}}if(c==d){x=x.filter(function(a,b,c){return c.lastIndexOf(a)===b}).sort();ia()}},{active_streams:1}):(x=Object.keys(mist.data.streams),ia());break;case "Triggers":if(!("triggers"in mist.data.config)||!mist.data.config.triggers)mist.data.config.triggers={};var C=$("<tbody>"),Ua=$("<table>").html($("<thead>").html($("<tr>").html($("<th>").text("Trigger on").attr("data-sort-type",
+-"string").addClass("sorting-asc")).append($("<th>").text("Applies to").attr("data-sort-type","string")).append($("<th>").text("Handler").attr("data-sort-type","string")).append($("<th>")))).append(C);c.append(UI.buildUI([{type:"help",help:"Triggers are a way to react to events that occur inside MistServer. These allow you to block specific users, redirect streams, keep tabs on what is being pushed where, etcetera. For full documentation, please refer to the developer documentation section on the MistServer website."}])).append($("<button>").text("New trigger").click(function(){UI.navto("Edit Trigger")})).append(Ua);
+-Ua.stupidtable();var ta=mist.data.config.triggers;for(r in ta)for(var Va in ta[r]){var ua=triggerRewrite(ta[r][Va]);C.append($("<tr>").attr("data-index",r+","+Va).append($("<td>").text(r)).append($("<td>").text("streams"in ua?ua.streams.join(", "):"")).append($("<td>").text(ua.handler)).append($("<td>").html($("<button>").text("Edit").click(function(){UI.navto("Edit Trigger",$(this).closest("tr").attr("data-index"))})).append($("<button>").text("Delete").click(function(){var a=$(this).closest("tr").attr("data-index").split(",");
++pointer:{main:i,index:"stream"},validate:["required",function(a){a=a.split("+");a=a[0];return a in mist.data.streams?false:{msg:"'"+a+"' is not a stream name.",classes:["orange"],"break":false}}],datalist:x},{label:"Target",type:"str",help:"Where the stream will be pushed to.<br> Valid formats: <ul> <li>"+e.join("</li><li>")+"</li> </ul> Valid text replacements: <ul> <li>$stream - inserts the stream name used to push to MistServer</li> <li>$day - inserts the current day number</li><li>$month - inserts the current month number</li> <li>$year - inserts the current year number</li><li>$hour - inserts the hour timestamp when stream was received</li> <li>$minute - inserts the minute timestamp the stream was received</li> <li>$seconds - inserts the seconds timestamp when the stream was received</li> <li>$datetime - inserts $year.$month.$day.$hour.$minute.$seconds timestamp when the stream was received</li> </ul> Valid URL parameters: <ul> <li>recstart=123 - media timestamp in milisseconds where the push should start</li> <li>recstop=456 - media timestamp in miliseconds where the push should stop</li> <li>recstartunix=150000000 - unix time in seconds where the push should start. This will override the recstart parameter.</li> <li>recstopunix=150000000 - unix time in seconds where the push should stop. This will override the recstop parameter.</li> </ul>",
++pointer:{main:i,index:"target"},validate:["required",function(a){for(var b in e)if(mist.inputMatch(e[b],a))return false;return{msg:"Does not match a valid target.<br>Valid formats:<ul><li>"+e.join("</li><li>")+"</li></ul>",classes:["red"]}}],"function":function(){var a=false;for(connector in g)for(var b in g[connector])if(mist.inputMatch(g[connector][b],$(this).getval())){a=connector;break}if(a){j.html($("<h3>").text(mist.data.capabilities.connectors[a].friendly));j.append(UI.buildUI(mist.convertBuildOptions({desc:mist.data.capabilities.connectors[a].desc,
++optional:mist.data.capabilities.connectors[a].push_parameters},i.params)))}else j.html($("<h4>").addClass("red").text("Unrecognized target.")).append($("<span>").text("Please edit the push target."))}},j];h.push({type:"buttons",buttons:[{type:"cancel",label:"Cancel","function":function(){UI.navto("Push")}},{type:"save",label:"Save","function":function(){var c=i.params,d;for(d in c)c[d]===null&&delete c[d];if(Object.keys(c).length){var f="?",e=i.target.split("?");if(e.length>1){f="&";e=e[e.length-
++1];e=e.split("&");for(d in e){var g=e[d].split("=")[0];g in c&&delete c[g]}}if(Object.keys(c).length){e=[];for(d in c)e.push(d+"="+c[d]);f=f+e.join("&");i.target=i.target+f}}c={};c[b=="auto"?"push_auto_add":"push_start"]=i;if(typeof a!="undefined"&&(a[0]!=i.stream||a[1]!=i.target))c.push_auto_remove=[a];mist.send(function(){UI.navto("Push")},c)}}]});c.append(UI.buildUI(h))}};mist.data.LTS?mist.send(function(a){(x=a.active_streams)||(x=[]);var a=[],b;for(b in x)x[b].indexOf("+")!=-1&&a.push(x[b].replace(/\+.*/,
++"")+"+");x=x.concat(a);var c=0,d=0;for(b in mist.data.streams){x.push(b);if(mist.inputMatch(UI.findInput("Folder").source_match,mist.data.streams[b].source)){x.push(b+"+");mist.send(function(a,b){var f=b.stream,e;for(e in a.browse.files)for(var g in mist.data.capabilities.inputs)g.indexOf("Buffer")>=0||(g.indexOf("Folder")>=0||g.indexOf("Buffer.exe")>=0||g.indexOf("Folder.exe")>=0)||mist.inputMatch(mist.data.capabilities.inputs[g].source_match,"/"+a.browse.files[e])&&x.push(f+"+"+a.browse.files[e]);
++d++;if(c==d){x=x.filter(function(a,b,c){return c.lastIndexOf(a)===b}).sort();ha()}},{browse:mist.data.streams[b].source},{stream:b});c++}}if(c==d){x=x.filter(function(a,b,c){return c.lastIndexOf(a)===b}).sort();ha()}},{active_streams:1}):(x=Object.keys(mist.data.streams),ha());break;case "Triggers":if(!("triggers"in mist.data.config)||!mist.data.config.triggers)mist.data.config.triggers={};var D=$("<tbody>"),Ua=$("<table>").html($("<thead>").html($("<tr>").html($("<th>").text("Trigger on").attr("data-sort-type",
++"string").addClass("sorting-asc")).append($("<th>").text("Applies to").attr("data-sort-type","string")).append($("<th>").text("Handler").attr("data-sort-type","string")).append($("<th>")))).append(D);c.append(UI.buildUI([{type:"help",help:"Triggers are a way to react to events that occur inside MistServer. These allow you to block specific users, redirect streams, keep tabs on what is being pushed where, etcetera. For full documentation, please refer to the developer documentation section on the MistServer website."}])).append($("<button>").text("New trigger").click(function(){UI.navto("Edit Trigger")})).append(Ua);
++Ua.stupidtable();var sa=mist.data.config.triggers;for(r in sa)for(var Va in sa[r]){var ta=triggerRewrite(sa[r][Va]);D.append($("<tr>").attr("data-index",r+","+Va).append($("<td>").text(r)).append($("<td>").text("streams"in ta?ta.streams.join(", "):"")).append($("<td>").text(ta.handler)).append($("<td>").html($("<button>").text("Edit").click(function(){UI.navto("Edit Trigger",$(this).closest("tr").attr("data-index"))})).append($("<button>").text("Delete").click(function(){var a=$(this).closest("tr").attr("data-index").split(",");
+ if(confirm("Are you sure you want to delete this "+a[0]+" trigger?")){mist.data.config.triggers[a[0]].splice(a[1],1);mist.data.config.triggers[a[0]].length==0&&delete mist.data.config.triggers[a[0]];mist.send(function(){UI.navto("Triggers")},{config:mist.data.config})}}))))}break;case "Edit Trigger":if(!("triggers"in mist.data.config)||!mist.data.config.triggers)mist.data.config.triggers={};if("undefined"==typeof mist.data.capabilities){mist.send(function(){UI.navto(a)},{capabilities:!0});c.append("Loading..");
+-return}if(b)var b=b.split(","),ba=triggerRewrite(mist.data.config.triggers[b[0]][b[1]]),p={triggeron:b[0],appliesto:ba.streams,url:ba.handler,async:ba.sync,"default":ba["default"],params:ba.params};else c.html($("<h2>").text("New Trigger")),p={};var Wa=[];for(r in mist.data.capabilities.triggers)Wa.push([r,r+": "+mist.data.capabilities.triggers[r].when]);var va=$("<div>").addClass("desc"),Xa=$("<div>");c.append(UI.buildUI([{label:"Trigger on",pointer:{main:p,index:"triggeron"},help:"For what event this trigger should activate.",
+-type:"select",select:Wa,LTSonly:!0,validate:["required"],"function":function(){var a=$(this).getval(),b=mist.data.capabilities.triggers[a];va.html("");if(b){a=[$("<h4>").text("Trigger properties"),{type:"help",help:'The trigger "<i>'+a+'</i>" has the following properties:'},{type:"span",label:"Triggers",value:b.when,help:"When this trigger is activated"}];b.payload!=""&&a.push({label:"Payload",type:"textarea",value:b.payload,rows:b.payload.split("\n").length,readonly:true,clipboard:true,help:"The information this trigger sends to the handler."});
++return}if(b)var b=b.split(","),aa=triggerRewrite(mist.data.config.triggers[b[0]][b[1]]),p={triggeron:b[0],appliesto:aa.streams,url:aa.handler,async:aa.sync,"default":aa["default"],params:aa.params};else c.html($("<h2>").text("New Trigger")),p={};var Wa=[];for(r in mist.data.capabilities.triggers)Wa.push([r,r+": "+mist.data.capabilities.triggers[r].when]);var ua=$("<div>").addClass("desc"),Xa=$("<div>");c.append(UI.buildUI([{label:"Trigger on",pointer:{main:p,index:"triggeron"},help:"For what event this trigger should activate.",
++type:"select",select:Wa,validate:["required"],"function":function(){var a=$(this).getval(),b=mist.data.capabilities.triggers[a];ua.html("");if(b){a=[$("<h4>").text("Trigger properties"),{type:"help",help:'The trigger "<i>'+a+'</i>" has the following properties:'},{type:"span",label:"Triggers",value:b.when,help:"When this trigger is activated"}];b.payload!=""&&a.push({label:"Payload",type:"textarea",value:b.payload,rows:b.payload.split("\n").length,readonly:true,clipboard:true,help:"The information this trigger sends to the handler."});
+ a.push({type:"span",label:"Requires response",value:function(a){switch(a){case "ignored":return"No. The trigger will ignore the response of the handler.";case "always":return"Yes. The trigger needs a response to proceed.";case "when-blocking":return"The trigger needs a response to proceed if it is configured to be blocking.";default:return a}}(b.response),help:"Whether this trigger requires a response from the trigger handler"});a.push({type:"span",label:"Response action",value:b.response_action,
+-help:"What this trigger will do with its handler's response"});va.append(UI.buildUI(a));b.stream_specific?$("[name=appliesto]").closest(".UIelement").show():$("[name=appliesto]").setval([]).closest(".UIelement").hide();if(b.argument){$("[name=params]").closest(".UIelement").show();Xa.text(b.argument)}else $("[name=params]").setval("").closest(".UIelement").hide()}}},va,$("<h4>").text("Trigger settings"),{label:"Applies to",pointer:{main:p,index:"appliesto"},help:"For triggers that can apply to specific streams, this value decides what streams they are triggered for. (none checked = always triggered)",
+-type:"checklist",checklist:Object.keys(mist.data.streams),LTSonly:!0},$("<br>"),{label:"Handler (URL or executable)",help:"This can be either an HTTP URL or a full path to an executable.",pointer:{main:p,index:"url"},validate:["required"],type:"str",LTSonly:!0},{label:"Blocking",type:"checkbox",help:"If checked, pauses processing and uses the response of the handler. If the response does not start with 1, true, yes or cont, further processing is aborted. If unchecked, processing is never paused and the response is not checked.",
+-pointer:{main:p,index:"async"},LTSonly:!0},{label:"Parameters",type:"str",help:$("<div>").text("The extra data you want this trigger to use.").append(Xa),pointer:{main:p,index:"params"},LTSonly:!0},{label:"Default response",type:"str",help:"The default response in case the handler fails or is set to non-blocking.",placeholder:"true",pointer:{main:p,index:"default"},LTSonly:!0},{type:"buttons",buttons:[{type:"cancel",label:"Cancel","function":function(){UI.navto("Triggers")}},{type:"save",label:"Save",
+-"function":function(){b&&mist.data.config.triggers[b[0]].splice(b[1],1);var a={handler:p.url,sync:p.async?true:false,streams:typeof p.appliesto=="undefined"?[]:p.appliesto,params:p.params,"default":p["default"]};if(!("triggers"in mist.data.config))mist.data.config.triggers={};p.triggeron in mist.data.config.triggers||(mist.data.config.triggers[p.triggeron]=[]);mist.data.config.triggers[p.triggeron].push(a);mist.send(function(){UI.navto("Triggers")},{config:mist.data.config})}}]}]));$("[name=triggeron]").trigger("change");
+-break;case "Logs":var Ya=$("<button>").text("Refresh now").click(function(){$(this).text("Loading..");mist.send(function(){wa();Ya.text("Refresh now")})}).css("padding","0.2em 0.5em").css("flex-grow",0);c.append(UI.buildUI([{type:"help",help:"Here you have an overview of all edited settings within MistServer and possible warnings or errors MistServer has encountered. MistServer stores up to 100 logs at a time."},{label:"Refresh every",type:"select",select:[[10,"10 seconds"],[30,"30 seconds"],[60,
+-"minute"],[300,"5 minutes"]],value:30,"function":function(){UI.interval.clear();UI.interval.set(function(){mist.send(function(){wa()})},$(this).val()*1E3)},help:"How often the table below should be updated."},{label:"..or",type:"DOMfield",DOMfield:Ya,help:"Instantly refresh the table below."}]));c.append($("<button>").text("Purge logs").click(function(){mist.send(function(){mist.data.log=[];UI.navto("Logs")},{clearstatlogs:true})}));C=$("<tbody>").css("font-size","0.9em");c.append($("<table>").addClass("logs").append(C));
+-var bb=function(a){var b=$("<span>").text(a);switch(a){case "WARN":b.addClass("orange");break;case "ERROR":case "FAIL":b.addClass("red")}return b},wa=function(){var a=mist.data.log;if(a){a.length>=2&&a[0][0]<a[a.length-1][0]&&a.reverse();C.html("");for(var b in a){var c=$("<span>").addClass("content"),d=a[b][2].split("|"),g;for(g in d)c.append($("<span>").text(d[g]));C.append($("<tr>").html($("<td>").text(UI.format.dateTime(a[b][0],"long")).css("white-space","nowrap")).append($("<td>").html(bb(a[b][1])).css("text-align",
+-"center")).append($("<td>").html(c).css("text-align","left")))}}};wa();break;case "Statistics":var D=$("<span>").text("Loading..");c.append(D);var p={graph:"new"},z=mist.stored.get().graphs?$.extend(!0,{},mist.stored.get().graphs):{},ca={};for(r in mist.data.streams)ca[r]=!0;for(r in mist.data.active_streams)ca[mist.data.active_streams[r]]=!0;var ca=Object.keys(ca).sort(),xa=[];for(r in mist.data.config.protocols)xa.push(mist.data.config.protocols[r].connector);xa.sort();mist.send(function(){UI.plot.datatype.templates.cpuload.cores=
+-0;for(var a in mist.data.capabilities.cpu)UI.plot.datatype.templates.cpuload.cores=UI.plot.datatype.templates.cpuload.cores+mist.data.capabilities.cpu[a].cores;D.html(UI.buildUI([{type:"help",help:"Here you will find the MistServer stream statistics, you can select various categories yourself. All statistics are live: up to five minutes are saved."},$("<h3>").text("Select the data to display"),{label:"Add to",type:"select",select:[["new","New graph"]],pointer:{main:p,index:"graph"},classes:["graph_ids"],
+-"function":function(){if($(this).val()){var a=D.find(".graph_xaxis"),b=D.find(".graph_id");if($(this).val()=="new"){a.children("option").prop("disabled",false);b.setval("Graph "+(Object.keys(z).length+1)).closest("label").show()}else{var c=z[$(this).val()].xaxis;a.children("option").prop("disabled",true).filter('[value="'+c+'"]').prop("disabled",false);b.closest("label").hide()}a.children('option[value="'+a.val()+'"]:disabled').length&&a.val(a.children("option:enabled").first().val());a.trigger("change")}}},
+-{label:"Graph id",type:"str",pointer:{main:p,index:"id"},classes:["graph_id"],validate:[function(a){return a in z?{msg:"This graph id has already been used. Please enter something else.",classes:["red"]}:false}]},{label:"Axis type",type:"select",select:[["time","Time line"]],pointer:{main:p,index:"xaxis"},value:"time",classes:["graph_xaxis"],"function":function(){$s=D.find(".graph_datatype");switch($(this).getval()){case "coords":$s.children("option").prop("disabled",true).filter('[value="coords"]').prop("disabled",
+-false);break;case "time":$s.children("option").prop("disabled",false).filter('[value="coords"]').prop("disabled",true)}if(!$s.val()||$s.children('option[value="'+$s.val()+'"]:disabled').length){$s.val($s.children("option:enabled").first().val());$s.trigger("change")}}},{label:"Data type",type:"select",select:[["clients","Connections"],["upbps","Bandwidth (up)"],["downbps","Bandwidth (down)"],["cpuload","CPU use"],["memload","Memory load"],["coords","Client location"],["perc_lost","Lost packages"],
+-["perc_retrans","Re-transmitted packages"]],pointer:{main:p,index:"datatype"},classes:["graph_datatype"],"function":function(){$s=D.find(".graph_origin");switch($(this).getval()){case "cpuload":case "memload":$s.find("input[type=radio]").not('[value="total"]').prop("disabled",true);$s.find('input[type=radio][value="total"]').prop("checked",true);break;default:$s.find("input[type=radio]").prop("disabled",false)}}},{label:"Data origin",type:"radioselect",radioselect:[["total","All"],["stream","The stream:",
+-ca],["protocol","The protocol:",xa]],pointer:{main:p,index:"origin"},value:["total"],classes:["graph_origin"]},{type:"buttons",buttons:[{label:"Add data set",type:"save","function":function(){var a;if(p.graph=="new"){a=UI.plot.addGraph(p,b);z[a.id]=a;D.find("input.graph_id").val("");D.find("select.graph_ids").append($("<option>").text(a.id)).val(a.id).trigger("change")}else a=z[p.graph];var c=UI.plot.datatype.getOptions({datatype:p.datatype,origin:p.origin});a.datasets.push(c);UI.plot.save(a);UI.plot.go(z)}}]}]));
+-var b=$("<div>").addClass("graph_container");c.append(b);var d=D.find("select.graph_ids");for(a in z){var g=UI.plot.addGraph(z[a],b);d.append($("<option>").text(g.id)).val(g.id);var e=[],f;for(f in z[a].datasets){var h=UI.plot.datatype.getOptions({datatype:z[a].datasets[f].datatype,origin:z[a].datasets[f].origin});e.push(h)}g.datasets=e;z[g.id]=g}d.trigger("change");UI.plot.go(z);UI.interval.set(function(){UI.plot.go(z)},1E4)},{active_streams:!0,capabilities:!0});break;case "Server Stats":if("undefined"==
+-typeof mist.data.capabilities){mist.send(function(){UI.navto(a)},{capabilities:!0});c.append("Loading..");return}var ya=$("<table>"),L=$("<table>"),Za={vheader:"CPUs",labels:["Model","Processor speed","Amount of cores","Amount of threads"],content:[]};for(r in mist.data.capabilities.cpu){var ja=mist.data.capabilities.cpu[r];Za.content.push({header:"CPU #"+(Number(r)+1),body:[ja.model,UI.format.addUnit(UI.format.number(ja.mhz),"MHz"),ja.cores,ja.threads]})}var cb=UI.buildVheaderTable(Za),$a=function(){var a=
+-mist.data.capabilities.mem,b=mist.data.capabilities.load,a={vheader:"Memory",labels:["Used","Cached","Available","Total"],content:[{header:"Physical memory",body:[UI.format.bytes(a.used*1048576)+" ("+UI.format.addUnit(b.memory,"%")+")",UI.format.bytes(a.cached*1048576),UI.format.bytes(a.free*1048576),UI.format.bytes(a.total*1048576)]},{header:"Swap memory",body:[UI.format.bytes((a.swaptotal-a.swapfree)*1048576),UI.format.addUnit("","N/A"),UI.format.bytes(a.swapfree*1048576),UI.format.bytes(a.swaptotal*
+-1048576)]}]},a=UI.buildVheaderTable(a);ya.replaceWith(a);ya=a;b={vheader:"Load average",labels:["CPU use","1 minute","5 minutes","15 minutes"],content:[{header:"&nbsp;",body:[UI.format.addUnit(UI.format.number(mist.data.capabilities.cpu_use/10),"%"),UI.format.number(b.one/100),UI.format.number(b.five/100),UI.format.number(b.fifteen/100)]}]};b=UI.buildVheaderTable(b);L.replaceWith(b);L=b};$a();c.append(UI.buildUI([{type:"help",help:"You can find general server statistics here. Note that memory and CPU usage is for your entire machine, not just MistServer."}])).append($("<table>").css("width",
+-"auto").addClass("nolay").append($("<tr>").append($("<td>").append(ya)).append($("<td>").append(L))).append($("<tr>").append($("<td>").append(cb).attr("colspan",2))));UI.interval.set(function(){mist.send(function(){$a()},{capabilities:true})},3E4);break;case "Email for Help":var E=$.extend({},mist.data);delete E.statistics;delete E.totals;delete E.clients;delete E.capabilities;E=JSON.stringify(E);E="Version: "+mist.data.config.version+"\n\nConfig:\n"+E;p={};c.append(UI.buildUI([{type:"help",help:"You can use this form to email MistServer support if you're having difficulties.<br>A copy of your server config file will automatically be included."},
+-{type:"str",label:"Your name",validate:["required"],pointer:{main:p,index:"name"},value:mist.user.name},{type:"email",label:"Your email address",validate:["required"],pointer:{main:p,index:"email"}},{type:"hidden",value:"Integrated Help",pointer:{main:p,index:"subject"}},{type:"hidden",value:"-",pointer:{main:p,index:"company"}},{type:"textarea",rows:20,label:"Your message",validate:["required"],pointer:{main:p,index:"message"}},{type:"textarea",rows:20,label:"Your config file",readonly:!0,value:E,
+-pointer:{main:p,index:"configfile"}},{type:"buttons",buttons:[{type:"save",label:"Send","function":function(a){$(a).text("Sending..");$.ajax({type:"POST",url:"https://mistserver.org/contact?skin=plain",data:p,success:function(a){a=$("<span>").html(a);a.find("script").remove();c.html(a[0].innerHTML)}})}}]}]));break;case "Disconnect":mist.user.password="";delete mist.user.authstring;delete mist.user.loggedin;sessionStorage.removeItem("mistLogin");UI.navto("Login");break;default:c.append($("<p>").text("This tab does not exist."))}c.find(".field").filter(function(){var a=
+-$(this).getval();return a==""||a==null?true:false}).each(function(){var a=[];$(this).is("input, select, textarea")?a.push($(this)):a=$(this).find("input, select, textarea");if(a.length){$(a[0]).focus();return false}})}};"origin"in location||(location.origin=location.protocol+"//");var host;host="file://"==location.origin?"http://localhost:4242/api":location.origin+location.pathname.replace(/\/+$/,"")+"/api";
++help:"What this trigger will do with its handler's response"});ua.append(UI.buildUI(a));b.stream_specific?$("[name=appliesto]").closest(".UIelement").show():$("[name=appliesto]").setval([]).closest(".UIelement").hide();if(b.argument){$("[name=params]").closest(".UIelement").show();Xa.text(b.argument)}else $("[name=params]").setval("").closest(".UIelement").hide()}}},ua,$("<h4>").text("Trigger settings"),{label:"Applies to",pointer:{main:p,index:"appliesto"},help:"For triggers that can apply to specific streams, this value decides what streams they are triggered for. (none checked = always triggered)",
++type:"checklist",checklist:Object.keys(mist.data.streams)},$("<br>"),{label:"Handler (URL or executable)",help:"This can be either an HTTP URL or a full path to an executable.",pointer:{main:p,index:"url"},validate:["required"],type:"str"},{label:"Blocking",type:"checkbox",help:"If checked, pauses processing and uses the response of the handler. If the response does not start with 1, true, yes or cont, further processing is aborted. If unchecked, processing is never paused and the response is not checked.",
++pointer:{main:p,index:"async"}},{label:"Parameters",type:"str",help:$("<div>").text("The extra data you want this trigger to use.").append(Xa),pointer:{main:p,index:"params"}},{label:"Default response",type:"str",help:"The default response in case the handler fails or is set to non-blocking.",placeholder:"true",pointer:{main:p,index:"default"}},{type:"buttons",buttons:[{type:"cancel",label:"Cancel","function":function(){UI.navto("Triggers")}},{type:"save",label:"Save","function":function(){b&&mist.data.config.triggers[b[0]].splice(b[1],
++1);var a={handler:p.url,sync:p.async?true:false,streams:typeof p.appliesto=="undefined"?[]:p.appliesto,params:p.params,"default":p["default"]};if(!("triggers"in mist.data.config))mist.data.config.triggers={};p.triggeron in mist.data.config.triggers||(mist.data.config.triggers[p.triggeron]=[]);mist.data.config.triggers[p.triggeron].push(a);mist.send(function(){UI.navto("Triggers")},{config:mist.data.config})}}]}]));$("[name=triggeron]").trigger("change");break;case "Logs":var Ya=$("<button>").text("Refresh now").click(function(){$(this).text("Loading..");
++mist.send(function(){va();Ya.text("Refresh now")})}).css("padding","0.2em 0.5em").css("flex-grow",0);c.append(UI.buildUI([{type:"help",help:"Here you have an overview of all edited settings within MistServer and possible warnings or errors MistServer has encountered. MistServer stores up to 100 logs at a time."},{label:"Refresh every",type:"select",select:[[10,"10 seconds"],[30,"30 seconds"],[60,"minute"],[300,"5 minutes"]],value:30,"function":function(){UI.interval.clear();UI.interval.set(function(){mist.send(function(){va()})},
++$(this).val()*1E3)},help:"How often the table below should be updated."},{label:"..or",type:"DOMfield",DOMfield:Ya,help:"Instantly refresh the table below."}]));c.append($("<button>").text("Purge logs").click(function(){mist.send(function(){mist.data.log=[];UI.navto("Logs")},{clearstatlogs:true})}));D=$("<tbody>").css("font-size","0.9em");c.append($("<table>").addClass("logs").append(D));var bb=function(a){var b=$("<span>").text(a);switch(a){case "WARN":b.addClass("orange");break;case "ERROR":case "FAIL":b.addClass("red")}return b},
++va=function(){var a=mist.data.log;if(a){a.length>=2&&a[0][0]<a[a.length-1][0]&&a.reverse();D.html("");for(var b in a){var c=$("<span>").addClass("content"),d=a[b][2].split("|"),f;for(f in d)c.append($("<span>").text(d[f]));D.append($("<tr>").html($("<td>").text(UI.format.dateTime(a[b][0],"long")).css("white-space","nowrap")).append($("<td>").html(bb(a[b][1])).css("text-align","center")).append($("<td>").html(c).css("text-align","left")))}}};va();break;case "Statistics":var G=$("<span>").text("Loading..");
++c.append(G);var p={graph:"new"},z=mist.stored.get().graphs?$.extend(!0,{},mist.stored.get().graphs):{},ba={};for(r in mist.data.streams)ba[r]=!0;for(r in mist.data.active_streams)ba[mist.data.active_streams[r]]=!0;var ba=Object.keys(ba).sort(),wa=[];for(r in mist.data.config.protocols)wa.push(mist.data.config.protocols[r].connector);wa.sort();mist.send(function(){UI.plot.datatype.templates.cpuload.cores=0;for(var a in mist.data.capabilities.cpu)UI.plot.datatype.templates.cpuload.cores=UI.plot.datatype.templates.cpuload.cores+
++mist.data.capabilities.cpu[a].cores;G.html(UI.buildUI([{type:"help",help:"Here you will find the MistServer stream statistics, you can select various categories yourself. All statistics are live: up to five minutes are saved."},$("<h3>").text("Select the data to display"),{label:"Add to",type:"select",select:[["new","New graph"]],pointer:{main:p,index:"graph"},classes:["graph_ids"],"function":function(){if($(this).val()){var a=G.find(".graph_xaxis"),b=G.find(".graph_id");if($(this).val()=="new"){a.children("option").prop("disabled",
++false);b.setval("Graph "+(Object.keys(z).length+1)).closest("label").show()}else{var c=z[$(this).val()].xaxis;a.children("option").prop("disabled",true).filter('[value="'+c+'"]').prop("disabled",false);b.closest("label").hide()}a.children('option[value="'+a.val()+'"]:disabled').length&&a.val(a.children("option:enabled").first().val());a.trigger("change")}}},{label:"Graph id",type:"str",pointer:{main:p,index:"id"},classes:["graph_id"],validate:[function(a){return a in z?{msg:"This graph id has already been used. Please enter something else.",
++classes:["red"]}:false}]},{label:"Axis type",type:"select",select:[["time","Time line"]],pointer:{main:p,index:"xaxis"},value:"time",classes:["graph_xaxis"],"function":function(){$s=G.find(".graph_datatype");switch($(this).getval()){case "coords":$s.children("option").prop("disabled",true).filter('[value="coords"]').prop("disabled",false);break;case "time":$s.children("option").prop("disabled",false).filter('[value="coords"]').prop("disabled",true)}if(!$s.val()||$s.children('option[value="'+$s.val()+
++'"]:disabled').length){$s.val($s.children("option:enabled").first().val());$s.trigger("change")}}},{label:"Data type",type:"select",select:[["clients","Connections"],["upbps","Bandwidth (up)"],["downbps","Bandwidth (down)"],["cpuload","CPU use"],["memload","Memory load"],["coords","Client location"],["perc_lost","Lost packages"],["perc_retrans","Re-transmitted packages"]],pointer:{main:p,index:"datatype"},classes:["graph_datatype"],"function":function(){$s=G.find(".graph_origin");switch($(this).getval()){case "cpuload":case "memload":$s.find("input[type=radio]").not('[value="total"]').prop("disabled",
++true);$s.find('input[type=radio][value="total"]').prop("checked",true);break;default:$s.find("input[type=radio]").prop("disabled",false)}}},{label:"Data origin",type:"radioselect",radioselect:[["total","All"],["stream","The stream:",ba],["protocol","The protocol:",wa]],pointer:{main:p,index:"origin"},value:["total"],classes:["graph_origin"]},{type:"buttons",buttons:[{label:"Add data set",type:"save","function":function(){var a;if(p.graph=="new"){a=UI.plot.addGraph(p,b);z[a.id]=a;G.find("input.graph_id").val("");
++G.find("select.graph_ids").append($("<option>").text(a.id)).val(a.id).trigger("change")}else a=z[p.graph];var c=UI.plot.datatype.getOptions({datatype:p.datatype,origin:p.origin});a.datasets.push(c);UI.plot.save(a);UI.plot.go(z)}}]}]));var b=$("<div>").addClass("graph_container");c.append(b);var d=G.find("select.graph_ids");for(a in z){var f=UI.plot.addGraph(z[a],b);d.append($("<option>").text(f.id)).val(f.id);var e=[],g;for(g in z[a].datasets){var h=UI.plot.datatype.getOptions({datatype:z[a].datasets[g].datatype,
++origin:z[a].datasets[g].origin});e.push(h)}f.datasets=e;z[f.id]=f}d.trigger("change");UI.plot.go(z);UI.interval.set(function(){UI.plot.go(z)},1E4)},{active_streams:!0,capabilities:!0});break;case "Server Stats":if("undefined"==typeof mist.data.capabilities){mist.send(function(){UI.navto(a)},{capabilities:!0});c.append("Loading..");return}var xa=$("<table>"),M=$("<table>"),Za={vheader:"CPUs",labels:["Model","Processor speed","Amount of cores","Amount of threads"],content:[]};for(r in mist.data.capabilities.cpu){var ia=
++mist.data.capabilities.cpu[r];Za.content.push({header:"CPU #"+(Number(r)+1),body:[ia.model,UI.format.addUnit(UI.format.number(ia.mhz),"MHz"),ia.cores,ia.threads]})}var cb=UI.buildVheaderTable(Za),$a=function(){var a=mist.data.capabilities.mem,b=mist.data.capabilities.load,a={vheader:"Memory",labels:["Used","Cached","Available","Total"],content:[{header:"Physical memory",body:[UI.format.bytes(a.used*1048576)+" ("+UI.format.addUnit(b.memory,"%")+")",UI.format.bytes(a.cached*1048576),UI.format.bytes(a.free*
++1048576),UI.format.bytes(a.total*1048576)]},{header:"Swap memory",body:[UI.format.bytes((a.swaptotal-a.swapfree)*1048576),UI.format.addUnit("","N/A"),UI.format.bytes(a.swapfree*1048576),UI.format.bytes(a.swaptotal*1048576)]}]},a=UI.buildVheaderTable(a);xa.replaceWith(a);xa=a;b={vheader:"Load average",labels:["CPU use","1 minute","5 minutes","15 minutes"],content:[{header:"&nbsp;",body:[UI.format.addUnit(UI.format.number(mist.data.capabilities.cpu_use/10),"%"),UI.format.number(b.one/100),UI.format.number(b.five/
++100),UI.format.number(b.fifteen/100)]}]};b=UI.buildVheaderTable(b);M.replaceWith(b);M=b};$a();c.append(UI.buildUI([{type:"help",help:"You can find general server statistics here. Note that memory and CPU usage is for your entire machine, not just MistServer."}])).append($("<table>").css("width","auto").addClass("nolay").append($("<tr>").append($("<td>").append(xa)).append($("<td>").append(M))).append($("<tr>").append($("<td>").append(cb).attr("colspan",2))));UI.interval.set(function(){mist.send(function(){$a()},
++{capabilities:true})},3E4);break;case "Email for Help":var H=$.extend({},mist.data);delete H.statistics;delete H.totals;delete H.clients;delete H.capabilities;H=JSON.stringify(H);H="Version: "+mist.data.config.version+"\n\nConfig:\n"+H;p={};c.append(UI.buildUI([{type:"help",help:"You can use this form to email MistServer support if you're having difficulties.<br>A copy of your server config file will automatically be included."},{type:"str",label:"Your name",validate:["required"],pointer:{main:p,
++index:"name"},value:mist.user.name},{type:"email",label:"Your email address",validate:["required"],pointer:{main:p,index:"email"}},{type:"hidden",value:"Integrated Help",pointer:{main:p,index:"subject"}},{type:"hidden",value:"-",pointer:{main:p,index:"company"}},{type:"textarea",rows:20,label:"Your message",validate:["required"],pointer:{main:p,index:"message"}},{type:"textarea",rows:20,label:"Your config file",readonly:!0,value:H,pointer:{main:p,index:"configfile"}},{type:"buttons",buttons:[{type:"save",
++label:"Send","function":function(a){$(a).text("Sending..");$.ajax({type:"POST",url:"https://mistserver.org/contact?skin=plain",data:p,success:function(a){a=$("<span>").html(a);a.find("script").remove();c.html(a[0].innerHTML)}})}}]}]));break;case "Disconnect":mist.user.password="";delete mist.user.authstring;delete mist.user.loggedin;sessionStorage.removeItem("mistLogin");UI.navto("Login");break;default:c.append($("<p>").text("This tab does not exist."))}c.find(".field").filter(function(){var a=$(this).getval();
++return a==""||a==null?true:false}).each(function(){var a=[];$(this).is("input, select, textarea")?a.push($(this)):a=$(this).find("input, select, textarea");if(a.length){$(a[0]).focus();return false}})}};"origin"in location||(location.origin=location.protocol+"//");var host;host="file://"==location.origin?"http://localhost:4242/api":location.origin+location.pathname.replace(/\/+$/,"")+"/api";
+ var mist={data:{},user:{name:"",password:"",host:host},send:function(a,b,c){var b=b||{},c=c||{},c=$.extend(true,{timeOut:3E4,sendData:b},c),d={authorize:{password:mist.user.authstring?MD5(mist.user.password+mist.user.authstring):"",username:mist.user.name}};$.extend(true,d,b);log("Send",$.extend(true,{},b));var e={url:mist.user.host,type:"POST",data:{command:JSON.stringify(d)},dataType:"jsonp",crossDomain:true,timeout:c.timeout*1E3,async:true,error:function(d,e,l){console.warn("connection failed :(",
+ l);delete mist.user.loggedin;if(!c.hide){switch(e){case "timeout":e=$("<i>").text("The connection timed out. ");break;case "abort":e=$("<i>").text("The connection was aborted. ");break;default:e=$("<i>").text(e+". ").css("text-transform","capitalize")}$("#message").addClass("red").text("An error occurred while attempting to communicate with MistServer:").append($("<br>")).append($("<span>").text(e)).append($("<a>").text("Send server request again").click(function(){mist.send(a,b,c)}))}UI.navto("Login")},
+-success:function(e){log("Receive",$.extend(true,{},e),"as reply to",c.sendData);delete mist.user.loggedin;switch(e.authorize.status){case "OK":if("streams"in e)if(e.streams)if("incomplete list"in e.streams){delete e.streams["incomplete list"];$.extend(mist.data.streams,e.streams)}else mist.data.streams=e.streams;else mist.data.streams={};var f=$.extend({},e),l=["config","capabilities","ui_settings","LTS","active_streams","browse","log","totals","bandwidth"],o;for(o in f)l.indexOf(o)==-1&&delete f[o];
+-if("bandwidth"in d&&!("bandwidth"in e))f.bandwidth=null;$.extend(mist.data,f);mist.user.loggedin=true;UI.elements.connection.status.text("Connected").removeClass("red").addClass("green");UI.elements.connection.user_and_host.text(mist.user.name+" @ "+mist.user.host);UI.elements.connection.msg.removeClass("red").text("Last communication with the server at "+UI.format.time((new Date).getTime()/1E3));e.LTS&&UI.elements.menu.find(".LTSonly").removeClass("LTSonly");if(e.log){f=e.log[e.log.length-1];UI.elements.connection.msg.append($("<br>")).append($("<span>").text("Last log entry: "+
+-UI.format.time(f[0])+" ["+f[1]+"] "+f[2]))}if("totals"in e){f=function(a,b,c){var d;d=function(){for(var a in c.fields)e[c.fields[a]].push([m,0])};var e={},f;for(f in c.fields)e[c.fields[f]]=[];var l=0,m;if(c.data){if(c.start>mist.data.config.time-600){m=(mist.data.config.time-600)*1E3;d();m=c.start*1E3;d()}else m=c.start*1E3;for(f in c.data){if(f==0){m=c.start*1E3;var o=0}else{m=m+c.interval[o][1]*1E3;c.interval[o][0]--;if(c.interval[o][0]<=0){o++;o<c.interval.length-1&&(l=l+2)}}if(l%2==1){d();l--}for(var t in c.data[f])e[c.fields[t]].push([m,
+-c.data[f][t]]);if(l){d();l--}}if(mist.data.config.time-c.end>20){d();m=(mist.data.config.time-15)*1E3;d()}}else{m=(mist.data.config.time-600)*1E3;d();m=(mist.data.config.time-15)*1E3;d()}d=e;stream=a?a.join(" "):"all_streams";protocol=b?b.join("_"):"all_protocols";stream in mist.data.totals||(mist.data.totals[stream]={});protocol in mist.data.totals[stream]||(mist.data.totals[stream][protocol]={});$.extend(mist.data.totals[stream][protocol],d)};mist.data.totals={};if("fields"in e.totals)f(b.totals.streams,
+-b.totals.protocols,e.totals);else for(o in e.totals)f(b.totals[o].streams,b.totals[o].protocols,e.totals[o])}a&&a(e,c);break;case "CHALL":if(e.authorize.challenge==mist.user.authstring){mist.user.password!=""&&UI.elements.connection.msg.text("The credentials you provided are incorrect.").addClass("red");UI.navto("Login")}else if(mist.user.password=="")UI.navto("Login");else{mist.user.authstring=e.authorize.challenge;mist.send(a,b,c);sessionStorage.setItem("mistLogin",JSON.stringify({host:mist.user.host,
++success:function(e){log("Receive",$.extend(true,{},e),"as reply to",c.sendData);delete mist.user.loggedin;switch(e.authorize.status){case "OK":if("streams"in e)if(e.streams)if("incomplete list"in e.streams){delete e.streams["incomplete list"];$.extend(mist.data.streams,e.streams)}else mist.data.streams=e.streams;else mist.data.streams={};var h=$.extend({},e),l=["config","capabilities","ui_settings","LTS","active_streams","browse","log","totals","bandwidth"],m;for(m in h)l.indexOf(m)==-1&&delete h[m];
++if("bandwidth"in d&&!("bandwidth"in e))h.bandwidth=null;$.extend(mist.data,h);mist.user.loggedin=true;UI.elements.connection.status.text("Connected").removeClass("red").addClass("green");UI.elements.connection.user_and_host.text(mist.user.name+" @ "+mist.user.host);UI.elements.connection.msg.removeClass("red").text("Last communication with the server at "+UI.format.time((new Date).getTime()/1E3));if(e.log){h=e.log[e.log.length-1];UI.elements.connection.msg.append($("<br>")).append($("<span>").text("Last log entry: "+
++UI.format.time(h[0])+" ["+h[1]+"] "+h[2]))}if("totals"in e){h=function(a,b,c){var d;d=function(){for(var a in c.fields)e[c.fields[a]].push([m,0])};var e={},h;for(h in c.fields)e[c.fields[h]]=[];var l=0,m;if(c.data){if(c.start>mist.data.config.time-600){m=(mist.data.config.time-600)*1E3;d();m=c.start*1E3;d()}else m=c.start*1E3;for(h in c.data){if(h==0){m=c.start*1E3;var n=0}else{m=m+c.interval[n][1]*1E3;c.interval[n][0]--;if(c.interval[n][0]<=0){n++;n<c.interval.length-1&&(l=l+2)}}if(l%2==1){d();l--}for(var t in c.data[h])e[c.fields[t]].push([m,
++c.data[h][t]]);if(l){d();l--}}if(mist.data.config.time-c.end>20){d();m=(mist.data.config.time-15)*1E3;d()}}else{m=(mist.data.config.time-600)*1E3;d();m=(mist.data.config.time-15)*1E3;d()}d=e;stream=a?a.join(" "):"all_streams";protocol=b?b.join("_"):"all_protocols";stream in mist.data.totals||(mist.data.totals[stream]={});protocol in mist.data.totals[stream]||(mist.data.totals[stream][protocol]={});$.extend(mist.data.totals[stream][protocol],d)};mist.data.totals={};if("fields"in e.totals)h(b.totals.streams,
++b.totals.protocols,e.totals);else for(m in e.totals)h(b.totals[m].streams,b.totals[m].protocols,e.totals[m])}a&&a(e,c);break;case "CHALL":if(e.authorize.challenge==mist.user.authstring){mist.user.password!=""&&UI.elements.connection.msg.text("The credentials you provided are incorrect.").addClass("red");UI.navto("Login")}else if(mist.user.password=="")UI.navto("Login");else{mist.user.authstring=e.authorize.challenge;mist.send(a,b,c);sessionStorage.setItem("mistLogin",JSON.stringify({host:mist.user.host,
+ name:mist.user.name,password:mist.user.password}))}break;case "NOACC":UI.navto("Create a new account");break;case "ACC_MADE":delete b.authorize;mist.send(a,b,c);break;default:UI.navto("Login")}}};c.hide||UI.elements.connection.msg.removeClass("red").text("Data sent, waiting for a reply..").append($("<br>")).append($("<a>").text("Cancel request").click(function(){l.abort()}));var l=$.ajax(e)},inputMatch:function(a,b){if(typeof a=="undefined")return false;typeof a=="string"&&(a=[a]);for(var c in a){var d=
+-a[c].replace(/[^\w\s]/g,"\\$&"),d=d.replace(/\\\*/g,".*");if(RegExp("^(?:[a-zA-Z]:)?"+d+"(?:\\?[^\\?]*)?$","i").test(b))return true}return false},convertBuildOptions:function(a,b){function c(a,c,d){var f={label:UI.format.capital(d.name?d.name:c),pointer:{main:b,index:c},validate:[]};e[a]=="required"&&(!("default"in d)||d["default"]=="")&&f.validate.push("required");if("default"in d){f.placeholder=d["default"];if(d.type=="select")for(var j in d.select)if(d.select[j][0]==d["default"]){f.placeholder=
+-d.select[j][1];break}}if("help"in d)f.help=d.help;if("unit"in d)f.unit=d.unit;if("placeholder"in d)f.placeholder=d.placeholder;if("type"in d)switch(d.type){case "int":f.type="int";if("max"in d)f.max=d.max;if("min"in d)f.min=d.min;break;case "uint":f.type="int";f.min=0;if("max"in d)f.max=d.max;if("min"in d)f.min=Math.max(f.min,d.min);break;case "json":case "debug":case "inputlist":f.type=d.type;break;case "radioselect":f.type="radioselect";f.radioselect=d.radioselect;break;case "select":f.type="select";
++a[c].replace(/[^\w\s]/g,"\\$&"),d=d.replace(/\\\*/g,".*");if(RegExp("^(?:[a-zA-Z]:)?"+d+"(?:\\?[^\\?]*)?$","i").test(b))return true}return false},convertBuildOptions:function(a,b){function c(a,c,d){var f={label:UI.format.capital(d.name?d.name:c),pointer:{main:b,index:c},validate:[]};e[a]=="required"&&(!("default"in d)||d["default"]=="")&&f.validate.push("required");if("default"in d){f.placeholder=d["default"];if(d.type=="select")for(var h in d.select)if(d.select[h][0]==d["default"]){f.placeholder=
++d.select[h][1];break}}if("help"in d)f.help=d.help;if("unit"in d)f.unit=d.unit;if("placeholder"in d)f.placeholder=d.placeholder;if("type"in d)switch(d.type){case "int":f.type="int";if("max"in d)f.max=d.max;if("min"in d)f.min=d.min;break;case "uint":f.type="int";f.min=0;if("max"in d)f.max=d.max;if("min"in d)f.min=Math.max(f.min,d.min);break;case "json":case "debug":case "inputlist":f.type=d.type;break;case "radioselect":f.type="radioselect";f.radioselect=d.radioselect;break;case "select":f.type="select";
+ f.select=d.select.slice(0);f.validate.indexOf("required")>=0&&f.select.unshift(["","placeholder"in f?"Default ("+f.placeholder+")":""]);break;case "sublist":f.type="sublist";f.saveas={};f.itemLabel=d.itemLabel;f.sublist=mist.convertBuildOptions(d,f.saveas);break;default:f.type="str"}else f.type="checkbox";"influences"in d&&(f["function"]=function(){var a=$(this).closest(".UIelement"),b=a.find("style");if(b.length)b=b[0];else{b=$("<style>").addClass("dependencies")[0];a.append(b)}b.innerHTML=".UIelement[data-dependent-"+
+ c+"]:not([data-dependent-"+c+'~="'+$(this).getval()+'"]) { display: none; }\n';$(b).data("content",b.innerHTML);$("style.dependencies.hidden").each(function(){$(this).html($(this).data("content")).removeClass("hidden")});$(".UIelement:not(:visible) style.dependencies:not(.hidden)").each(function(){$(this).addClass("hidden");$(this).html("")})});if("dependent"in d)f.dependent=d.dependent;if("value"in d)f.value=d.value;if("validate"in d)f.validate=f.validate.concat(d.validate);return f}var d=[],e=["required",
+-"optional"];"desc"in a&&d.push({type:"help",help:a.desc});for(var l in e)if(a[e[l]]){d.push($("<h4>").text(UI.format.capital(e[l])+" parameters"));var m=Object.keys(a[e[l]]);"sort"in a&&m.sort(function(b,c){return(""+a[e[l]][b][a.sort]).localeCompare(a[e[l]][c][a.sort])});for(var f in m){var t=m[f],o=a[e[l]][t];if(Array.isArray(o))for(var k in o)d.push(c(l,t,o[k]));else d.push(c(l,t,o))}}return d},stored:{get:function(){return mist.data.ui_settings||{}},set:function(a,b){var c=this.get();c[a]=b;mist.send(function(){},
++"optional"];"desc"in a&&d.push({type:"help",help:a.desc});for(var l in e)if(a[e[l]]){d.push($("<h4>").text(UI.format.capital(e[l])+" parameters"));var n=Object.keys(a[e[l]]);"sort"in a&&n.sort(function(b,c){return(""+a[e[l]][b][a.sort]).localeCompare(a[e[l]][c][a.sort])});for(var h in n){var t=n[h],m=a[e[l]][t];if(Array.isArray(m))for(var k in m)d.push(c(l,t,m[k]));else d.push(c(l,t,m))}}return d},stored:{get:function(){return mist.data.ui_settings||{}},set:function(a,b){var c=this.get();c[a]=b;mist.send(function(){},
+ {ui_settings:c})},del:function(a){delete mist.data.ui_settings[a];mist.send(function(){},{ui_settings:mist.data.ui_settings})}}};function log(){try{UI.debug&&[].push.call(arguments,Error().stack);[].unshift.call(arguments,"["+UI.format.time((new Date).getTime()/1E3)+"]");console.log.apply(console,arguments)}catch(a){}}
+ $.fn.getval=function(){var a=$(this).data("opts"),b=$(this).val();if(a&&"type"in a)switch(a.type){case "int":b!=""&&(b=Number(b));break;case "span":b=$(this).html();break;case "debug":b=$(this).val()==""?null:Number($(this).val());break;case "checkbox":b=$(this).prop("checked");break;case "radioselect":a=$(this).find("label > input[type=radio]:checked").parent();if(a.length){b=[];b.push(a.children("input[type=radio]").val());a=a.children("select");a.length&&b.push(a.val())}else b="";break;case "checklist":b=
+ [];$(this).find(".checklist input[type=checkbox]:checked").each(function(){b.push($(this).attr("name"))});break;case "unix":b!=""&&(b=Math.round(new Date($(this).val())/1E3));break;case "selectinput":b=$(this).children("select").first().val();b=="CUSTOM"&&(b=$(this).children("label").first().find(".field_container").children().first().getval());break;case "inputlist":b=[];$(this).children().each(function(){$(this).val()!=""&&b.push($(this).val())});break;case "sublist":b=$(this).data("savelist");
+-break;case "json":try{b=JSON.parse($(this).val())}catch(c){b=null}}return b};
++break;case "json":try{b=JSON.parse($(this).val())}catch(c){b=null}break;case "bitmask":b=0;$(this).find("input").each(function(){$(this).prop("checked")&&(b=b+Number($(this).val()))})}return b};
+ $.fn.setval=function(a){var b=$(this).data("opts");$(this).val(a);if(b&&"type"in b)switch(b.type){case "span":$(this).html(a);break;case "checkbox":$(this).prop("checked",a);break;case "geolimited":case "hostlimited":b=$(this).closest(".field_container").data("subUI");if(typeof a=="undefined"||a.length==0)a="-";b.blackwhite.val(a.charAt(0));var a=a.substr(1).split(" "),c;for(c in a)b.values.append(b.prototype.clone(true).val(a[c]));b.blackwhite.trigger("change");break;case "radioselect":if(typeof a==
+ "undefined")return $(this);c=$(this).find('label > input[type=radio][value="'+a[0]+'"]').prop("checked",true).parent();a.length>1&&c.children("select").val(a[1]);break;case "checklist":b=$(this).find(".checklist input[type=checkbox]").prop("checked",false);for(c in a)b.filter('[name="'+a[c]+'"]').prop("checked",true);break;case "unix":if(typeof a!="undefined"){a=new Date(Math.round(a)*1E3);a.setMinutes(a.getMinutes()-a.getTimezoneOffset());a=a.toISOString();$(this).val(a.split("Z")[0])}break;case "selectinput":a===
+ null&&(a="");var d=false;for(c in b.selectinput){var e;typeof b.selectinput[c]=="string"?e=b.selectinput[c]:typeof b.selectinput[c][0]=="string"&&(e=b.selectinput[c][0]);if(e==a){$(this).children("select").first().val(a);d=true;break}}if(!d){$(this).children("label").first().find(".field_container").children().first().setval(a);$(this).children("select").first().val("CUSTOM").trigger("change")}break;case "inputlist":typeof a=="string"&&(a=[a]);for(c in a)$(this).append($(this).data("newitem")().val(a[c]));
+-$(this).append($(this).children().first());break;case "sublist":var l=$(this),b=$(this).children(".curvals");b.html("");if(a&&a.length)for(c in a){var d=$.extend(true,{},a[c]),m=function(a){for(var b in a)b.slice(0,6)=="x-LSP-"?delete a[b]:typeof a[b]=="object"&&m(a[b])};m(d);b.append($("<div>").addClass("subitem").append($("<span>").addClass("itemdetails").text(a[c]["x-LSP-name"]?a[c]["x-LSP-name"]:JSON.stringify(d)).attr("title",JSON.stringify(d,null,2))).append($("<button>").addClass("move").text("^").attr("title",
++$(this).append($(this).children().first());break;case "sublist":var l=$(this),b=$(this).children(".curvals");b.html("");if(a&&a.length)for(c in a){var d=$.extend(true,{},a[c]),n=function(a){for(var b in a)b.slice(0,6)=="x-LSP-"?delete a[b]:typeof a[b]=="object"&&n(a[b])};n(d);b.append($("<div>").addClass("subitem").append($("<span>").addClass("itemdetails").text(a[c]["x-LSP-name"]?a[c]["x-LSP-name"]:JSON.stringify(d)).attr("title",JSON.stringify(d,null,2))).append($("<button>").addClass("move").text("^").attr("title",
+ "Move item up").click(function(){var a=$(this).parent().index();if(a!=0){var b=l.getval();b.splice(a-1,0,b.splice(a,1)[0]);l.setval(b)}})).append($("<button>").text("Edit").click(function(){var a=$(this).parent().index(),b=$(this).closest(".field");b.data("build")(Object.assign({},b.getval()[a]),a)})).append($("<button>").text("x").attr("title","Remove item").click(function(a){var b=$(this).parent().index(),c=l.data("savelist");c.splice(b,1);l.setval(c);a.preventDefault()})))}else b.append("None.");
+-l.data("savelist",a);break;case "json":$(this).val(a===null?"":JSON.stringify(a,null,2))}$(this).trigger("change");return $(this)};function parseURL(a,b){var c=document.createElement("a");c.href=a;if(b)for(var d in b)c[d]=b[d];return{full:c.href,protocol:c.protocol+"//",host:c.hostname,port:c.port?":"+c.port:""}}function triggerRewrite(a){return typeof a=="object"&&typeof a.length=="undefined"?a:obj={handler:a[0],sync:a[1],streams:a[2],"default":a[3]}};
++l.data("savelist",a);break;case "json":$(this).val(a===null?"":JSON.stringify(a,null,2));break;case "bitmask":d=$(this).data("opts").bitmask;b=$(this).find("input");for(c in d){$el=b.eq(c);(a&d[c][0])==d[c][0]?$el.attr("checked","checked"):$el.removeAttr("checked")}}$(this).trigger("change");return $(this)};function parseURL(a,b){var c=document.createElement("a");c.href=a;if(b)for(var d in b)c[d]=b[d];return{full:c.href,protocol:c.protocol+"//",host:c.hostname,port:c.port?":"+c.port:""}}
++function triggerRewrite(a){return typeof a=="object"&&typeof a.length=="undefined"?a:obj={handler:a[0],sync:a[1],streams:a[2],"default":a[3]}};
+diff --git a/lsp/mist.js b/lsp/mist.js
+index 4187b75b..c85d2eb0 100644
+--- a/lsp/mist.js
++++ b/lsp/mist.js
+@@ -249,6 +249,7 @@ var UI = {
+ menu: [
+ {
+ Overview: {},
++ General: {},
+ Protocols: {},
+ Streams: {
+ hiddenmenu: {
+@@ -257,12 +258,8 @@ var UI = {
+ Embed: {}
+ }
+ },
+- Push: {
+- LTSonly: true
+- },
+- 'Triggers': {
+- LTSonly: false
+- },
++ Push: {},
++ Triggers: {},
+ Logs: {},
+ Statistics: {},
+ 'Server Stats': {}
+@@ -300,9 +297,6 @@ var UI = {
+ for (var k in button.classes) {
+ $button.addClass(button.classes[k]);
+ }
+- if ('LTSonly' in button) {
+- $button.addClass('LTSonly');
+- }
+ if ('link' in button) {
+ $button.attr('href',button.link).attr('target','_blank');
+ }
+@@ -598,7 +592,7 @@ var UI = {
+ $field = $('<div>').addClass('radioselect');
+ for (var i in e.radioselect) {
+ var $radio = $('<input>').attr('type','radio').val(e.radioselect[i][0]).attr('name',e.label);
+- if ((('LTSonly' in e) && (!mist.data.LTS)) || (e.readonly)) {
++ if (e.readonly) {
+ $radio.prop('disabled',true);
+ }
+ var $label = $('<label>').append(
+@@ -612,7 +606,7 @@ var UI = {
+ $(this).parent().find('input[type=radio]:enabled').prop('checked','true');
+ });
+ $label.append($select);
+- if ((('LTSonly' in e) && (!mist.data.LTS)) || (e.readonly)) {
++ if (e.readonly) {
+ $select.prop('disabled',true);
+ }
+ for (var j in e.radioselect[i][2]) {
+@@ -659,10 +653,6 @@ var UI = {
+ $field.append($select);
+ $select.data("input",false);
+
+- if (('LTSonly' in e) && (!mist.data.LTS)) {
+- $select.prop('disabled',true);
+- }
+-
+ for (var i in e.selectinput) {
+ var $option = $("<option>");
+ $select.append($option);
+@@ -699,7 +689,7 @@ var UI = {
+ $field = $('<div>').addClass('inputlist');
+ var newitem = function(){
+ var $part = $("<input>").attr("type","text").addClass("listitem");
+- if ((('LTSonly' in e) && (!mist.data.LTS)) || (e.readonly)) {
++ if (e.readonly) {
+ $part.prop('disabled',true);
+ }
+ var keyup = function(e){
+@@ -832,7 +822,7 @@ var UI = {
+ $c.append($itemsettings);
+ break;
+ }
+- case "json":
++ case "json": {
+ $field = $("<textarea>").on('keydown',function(e){
+ e.stopPropagation();
+ }).on('keyup change',function(e){
+@@ -855,6 +845,23 @@ var UI = {
+ e.validate = [f];
+ }
+ break;
++ }
++ case "bitmask": {
++ $field = $("<div>").addClass("bitmask");
++ for (var i in e.bitmask) {
++ $field.append(
++ $("<label>").append(
++ $("<input>").attr("type","checkbox").attr("name","bitmask_"+("pointer" in e ? e.pointer.index : "")).attr("value",e.bitmask[i][0]).addClass("field")
++ ).append(
++ $("<span>").text(e.bitmask[i][1])
++ )
++ );
++ }
++
++ //when the main label is clicked, do nothing (instead of toggeling the first checkbox)
++ $e.attr("for","none");
++ break;
++ }
+ default:
+ $field = $('<input>').attr('type','text');
+ }
+@@ -941,10 +948,6 @@ var UI = {
+ if ('rows' in e) {
+ $field.attr('rows',e.rows);
+ }
+- if (('LTSonly' in e) && (!mist.data.LTS)) {
+- $fc.addClass('LTSonly');
+- $field.prop('disabled',true);
+- }
+ if ("dependent" in e) {
+ for (var i in e.dependent) {
+ $e.attr("data-dependent-"+i,e.dependent[i]);
+@@ -1128,10 +1131,6 @@ var UI = {
+ }
+ subUI.field.trigger('change');
+ });
+- if (('LTSonly' in e) && (!mist.data.LTS)) {
+- subUI.blackwhite.prop('disabled',true);
+- subUI.prototype.prop('disabled',true);
+- }
+ subUI.values.append(subUI.prototype.clone(true));
+ $fc.data('subUI',subUI).addClass('limit_list').append(subUI.blackwhite).append(subUI.values);
+ break;
+@@ -2393,34 +2392,10 @@ var UI = {
+ var $protocols_on = $('<span>');
+ var $protocols_off = $('<span>');
+
+- var s = {
+- serverid: mist.data.config.serverid,
+- debug: mist.data.config.debug,
+- accesslog: mist.data.config.accesslog,
+- prometheus: mist.data.config.prometheus,
+- defaultStream: mist.data.config.defaultStream,
+- trustedproxy: mist.data.config.trustedproxy,
+- location: "location" in mist.data.config ? mist.data.config.location : {}
+- };
+- var b = {};
+- if ("bandwidth" in mist.data) {
+- b = mist.data.bandwidth;
+- if (b == null) { b = {}; }
+- if (!b.limit) {
+- b.limit = "";
+- }
+- }
+- var $bitunit = $("<select>").html(
+- $("<option>").val(1).text("bytes/s")
+- ).append(
+- $("<option>").val(1024).text("KiB/s")
+- ).append(
+- $("<option>").val(1048576).text("MiB/s")
+- ).append(
+- $("<option>").val(1073741824).text("GiB/s")
+- );
+ var host = parseURL(mist.user.host);
+ host = host.protocol+host.host+host.port;
++
++ var s = {};
+
+ $main.append(UI.buildUI([
+ {
+@@ -2436,8 +2411,7 @@ var UI = {
+ },{
+ type: 'span',
+ label: 'Version check',
+- value: $versioncheck,
+- LTSonly: true
++ value: $versioncheck
+ },{
+ type: 'span',
+ label: 'Server time',
+@@ -2445,13 +2419,11 @@ var UI = {
+ },{
+ type: 'span',
+ label: 'Licensed to',
+- value: ("license" in mist.data.config ? mist.data.config.license.user : ""),
+- LTSonly: true
++ value: ("license" in mist.data.config ? mist.data.config.license.user : "")
+ },{
+ type: 'span',
+ label: 'Active licenses',
+- value: $activeproducts,
+- LTSonly: true
++ value: $activeproducts
+ },{
+ type: 'span',
+ label: 'Configured streams',
+@@ -2476,156 +2448,26 @@ var UI = {
+ type: 'span',
+ label: 'Recent problems',
+ value: $errors
+- },$('<br>'),{
+- type: 'str',
+- label: 'Human readable name',
+- pointer: {
+- main: s,
+- index: 'serverid'
+- },
+- help: 'You can name your MistServer here for personal use. You\'ll still need to set host name within your network yourself.'
+- },{
+- type: 'debug',
+- label: 'Debug level',
+- pointer: {
+- main: s,
+- index: 'debug'
+- },
+- help: 'You can set the amount of debug information MistServer saves in the log. A full reboot of MistServer is required before some components of MistServer can post debug information.'
+- },{
+- type: "selectinput",
+- label: "Access log",
+- selectinput: [
+- ["","Do not track"],
+- ["LOG","Log to MistServer log"],
+- [{
+- type:"str",
+- label:"Path",
+- LTSonly: true
+- },"Log to file"]
+- ],
+- pointer: {
+- main: s,
+- index: "accesslog"
+- },
+- help: "Enable access logs.",
+- LTSonly: true
+- },{
+- type: "selectinput",
+- label: "Prometheus stats output",
+- selectinput: [
+- ["","Disabled"],
+- [{
+- type: "str",
+- label:"Passphrase",
+- LTSonly: true
+- },"Enabled"]
+- ],
+- pointer: {
+- main: s,
+- index: "prometheus"
+- },
+- help: "Make stats available in Prometheus format. These can be accessed via "+host+"/PASSPHRASE or "+host+"/PASSPHRASE.json.",
+- LTSonly: true
+- },{
+- type: "inputlist",
+- label: "Trusted proxies",
+- help: "List of proxy server addresses that are allowed to override the viewer IP address to arbitrary values.<br>You may use a hostname or IP address.",
+- LTSonly: true,
+- pointer: {
+- main: s,
+- index: "trustedproxy"
+- }
+- },{
+- type: "selectinput",
+- label: "Load balancer bandwidth limit",
+- selectinput: [
+- ["","Default (1 gbps)"],
+- [{
+- label: "Custom",
+- type: "int",
+- min: 0,
+- unit: $bitunit
+- },"Custom"]
+- ],
+- pointer: {
+- main: b,
+- index: "limit"
+- },
+- help: "This setting only applies when MistServer is combined with a load balancer. This is the amount of traffic this server is willing to handle.",
+- LTSonly: true
+- },{
+- type: "inputlist",
+- label: "Load balancer bandwidth exceptions",
+- pointer: {
+- main: b,
+- index: "exceptions"
+- },
+- help: "This setting only applies when MistServer is combined with a load balancer. Data sent to the hosts and subnets listed here will not count towards reported bandwidth usage.<br>Examples:<ul><li>192.168.0.0/16</li><li>localhost</li><li>10.0.0.0/8</li><li>fe80::/16</li></ul>",
+- LTSonly: true
+- },{
+- type: "int",
+- step: 0.00000001,
+- label: "Server latitude",
+- pointer: {
+- main: s.location,
+- index: "lat"
+- },
+- help: "This setting is only useful when MistServer is combined with a load balancer. When this is set, the balancer can send users to a server close to them.",
+- LTSonly: true
+- },{
+- type: "int",
+- step: 0.00000001,
+- label: "Server longitude",
+- pointer: {
+- main: s.location,
+- index: "lon"
+- },
+- help: "This setting is only useful when MistServer is combined with a load balancer. When this is set, the balancer can send users to a server close to them.",
+- LTSonly: true
+- },{
+- type: "str",
+- label: "Server location name",
+- pointer: {
+- main: s.location,
+- index: "name"
+- },
+- help: "This setting is only useful when MistServer is combined with a load balancer. This will be displayed as the server's location.",
+- LTSonly: true
+- },{
+- type: "str",
+- validate: ['streamname_with_wildcard_and_variables'],
+- label: 'Fallback stream',
+- pointer: {
+- main: s,
+- index: "defaultStream"
+- },
+- help: "When this is set, if someone attempts to view a stream that does not exist, or is offline, they will be redirected to this stream instead. $stream may be used to refer to the original stream name.",
+- LTSonly: true
++ },
++ $("<br>"),
++ $("<h3>").text("Write config now"),
++ {
++ type: "help",
++ help: "Tick the box in order to force an immediate save to the config.json MistServer uses to save your settings. Saving will otherwise happen upon closing MistServer. Don\'t forget to press save after ticking the box."
+ },{
+ type: 'checkbox',
+ label: 'Force configurations save',
+ pointer: {
+ main: s,
+ index: 'save'
+- },
+- help: 'Tick the box in order to force an immediate save to the config.json MistServer uses to save your settings. Saving will otherwise happen upon closing MistServer. Don\'t forget to press save after ticking the box.'
++ }
+ },{
+ type: 'buttons',
+ buttons: [{
+ type: 'save',
+ label: 'Save',
+ 'function': function(){
+- var save = {config: s};
+-
+- var bandwidth = {};
+- bandwidth.limit = (b.limit ? $bitunit.val() * b.limit : 0);
+- bandwidth.exceptions = b.exceptions;
+- if (bandwidth.exceptions === null) {
+- bandwidth.exceptions = [];
+- }
+-
+- save.bandwidth = bandwidth;
++ var save = {};
+
+ if (s.save) {
+ save.save = s.save;
+@@ -2857,6 +2699,289 @@ var UI = {
+ UI.interval.set(updateViewers,30e3);
+
+ break;
++ case 'General': {
++
++ var s = {
++ serverid: mist.data.config.serverid,
++ debug: mist.data.config.debug,
++ accesslog: mist.data.config.accesslog,
++ prometheus: mist.data.config.prometheus,
++ sessionViewerMode: mist.data.config.sessionViewerMode,
++ sessionInputMode: mist.data.config.sessionInputMode,
++ sessionOutputMode: mist.data.config.sessionOutputMode,
++ sessionUnspecifiedMode: mist.data.config.sessionUnspecifiedMode,
++ tknMode: mist.data.config.tknMode,
++ sessionStreamInfoMode: mist.data.config.sessionStreamInfoMode,
++ defaultStream: mist.data.config.defaultStream,
++ trustedproxy: mist.data.config.trustedproxy,
++ location: "location" in mist.data.config ? mist.data.config.location : {}
++ };
++ var b = {};
++ if ("bandwidth" in mist.data) {
++ b = mist.data.bandwidth;
++ if (b == null) { b = {}; }
++ if (!b.limit) {
++ b.limit = "";
++ }
++ }
++ var $bitunit = $("<select>").html(
++ $("<option>").val(1).text("bytes/s")
++ ).append(
++ $("<option>").val(1024).text("KiB/s")
++ ).append(
++ $("<option>").val(1048576).text("MiB/s")
++ ).append(
++ $("<option>").val(1073741824).text("GiB/s")
++ );
++
++
++ $main.html(UI.buildUI([
++ $("<h2>").text("General settings"),{
++ type: "help",
++ help: "These are settings that apply to your MistServer instance in general."
++ },{
++ type: 'str',
++ label: 'Human readable name',
++ pointer: {
++ main: s,
++ index: 'serverid'
++ },
++ help: 'You can name your MistServer here for personal use. You\'ll still need to set host name within your network yourself.'
++ },{
++ type: 'debug',
++ label: 'Debug level',
++ pointer: {
++ main: s,
++ index: 'debug'
++ },
++ help: 'You can set the amount of debug information MistServer saves in the log. A full reboot of MistServer is required before some components of MistServer can post debug information.'
++ },{
++ type: "selectinput",
++ label: "Access log",
++ selectinput: [
++ ["","Do not track"],
++ ["LOG","Log to MistServer log"],
++ [{
++ type:"str",
++ label:"Path"
++ },"Log to file"]
++ ],
++ pointer: {
++ main: s,
++ index: "accesslog"
++ },
++ help: "Enable access logs."
++ },{
++ type: "selectinput",
++ label: "Prometheus stats output",
++ selectinput: [
++ ["","Disabled"],
++ [{
++ type: "str",
++ label:"Passphrase"
++ },"Enabled"]
++ ],
++ pointer: {
++ main: s,
++ index: "prometheus"
++ },
++ help: "Make stats available in Prometheus format. These can be accessed via "+host+"/PASSPHRASE or "+host+"/PASSPHRASE.json."
++ },{
++ type: "inputlist",
++ label: "Trusted proxies",
++ help: "List of proxy server addresses that are allowed to override the viewer IP address to arbitrary values.<br>You may use a hostname or IP address.",
++ pointer: {
++ main: s,
++ index: "trustedproxy"
++ }
++ },{
++ type: "str",
++ validate: ['streamname_with_wildcard_and_variables'],
++ label: 'Fallback stream',
++ pointer: {
++ main: s,
++ index: "defaultStream"
++ },
++ help: "When this is set, if someone attempts to view a stream that does not exist, or is offline, they will be redirected to this stream instead. $stream may be used to refer to the original stream name."
++ },
++
++
++
++ $("<h3>").text("Sessions"),
++
++ {
++ type: 'bitmask',
++ label: 'Bundle viewer sessions by',
++ bitmask: [
++ [8,"Stream name"],
++ [4,"IP address"],
++ [2,"Token"],
++ [1,"Protocol"]
++ ],
++ pointer: {
++ main: s,
++ index: 'sessionViewerMode'
++ },
++ help: 'Change the way viewer connections are bundled into sessions.<br>Default: stream name, viewer IP and token'
++ },{
++ type: 'bitmask',
++ label: 'Bundle input sessions by',
++ bitmask: [
++ [8,"Stream name"],
++ [4,"IP address"],
++ [2,"Token"],
++ [1,"Protocol"]
++ ],
++ pointer: {
++ main: s,
++ index: 'sessionInputMode'
++ },
++ help: 'Change the way input connections are bundled into sessions.<br>Default: stream name, input IP, token and protocol'
++ },{
++ type: 'bitmask',
++ label: 'Bundle output sessions by',
++ bitmask: [
++ [8,"Stream name"],
++ [4,"IP address"],
++ [2,"Token"],
++ [1,"Protocol"]
++ ],
++ pointer: {
++ main: s,
++ index: 'sessionOutputMode'
++ },
++ help: 'Change the way output connections are bundled into sessions.<br>Default: stream name, output IP, token and protocol'
++ },{
++ type: 'bitmask',
++ label: 'Bundle unspecified sessions by',
++ bitmask: [
++ [8,"Stream name"],
++ [4,"IP address"],
++ [2,"Token"],
++ [1,"Protocol"]
++ ],
++ pointer: {
++ main: s,
++ index: 'sessionUnspecifiedMode'
++ },
++ help: 'Change the way unspecified connections are bundled into sessions.<br>Default: none'
++ },{
++ type: 'select',
++ label: 'Treat HTTP-only sessions as',
++ select: [
++ [1, 'A viewer session'],
++ [2, 'An output session: skip executing the USER_NEW and USER_END triggers'],
++ [4, 'A separate \'unspecified\' session: skip executing the USER_NEW and USER_END triggers'],
++ [3, 'Do not start a session: skip executing the USER_NEW and USER_END triggers and do not count for statistics']
++ ],
++ pointer: {
++ main: s,
++ index: 'sessionStreamInfoMode'
++ },
++ help: 'Change the way the stream info connection gets treated.<br>Default: as a viewer session'
++ },{
++ type: "bitmask",
++ label: "Communicate session token",
++ bitmask: [
++ [8,"Write to cookie"],
++ [4,"Write to URL parameter"],
++ [2,"Read from cookie"],
++ [1,"Read from URL parameter"]
++ ],
++ pointer: {
++ main: s,
++ index: "tknMode"
++ },
++ help: "Change the way the session token gets passed to and from MistServer, which can be set as a cookie or URL parameter named `tkn`. Reading the session token as a URL parameter takes precedence over reading from the cookie.<br>Default: all"
++ },
++
++
++
++ $('<h3>').text("Load balancer"),
++ {
++ type: "help",
++ help: "If you're using MistServer's load balancer, the information below is passed to it so that it can make informed decisions."
++ },
++
++ {
++ type: "selectinput",
++ label: "Server's bandwidth limit",
++ selectinput: [
++ ["","Default (1 gbps)"],
++ [{
++ label: "Custom",
++ type: "int",
++ min: 0,
++ unit: $bitunit
++ },"Custom"]
++ ],
++ pointer: {
++ main: b,
++ index: "limit"
++ },
++ help: "This is the amount of traffic this server is willing to handle."
++ },{
++ type: "inputlist",
++ label: "Bandwidth exceptions",
++ pointer: {
++ main: b,
++ index: "exceptions"
++ },
++ help: "Data sent to the hosts and subnets listed here will not count towards reported bandwidth usage.<br>Examples:<ul><li>192.168.0.0/16</li><li>localhost</li><li>10.0.0.0/8</li><li>fe80::/16</li></ul>"
++ },{
++ type: "int",
++ step: 0.00000001,
++ label: "Server latitude",
++ pointer: {
++ main: s.location,
++ index: "lat"
++ },
++ help: "This setting is only useful when MistServer is combined with a load balancer. When this is set, the balancer can send users to a server close to them."
++ },{
++ type: "int",
++ step: 0.00000001,
++ label: "Server longitude",
++ pointer: {
++ main: s.location,
++ index: "lon"
++ },
++ help: "This setting is only useful when MistServer is combined with a load balancer. When this is set, the balancer can send users to a server close to them."
++ },{
++ type: "str",
++ label: "Server location name",
++ pointer: {
++ main: s.location,
++ index: "name"
++ },
++ help: "This setting is only useful when MistServer is combined with a load balancer. This will be displayed as the server's location."
++ },{
++ type: 'buttons',
++ buttons: [{
++ type: 'save',
++ label: 'Save',
++ 'function': function(ele){
++ $(ele).text("Saving..");
++
++ var save = {config: s};
++
++ var bandwidth = {};
++ bandwidth.limit = (b.limit ? $bitunit.val() * b.limit : 0);
++ bandwidth.exceptions = b.exceptions;
++ if (bandwidth.exceptions === null) {
++ bandwidth.exceptions = [];
++ }
++ save.bandwidth = bandwidth;
++
++ mist.send(function(){
++ UI.navto('Overview');
++ },save)
++ }
++ }]
++ }
++
++ ]));
++ break;
++ }
+ case 'Protocols':
+ if (typeof mist.data.capabilities == 'undefined') {
+ mist.send(function(d){
+@@ -3832,7 +3957,7 @@ var UI = {
+ Linux/MacOS:&nbsp;/PATH/<br>\
+ Windows:&nbsp;/cygdrive/DRIVE/PATH/\
+ </td>\
+- <td class=LTSonly>\
++ <td>\
+ A folder stream makes all the recognised files in the selected folder available as a stream.\
+ </td>\
+ </tr>\
+@@ -3851,12 +3976,12 @@ var UI = {
+ <tr>\
+ <th>RTSP</th>\
+ <td>push://(IP)(@PASSWORD)</td>\
+- <td class=LTSonly>IP is white listed IP for pushing towards MistServer, if left empty all are white listed.</td>\
++ <td>IP is white listed IP for pushing towards MistServer, if left empty all are white listed.</td>\
+ </tr>\
+ <tr>\
+ <th>TS</th>\
+ <td>tsudp://(IP):PORT(/INTERFACE)</td>\
+- <td class=LTSonly>\
++ <td>\
+ IP is the IP address used to listen for this stream, multi-cast IP range is: 224.0.0.0 - 239.255.255.255. If IP is not set all addresses will listened to.<br>\
+ PORT is the port you reserve for this stream on the chosen IP.<br>\
+ INTERFACE is the interface used, if left all interfaces will be used.\
+@@ -3877,19 +4002,19 @@ var UI = {
+ <tr>\
+ <th>HLS</th>\
+ <td>http://URL/TO/STREAM.m3u8</td>\
+- <td class=LTSonly>The URL where the HLS stream is available to MistServer.</td>\
++ <td>The URL where the HLS stream is available to MistServer.</td>\
+ </tr>\
+ <tr>\
+ <th>RTSP</th>\
+ <td>rtsp://(USER:PASSWORD@)IP(:PORT)(/path)</td>\
+- <td class=LTSonly>\
++ <td>\
+ USER:PASSWORD is the account used if authorization is required.<br>\
+ IP is the IP address used to pull this stream from.<br>\
+ PORT is the port used to connect through.<br>\
+ PATH is the path to be used to identify the correct stream.\
+ </td>\
+ </tr>\
+- </table>").replace(/LTSonly/g,(mist.data.LTS ? "\"\"" : "LTSonly"))
++ </table>")
+ ,
+ 'function': function(){
+ var source = $(this).val();
+@@ -3993,7 +4118,6 @@ var UI = {
+ label: 'Stop sessions',
+ type: 'checkbox',
+ help: 'When saving these stream settings, kill this stream\'s current connections.',
+- LTSonly: true,
+ pointer: {
+ main: saveas,
+ index: 'stop_sessions'
+@@ -5287,8 +5411,7 @@ var UI = {
+ pointer: {
+ main: push_settings,
+ index: 'wait'
+- },
+- LTSonly: 1
++ }
+ },{
+ label: 'Maximum retries',
+ unit: '/s',
+@@ -5299,8 +5422,7 @@ var UI = {
+ pointer: {
+ main: push_settings,
+ index: 'maxspeed'
+- },
+- LTSonly: 1
++ }
+ },{
+ type: 'buttons',
+ buttons: [{
+@@ -5556,8 +5678,7 @@ var UI = {
+ "break": false
+ };
+ }],
+- datalist: allthestreams,
+- LTSonly: 1
++ datalist: allthestreams
+ },{
+ label: 'Target',
+ type: 'str',
+@@ -5622,8 +5743,7 @@ var UI = {
+ optional: mist.data.capabilities.connectors[match].push_parameters
+ };
+ $additional_params.append(UI.buildUI(mist.convertBuildOptions(capa,saveas.params)));
+- },
+- LTSonly: 1
++ }
+ },$additional_params];
+
+
+@@ -5909,7 +6029,6 @@ var UI = {
+ help: 'For what event this trigger should activate.',
+ type: 'select',
+ select: triggerSelect,
+- LTSonly: true,
+ validate: ['required'],
+ 'function': function(){
+ var v = $(this).getval();
+@@ -5992,8 +6111,7 @@ var UI = {
+ },
+ help: 'For triggers that can apply to specific streams, this value decides what streams they are triggered for. (none checked = always triggered)',
+ type: 'checklist',
+- checklist: Object.keys(mist.data.streams),
+- LTSonly: true
++ checklist: Object.keys(mist.data.streams)
+ },$('<br>'),{
+ label: 'Handler (URL or executable)',
+ help: 'This can be either an HTTP URL or a full path to an executable.',
+@@ -6002,8 +6120,7 @@ var UI = {
+ index: 'url'
+ },
+ validate: ['required'],
+- type: 'str',
+- LTSonly: true
++ type: 'str'
+ },{
+ label: 'Blocking',
+ type: 'checkbox',
+@@ -6011,8 +6128,7 @@ var UI = {
+ pointer: {
+ main: saveas,
+ index: 'async'
+- },
+- LTSonly: true
++ }
+ },{
+ label: 'Parameters',
+ type: 'str',
+@@ -6020,8 +6136,7 @@ var UI = {
+ pointer: {
+ main: saveas,
+ index: 'params'
+- },
+- LTSonly: true
++ }
+ },{
+ label: 'Default response',
+ type: 'str',
+@@ -6030,8 +6145,7 @@ var UI = {
+ pointer: {
+ main: saveas,
+ index: 'default'
+- },
+- LTSonly: true
++ }
+ },{
+ type: 'buttons',
+ buttons: [
+@@ -6730,8 +6844,6 @@ var mist = {
+ UI.elements.connection.user_and_host.text(mist.user.name+' @ '+mist.user.host);
+ UI.elements.connection.msg.removeClass('red').text('Last communication with the server at '+UI.format.time((new Date).getTime()/1000));
+
+- //if this is LTS, get rid of the banner on menu buttons
+- if (d.LTS) { UI.elements.menu.find('.LTSonly').removeClass('LTSonly'); }
+
+ if (d.log) {
+ var lastlog = d.log[d.log.length-1];
+@@ -7181,6 +7293,15 @@ $.fn.getval = function(){
+ val = null;
+ }
+ break;
++ case "bitmask": {
++ val = 0;
++ $(this).find("input").each(function(){
++ if ($(this).prop("checked")) {
++ val += Number($(this).val());
++ }
++ });
++ break;
++ }
+ }
+ }
+ return val;
+@@ -7327,6 +7448,21 @@ $.fn.setval = function(val){
+ break;
+ case "json": {
+ $(this).val(val === null ? "" : JSON.stringify(val,null,2));
++ break;
++ }
++ case "bitmask": {
++ var map = $(this).data("opts").bitmask;
++ var $inputs = $(this).find("input");
++ for (var i in map) {
++ $el = $inputs.eq(i);
++ if ((val & map[i][0]) == map[i][0]) {
++ $el.attr("checked","checked");
++ }
++ else {
++ $el.removeAttr("checked");
++ }
++ }
++ break;
+ }
+ }
+ }
+--
+2.25.1
+
+
+From 7098f8430b3a9901e2c819c7bc562e3f99f6e19a Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Thu, 6 Oct 2022 16:23:29 +0200
+Subject: [PATCH 29/38] Update input codec capabilities to be more
+ consistent/accurate, and more easily automatically parsed
+
+---
+ src/input/input_aac.cpp | 2 +-
+ src/input/input_av.cpp | 10 ++++-----
+ src/input/input_balancer.cpp | 3 ---
+ src/input/input_buffer.cpp | 3 ---
+ src/input/input_dtsc.cpp | 14 ++++++-------
+ src/input/input_ebml.cpp | 40 ++++++++++++++++++------------------
+ src/input/input_flv.cpp | 10 ++++-----
+ src/input/input_h264.cpp | 2 +-
+ src/input/input_hls.cpp | 8 ++++----
+ src/input/input_ismv.cpp | 4 ++--
+ src/input/input_mp3.cpp | 2 +-
+ src/input/input_mp4.cpp | 14 ++++++-------
+ src/input/input_ogg.cpp | 6 +++---
+ src/input/input_rtsp.cpp | 26 +++++++++++------------
+ src/input/input_sdp.cpp | 26 +++++++++++------------
+ src/input/input_srt.cpp | 2 +-
+ src/input/input_ts.cpp | 16 +++++++--------
+ src/input/input_tsrist.cpp | 18 ++++++++--------
+ src/input/input_tssrt.cpp | 18 ++++++++--------
+ 19 files changed, 108 insertions(+), 116 deletions(-)
+
+diff --git a/src/input/input_aac.cpp b/src/input/input_aac.cpp
+index a107bee6..cff14289 100644
+--- a/src/input/input_aac.cpp
++++ b/src/input/input_aac.cpp
+@@ -65,7 +65,7 @@ namespace Mist{
+ capa["source_match"] = "/*.aac";
+ capa["source_file"] = "$source";
+ capa["priority"] = 9;
+- capa["codecs"][0u][1u].append("AAC");
++ capa["codecs"]["audio"].append("AAC");
+ thisTime = 0;
+ // init filePos at 1, else a 15 bit mismatch in expected frame size occurs
+ // dtsc.ccp +- line 215
+diff --git a/src/input/input_av.cpp b/src/input/input_av.cpp
+index 94774cb9..aa7cd28c 100644
+--- a/src/input/input_av.cpp
++++ b/src/input/input_av.cpp
+@@ -20,15 +20,13 @@ namespace Mist{
+ capa["source_match"] = "/*";
+ capa["source_file"] = "$source";
+ capa["priority"] = 1;
+- capa["codecs"][0u][0u].null();
+- capa["codecs"][0u][1u].null();
+- capa["codecs"][0u][2u].null();
++ capa["codecs"].null();
+ av_register_all();
+ AVCodec *cInfo = 0;
+ while ((cInfo = av_codec_next(cInfo)) != 0){
+- if (cInfo->type == AVMEDIA_TYPE_VIDEO){capa["codecs"][0u][0u].append(cInfo->name);}
+- if (cInfo->type == AVMEDIA_TYPE_AUDIO){capa["codecs"][0u][1u].append(cInfo->name);}
+- if (cInfo->type == AVMEDIA_TYPE_SUBTITLE){capa["codecs"][0u][3u].append(cInfo->name);}
++ if (cInfo->type == AVMEDIA_TYPE_VIDEO){capa["codecs"]["video"].append(cInfo->name);}
++ if (cInfo->type == AVMEDIA_TYPE_AUDIO){capa["codecs"]["audio"].append(cInfo->name);}
++ if (cInfo->type == AVMEDIA_TYPE_SUBTITLE){capa["codecs"]["subtitle"].append(cInfo->name);}
+ }
+ }
+
+diff --git a/src/input/input_balancer.cpp b/src/input/input_balancer.cpp
+index 36db5709..48997795 100644
+--- a/src/input/input_balancer.cpp
++++ b/src/input/input_balancer.cpp
+@@ -80,9 +80,6 @@ namespace Mist{
+ capa["optional"]["segmentsize"]["option"] = "--segment-size";
+ capa["optional"]["segmentsize"]["type"] = "uint";
+ capa["optional"]["segmentsize"]["default"] = 5000;
+- capa["codecs"][0u][0u].append("*");
+- capa["codecs"][0u][1u].append("*");
+- capa["codecs"][0u][2u].append("*");
+ }
+
+ int inputBalancer::boot(int argc, char *argv[]){
+diff --git a/src/input/input_buffer.cpp b/src/input/input_buffer.cpp
+index 534f9f6e..d1ccfbcf 100644
+--- a/src/input/input_buffer.cpp
++++ b/src/input/input_buffer.cpp
+@@ -129,9 +129,6 @@ namespace Mist{
+ "live media data. The push://[host][@password] style source allows all enabled protocols "
+ "that support push input to accept a push into MistServer, where you can accept incoming "
+ "streams from everyone, based on a set password, and/or use hostname/IP whitelisting.";
+- capa["codecs"][0u][0u].append("*");
+- capa["codecs"][0u][1u].append("*");
+- capa["codecs"][0u][2u].append("*");
+ bufferTime = 50000;
+ cutTime = 0;
+ segmentSize = 1900;
+diff --git a/src/input/input_dtsc.cpp b/src/input/input_dtsc.cpp
+index 381b59f1..bf7012db 100644
+--- a/src/input/input_dtsc.cpp
++++ b/src/input/input_dtsc.cpp
+@@ -23,13 +23,13 @@ namespace Mist{
+ capa["source_match"].append("dtsc://*");
+ capa["always_match"].append("dtsc://*"); // can be said to always-on mode
+ capa["source_file"] = "$source";
+- capa["codecs"][0u][0u].append("H264");
+- capa["codecs"][0u][0u].append("H263");
+- capa["codecs"][0u][0u].append("VP6");
+- capa["codecs"][0u][0u].append("theora");
+- capa["codecs"][0u][1u].append("AAC");
+- capa["codecs"][0u][1u].append("MP3");
+- capa["codecs"][0u][1u].append("vorbis");
++ capa["codecs"]["video"].append("H264");
++ capa["codecs"]["video"].append("H263");
++ capa["codecs"]["video"].append("VP6");
++ capa["codecs"]["video"].append("theora");
++ capa["codecs"]["audio"].append("AAC");
++ capa["codecs"]["audio"].append("MP3");
++ capa["codecs"]["audio"].append("vorbis");
+
+ JSON::Value option;
+ option["arg"] = "integer";
+diff --git a/src/input/input_ebml.cpp b/src/input/input_ebml.cpp
+index bcce5a77..958c1222 100644
+--- a/src/input/input_ebml.cpp
++++ b/src/input/input_ebml.cpp
+@@ -19,26 +19,26 @@ namespace Mist{
+ capa["always_match"].append("mkv-exec:*");
+ capa["source_file"] = "$source";
+ capa["priority"] = 9;
+- capa["codecs"].append("H264");
+- capa["codecs"].append("HEVC");
+- capa["codecs"].append("VP8");
+- capa["codecs"].append("VP9");
+- capa["codecs"].append("AV1");
+- capa["codecs"].append("opus");
+- capa["codecs"].append("vorbis");
+- capa["codecs"].append("theora");
+- capa["codecs"].append("AAC");
+- capa["codecs"].append("PCM");
+- capa["codecs"].append("ALAW");
+- capa["codecs"].append("ULAW");
+- capa["codecs"].append("MP2");
+- capa["codecs"].append("MPEG2");
+- capa["codecs"].append("MP3");
+- capa["codecs"].append("AC3");
+- capa["codecs"].append("FLOAT");
+- capa["codecs"].append("DTS");
+- capa["codecs"].append("JSON");
+- capa["codecs"].append("subtitle");
++ capa["codecs"]["video"].append("H264");
++ capa["codecs"]["video"].append("HEVC");
++ capa["codecs"]["video"].append("VP8");
++ capa["codecs"]["video"].append("VP9");
++ capa["codecs"]["video"].append("AV1");
++ capa["codecs"]["video"].append("theora");
++ capa["codecs"]["video"].append("MPEG2");
++ capa["codecs"]["audio"].append("opus");
++ capa["codecs"]["audio"].append("vorbis");
++ capa["codecs"]["audio"].append("AAC");
++ capa["codecs"]["audio"].append("PCM");
++ capa["codecs"]["audio"].append("ALAW");
++ capa["codecs"]["audio"].append("ULAW");
++ capa["codecs"]["audio"].append("MP2");
++ capa["codecs"]["audio"].append("MP3");
++ capa["codecs"]["audio"].append("AC3");
++ capa["codecs"]["audio"].append("FLOAT");
++ capa["codecs"]["audio"].append("DTS");
++ capa["codecs"]["metadata"].append("JSON");
++ capa["codecs"]["subtitle"].append("subtitle");
+ lastClusterBPos = 0;
+ lastClusterTime = 0;
+ bufferedPacks = 0;
+diff --git a/src/input/input_flv.cpp b/src/input/input_flv.cpp
+index d7399c23..82565119 100644
+--- a/src/input/input_flv.cpp
++++ b/src/input/input_flv.cpp
+@@ -21,11 +21,11 @@ namespace Mist{
+ capa["source_match"] = "/*.flv";
+ capa["source_file"] = "$source";
+ capa["priority"] = 9;
+- capa["codecs"][0u][0u].append("H264");
+- capa["codecs"][0u][0u].append("H263");
+- capa["codecs"][0u][0u].append("VP6");
+- capa["codecs"][0u][1u].append("AAC");
+- capa["codecs"][0u][1u].append("MP3");
++ capa["codecs"]["video"].append("H264");
++ capa["codecs"]["video"].append("H263");
++ capa["codecs"]["video"].append("VP6");
++ capa["codecs"]["audio"].append("AAC");
++ capa["codecs"]["audio"].append("MP3");
+ }
+
+ inputFLV::~inputFLV(){}
+diff --git a/src/input/input_h264.cpp b/src/input/input_h264.cpp
+index b9c72e47..f374c84f 100644
+--- a/src/input/input_h264.cpp
++++ b/src/input/input_h264.cpp
+@@ -11,7 +11,7 @@ namespace Mist{
+ // May be set to always-on mode
+ capa["always_match"].append("h264-exec:*");
+ capa["priority"] = 0;
+- capa["codecs"][0u][0u].append("H264");
++ capa["codecs"]["video"].append("H264");
+ frameCount = 0;
+ startTime = Util::bootMS();
+ inputProcess = 0;
+diff --git a/src/input/input_hls.cpp b/src/input/input_hls.cpp
+index e51a951d..dc999eb4 100644
+--- a/src/input/input_hls.cpp
++++ b/src/input/input_hls.cpp
+@@ -592,10 +592,10 @@ namespace Mist{
+ capa["always_match"] = capa["source_match"];
+
+ capa["priority"] = 9;
+- capa["codecs"][0u][0u].append("H264");
+- capa["codecs"][0u][1u].append("AAC");
+- capa["codecs"][0u][1u].append("AC3");
+- capa["codecs"][0u][1u].append("MP3");
++ capa["codecs"]["video"].append("H264");
++ capa["codecs"]["audio"].append("AAC");
++ capa["codecs"]["audio"].append("AC3");
++ capa["codecs"]["audio"].append("MP3");
+
+ inFile = NULL;
+ }
+diff --git a/src/input/input_ismv.cpp b/src/input/input_ismv.cpp
+index 4e5a5cfa..71084955 100644
+--- a/src/input/input_ismv.cpp
++++ b/src/input/input_ismv.cpp
+@@ -16,8 +16,8 @@ namespace Mist{
+ capa["desc"] = "This input allows you to stream ISMV Video on Demand files.";
+ capa["source_match"] = "/*.ismv";
+ capa["priority"] = 9;
+- capa["codecs"][0u][0u].append("H264");
+- capa["codecs"][0u][1u].append("AAC");
++ capa["codecs"]["video"].append("H264");
++ capa["codecs"]["audio"].append("AAC");
+
+ inFile = 0;
+ }
+diff --git a/src/input/input_mp3.cpp b/src/input/input_mp3.cpp
+index 6bc656dd..4d757217 100644
+--- a/src/input/input_mp3.cpp
++++ b/src/input/input_mp3.cpp
+@@ -19,7 +19,7 @@ namespace Mist{
+ capa["source_match"] = "/*.mp3";
+ capa["source_file"] = "$source";
+ capa["priority"] = 9;
+- capa["codecs"][0u][0u].append("MP3");
++ capa["codecs"]["audio"].append("MP3");
+ timestamp = 0;
+ }
+
+diff --git a/src/input/input_mp4.cpp b/src/input/input_mp4.cpp
+index f85911fb..5becc8c9 100644
+--- a/src/input/input_mp4.cpp
++++ b/src/input/input_mp4.cpp
+@@ -169,13 +169,13 @@ namespace Mist{
+ capa["source_match"].append("mp4:*");
+ capa["source_file"] = "$source";
+ capa["priority"] = 9;
+- capa["codecs"][0u][0u].append("HEVC");
+- capa["codecs"][0u][0u].append("H264");
+- capa["codecs"][0u][0u].append("H263");
+- capa["codecs"][0u][0u].append("VP6");
+- capa["codecs"][0u][1u].append("AAC");
+- capa["codecs"][0u][1u].append("AC3");
+- capa["codecs"][0u][1u].append("MP3");
++ capa["codecs"]["video"].append("HEVC");
++ capa["codecs"]["video"].append("H264");
++ capa["codecs"]["video"].append("H263");
++ capa["codecs"]["video"].append("VP6");
++ capa["codecs"]["audio"].append("AAC");
++ capa["codecs"]["audio"].append("AC3");
++ capa["codecs"]["audio"].append("MP3");
+ readPos = 0;
+ }
+
+diff --git a/src/input/input_ogg.cpp b/src/input/input_ogg.cpp
+index 7e262138..62380bf0 100644
+--- a/src/input/input_ogg.cpp
++++ b/src/input/input_ogg.cpp
+@@ -38,9 +38,9 @@ namespace Mist{
+ capa["desc"] = "This input allows streaming of OGG files as Video on Demand.";
+ capa["source_match"] = "/*.ogg";
+ capa["source_file"] = "$source";
+- capa["codecs"][0u][0u].append("theora");
+- capa["codecs"][0u][1u].append("vorbis");
+- capa["codecs"][0u][1u].append("opus");
++ capa["codecs"]["video"].append("theora");
++ capa["codecs"]["audio"].append("vorbis");
++ capa["codecs"]["audio"].append("opus");
+ }
+
+ bool inputOGG::checkArguments(){
+diff --git a/src/input/input_rtsp.cpp b/src/input/input_rtsp.cpp
+index 25be6e23..59b25e1f 100644
+--- a/src/input/input_rtsp.cpp
++++ b/src/input/input_rtsp.cpp
+@@ -42,19 +42,19 @@ namespace Mist{
+ // These can/may be set to always-on mode
+ capa["always_match"].append("rtsp://*");
+ capa["priority"] = 9;
+- capa["codecs"][0u][0u].append("H264");
+- capa["codecs"][0u][0u].append("HEVC");
+- capa["codecs"][0u][0u].append("MPEG2");
+- capa["codecs"][0u][0u].append("VP8");
+- capa["codecs"][0u][0u].append("VP9");
+- capa["codecs"][0u][1u].append("AAC");
+- capa["codecs"][0u][1u].append("MP3");
+- capa["codecs"][0u][1u].append("AC3");
+- capa["codecs"][0u][1u].append("ALAW");
+- capa["codecs"][0u][1u].append("ULAW");
+- capa["codecs"][0u][1u].append("PCM");
+- capa["codecs"][0u][1u].append("opus");
+- capa["codecs"][0u][1u].append("MP2");
++ capa["codecs"]["video"].append("H264");
++ capa["codecs"]["video"].append("HEVC");
++ capa["codecs"]["video"].append("MPEG2");
++ capa["codecs"]["video"].append("VP8");
++ capa["codecs"]["video"].append("VP9");
++ capa["codecs"]["audio"].append("AAC");
++ capa["codecs"]["audio"].append("MP3");
++ capa["codecs"]["audio"].append("AC3");
++ capa["codecs"]["audio"].append("ALAW");
++ capa["codecs"]["audio"].append("ULAW");
++ capa["codecs"]["audio"].append("PCM");
++ capa["codecs"]["audio"].append("opus");
++ capa["codecs"]["audio"].append("MP2");
+
+ JSON::Value option;
+ option["arg"] = "integer";
+diff --git a/src/input/input_sdp.cpp b/src/input/input_sdp.cpp
+index 0b8ddeb5..0072cbeb 100644
+--- a/src/input/input_sdp.cpp
++++ b/src/input/input_sdp.cpp
+@@ -41,19 +41,19 @@ namespace Mist{
+ capa["source_match"].append("*.sdp");
+ capa["always_match"].append("*.sdp");
+ capa["priority"] = 9;
+- capa["codecs"][0u][0u].append("H264");
+- capa["codecs"][0u][0u].append("HEVC");
+- capa["codecs"][0u][0u].append("MPEG2");
+- capa["codecs"][0u][0u].append("VP8");
+- capa["codecs"][0u][0u].append("VP9");
+- capa["codecs"][0u][1u].append("AAC");
+- capa["codecs"][0u][1u].append("MP3");
+- capa["codecs"][0u][1u].append("AC3");
+- capa["codecs"][0u][1u].append("ALAW");
+- capa["codecs"][0u][1u].append("ULAW");
+- capa["codecs"][0u][1u].append("PCM");
+- capa["codecs"][0u][1u].append("opus");
+- capa["codecs"][0u][1u].append("MP2");
++ capa["codecs"]["video"].append("H264");
++ capa["codecs"]["video"].append("HEVC");
++ capa["codecs"]["video"].append("MPEG2");
++ capa["codecs"]["video"].append("VP8");
++ capa["codecs"]["video"].append("VP9");
++ capa["codecs"]["audio"].append("AAC");
++ capa["codecs"]["audio"].append("MP3");
++ capa["codecs"]["audio"].append("AC3");
++ capa["codecs"]["audio"].append("ALAW");
++ capa["codecs"]["audio"].append("ULAW");
++ capa["codecs"]["audio"].append("PCM");
++ capa["codecs"]["audio"].append("opus");
++ capa["codecs"]["audio"].append("MP2");
+
+ JSON::Value option;
+ option["arg"] = "integer";
+diff --git a/src/input/input_srt.cpp b/src/input/input_srt.cpp
+index b89fa32e..22c1c3ee 100644
+--- a/src/input/input_srt.cpp
++++ b/src/input/input_srt.cpp
+@@ -10,7 +10,7 @@ namespace Mist{
+ capa["source_match"].append("/*.srt");
+ capa["source_match"].append("/*.vtt");
+ capa["priority"] = 9;
+- capa["codecs"][0u][0u].append("subtitle");
++ capa["codecs"]["subtitle"].append("subtitle");
+ }
+
+ bool InputSrt::preRun(){
+diff --git a/src/input/input_ts.cpp b/src/input/input_ts.cpp
+index e405e3b6..3bb02806 100644
+--- a/src/input/input_ts.cpp
++++ b/src/input/input_ts.cpp
+@@ -191,14 +191,14 @@ namespace Mist{
+ capa["incoming_push_url"] = "udp://$host:$port";
+ capa["incoming_push_url_match"] = "tsudp://*";
+ capa["priority"] = 9;
+- capa["codecs"][0u][0u].append("H264");
+- capa["codecs"][0u][0u].append("HEVC");
+- capa["codecs"][0u][0u].append("MPEG2");
+- capa["codecs"][0u][1u].append("AAC");
+- capa["codecs"][0u][1u].append("AC3");
+- capa["codecs"][0u][1u].append("MP2");
+- capa["codecs"][0u][1u].append("opus");
+- capa["codecs"][1u][0u].append("rawts");
++ capa["codecs"]["video"].append("H264");
++ capa["codecs"]["video"].append("HEVC");
++ capa["codecs"]["video"].append("MPEG2");
++ capa["codecs"]["audio"].append("AAC");
++ capa["codecs"]["audio"].append("AC3");
++ capa["codecs"]["audio"].append("MP2");
++ capa["codecs"]["audio"].append("opus");
++ capa["codecs"]["passthrough"].append("rawts");
+ inFile = NULL;
+ inputProcess = 0;
+ isFinished = false;
+diff --git a/src/input/input_tsrist.cpp b/src/input/input_tsrist.cpp
+index df658da1..33f1155e 100644
+--- a/src/input/input_tsrist.cpp
++++ b/src/input/input_tsrist.cpp
+@@ -92,15 +92,15 @@ namespace Mist{
+ // These can/may be set to always-on mode
+ capa["always_match"].append("rist://*");
+ capa["priority"] = 9;
+- capa["codecs"][0u][0u].append("H264");
+- capa["codecs"][0u][0u].append("HEVC");
+- capa["codecs"][0u][0u].append("MPEG2");
+- capa["codecs"][0u][1u].append("AAC");
+- capa["codecs"][0u][1u].append("MP3");
+- capa["codecs"][0u][1u].append("AC3");
+- capa["codecs"][0u][1u].append("MP2");
+- capa["codecs"][0u][1u].append("opus");
+- capa["codecs"][1u][0u].append("rawts");
++ capa["codecs"]["video"].append("H264");
++ capa["codecs"]["video"].append("HEVC");
++ capa["codecs"]["video"].append("MPEG2");
++ capa["codecs"]["audio"].append("AAC");
++ capa["codecs"]["audio"].append("MP3");
++ capa["codecs"]["audio"].append("AC3");
++ capa["codecs"]["audio"].append("MP2");
++ capa["codecs"]["audio"].append("opus");
++ capa["codecs"]["passthrough"].append("rawts");
+
+ JSON::Value option;
+ option["arg"] = "integer";
+diff --git a/src/input/input_tssrt.cpp b/src/input/input_tssrt.cpp
+index 8fef6d7d..28342930 100644
+--- a/src/input/input_tssrt.cpp
++++ b/src/input/input_tssrt.cpp
+@@ -61,15 +61,15 @@ namespace Mist{
+ capa["incoming_push_url"] = "srt://$host:$port";
+ capa["incoming_push_url_match"] = "srt://*";
+ capa["priority"] = 9;
+- capa["codecs"][0u][0u].append("H264");
+- capa["codecs"][0u][0u].append("HEVC");
+- capa["codecs"][0u][0u].append("MPEG2");
+- capa["codecs"][0u][1u].append("AAC");
+- capa["codecs"][0u][1u].append("MP3");
+- capa["codecs"][0u][1u].append("AC3");
+- capa["codecs"][0u][1u].append("MP2");
+- capa["codecs"][0u][1u].append("opus");
+- capa["codecs"][1u][0u].append("rawts");
++ capa["codecs"]["video"].append("H264");
++ capa["codecs"]["video"].append("HEVC");
++ capa["codecs"]["video"].append("MPEG2");
++ capa["codecs"]["audio"].append("AAC");
++ capa["codecs"]["audio"].append("MP3");
++ capa["codecs"]["audio"].append("AC3");
++ capa["codecs"]["audio"].append("MP2");
++ capa["codecs"]["audio"].append("opus");
++ capa["codecs"]["passthrough"].append("rawts");
+
+ JSON::Value option;
+ option["arg"] = "integer";
+--
+2.25.1
+
+
+From 067444306671531d3df3d39ad27d5df7cf14f3cc Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Mon, 10 Oct 2022 14:16:48 +0200
+Subject: [PATCH 30/38] Fix push outputs to use the correct remote addresses
+
+---
+ lib/socket.cpp | 2 +-
+ lib/socket_srt.cpp | 1 +
+ src/output/output.cpp | 7 +++----
+ src/output/output_cmaf.cpp | 1 +
+ src/output/output_rtmp.cpp | 1 +
+ src/output/output_ts.cpp | 1 +
+ src/output/output_tsrist.cpp | 1 +
+ 7 files changed, 9 insertions(+), 5 deletions(-)
+
+diff --git a/lib/socket.cpp b/lib/socket.cpp
+index 47b2b6bd..aaefa2f2 100644
+--- a/lib/socket.cpp
++++ b/lib/socket.cpp
+@@ -1207,7 +1207,7 @@ std::string Socket::Connection::getBinHost(){
+ }
+
+ /// Sets hostname for connection manually.
+-/// Overwrites the detected host, thus possibily making it incorrect.
++/// Overwrites the detected host, thus possibly making it incorrect.
+ void Socket::Connection::setHost(std::string host){
+ remotehost = host;
+ struct addrinfo *result, hints;
+diff --git a/lib/socket_srt.cpp b/lib/socket_srt.cpp
+index 6d1d5695..ac08dd8c 100644
+--- a/lib/socket_srt.cpp
++++ b/lib/socket_srt.cpp
+@@ -208,6 +208,7 @@ namespace Socket{
+ if (outgoing_port){setupAdapter("", outgoing_port);}
+
+ sockaddr_in sa = createInetAddr(_host, _port);
++ memcpy(&remoteaddr, &sa, sizeof(sockaddr_in));
+ sockaddr *psa = (sockaddr *)&sa;
+
+ HIGH_MSG("Going to connect sock %d", sock);
+diff --git a/src/output/output.cpp b/src/output/output.cpp
+index d525b8ea..307c6b8c 100644
+--- a/src/output/output.cpp
++++ b/src/output/output.cpp
+@@ -218,10 +218,9 @@ namespace Mist{
+
+ std::string Output::getConnectedBinHost(){
+ if (!prevHost.size()){
+- if (myConn && myConn.getPureSocket() != -1){
+- prevHost = myConn.getBinHost();
+- }
+- if (!prevHost.size()){prevHost.assign("\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\001", 16);}
++ MEDIUM_MSG("Setting prevHost to %s", getConnectedHost().c_str());
++ prevHost = myConn.getBinHost();
++ if (!prevHost.size()){prevHost.assign("\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", 16);}
+ }
+ return prevHost;
+ }
+diff --git a/src/output/output_cmaf.cpp b/src/output/output_cmaf.cpp
+index eb7c87da..fa046db5 100644
+--- a/src/output/output_cmaf.cpp
++++ b/src/output/output_cmaf.cpp
+@@ -93,6 +93,7 @@ namespace Mist{
+
+ INFO_MSG("About to push stream %s out. Host: %s, port: %" PRIu32 ", location: %s",
+ streamName.c_str(), pushUrl.host.c_str(), pushUrl.getPort(), pushUrl.path.c_str());
++ myConn.setHost(pushUrl.host);
+ initialize();
+ initialSeek();
+ startPushOut();
+diff --git a/src/output/output_rtmp.cpp b/src/output/output_rtmp.cpp
+index 23194f4a..41cf2191 100644
+--- a/src/output/output_rtmp.cpp
++++ b/src/output/output_rtmp.cpp
+@@ -54,6 +54,7 @@ namespace Mist{
+ }
+ INFO_MSG("About to push stream %s out. Host: %s, port: %d, app: %s, stream: %s", streamName.c_str(),
+ pushUrl.host.c_str(), pushUrl.getPort(), app.c_str(), streamOut.c_str());
++ myConn.setHost(pushUrl.host);
+ initialize();
+ initialSeek();
+ startPushOut("");
+diff --git a/src/output/output_ts.cpp b/src/output/output_ts.cpp
+index bf46663b..cec6eee4 100644
+--- a/src/output/output_ts.cpp
++++ b/src/output/output_ts.cpp
+@@ -85,6 +85,7 @@ namespace Mist{
+ }
+ }
+ pushSock.SetDestination(target.host, target.getPort());
++ myConn.setHost(target.host);
+ pushing = false;
+ }else{
+ //No push target? Check if this is a push input or pull output by waiting for data for 5s
+diff --git a/src/output/output_tsrist.cpp b/src/output/output_tsrist.cpp
+index 10b29673..a2861184 100644
+--- a/src/output/output_tsrist.cpp
++++ b/src/output/output_tsrist.cpp
+@@ -130,6 +130,7 @@ namespace Mist{
+ onFail("Failed to start RIST connection");
+ return;
+ }
++ myConn.setHost(target.host);
+ wantRequest = false;
+ parseData = true;
+ initialize();
+--
+2.25.1
+
+
+From 8175ad6dd54a2d59393257d80fc96f7849bfbdcb Mon Sep 17 00:00:00 2001
+From: Thulinma <jaron@vietors.com>
+Date: Thu, 13 Oct 2022 13:24:20 +0200
+Subject: [PATCH 31/38] Fix token handling when multiple sessions are
+ multiplexed over a single HTTP connection
+
+---
+ src/output/output_http.cpp | 55 ++++++++++++++++++++------------------
+ 1 file changed, 29 insertions(+), 26 deletions(-)
+
+diff --git a/src/output/output_http.cpp b/src/output/output_http.cpp
+index ed8fa33b..6d49c5c6 100644
+--- a/src/output/output_http.cpp
++++ b/src/output/output_http.cpp
+@@ -218,6 +218,34 @@ namespace Mist{
+ return;
+ }
+
++ tkn.clear();
++ // Read the session token
++ if (Comms::tknMode & 0x01){
++ // Get session token from the request url
++ if (H.GetVar("tkn") != ""){
++ tkn = H.GetVar("tkn");
++ } else if (H.GetVar("sid") != ""){
++ tkn = H.GetVar("sid");
++ } else if (H.GetVar("sessId") != ""){
++ tkn = H.GetVar("sessId");
++ }
++ }
++ if ((Comms::tknMode & 0x02) && !tkn.size()){
++ // Get session token from the request cookie
++ std::map<std::string, std::string> storage;
++ const std::string koekjes = H.GetHeader("Cookie");
++ HTTP::parseVars(koekjes, storage, "; ");
++ if (storage.count("tkn")){
++ tkn = storage.at("tkn");
++ }
++ }
++ // Generate a session token if it is being sent as a cookie or url parameter and we couldn't read one
++ if (!tkn.size() && Comms::tknMode > 3){
++ const std::string newTkn = UA + JSON::Value(getpid()).asString();
++ tkn = JSON::Value(checksum::crc32(0, newTkn.data(), newTkn.size())).asString();
++ HIGH_MSG("Generated tkn '%s'", tkn.c_str());
++ }
++
+ //Check if we need to change binary and/or reconnect
+ if (handler != capa["name"].asStringRef() || H.GetVar("stream") != streamName || (statComm && (statComm.getHost() != getConnectedBinHost() || statComm.getTkn() != tkn))){
+ MEDIUM_MSG("Switching from %s (%s) to %s (%s)", capa["name"].asStringRef().c_str(),
+@@ -270,32 +298,7 @@ namespace Mist{
+ realTime = 0;
+ }
+ }
+- // Read the session token
+- if (Comms::tknMode & 0x01){
+- // Get session token from the request url
+- if (H.GetVar("tkn") != ""){
+- tkn = H.GetVar("tkn");
+- } else if (H.GetVar("sid") != ""){
+- tkn = H.GetVar("sid");
+- } else if (H.GetVar("sessId") != ""){
+- tkn = H.GetVar("sessId");
+- }
+- }
+- if ((Comms::tknMode & 0x02) && !tkn.size()){
+- // Get session token from the request cookie
+- std::map<std::string, std::string> storage;
+- const std::string koekjes = H.GetHeader("Cookie");
+- HTTP::parseVars(koekjes, storage, "; ");
+- if (storage.count("tkn")){
+- tkn = storage.at("tkn");
+- }
+- }
+- // Generate a session token if it is being sent as a cookie or url parameter and we couldn't read one
+- if (!tkn.size() && Comms::tknMode > 3){
+- const std::string newTkn = UA + JSON::Value(getpid()).asString();
+- tkn = JSON::Value(checksum::crc32(0, newTkn.data(), newTkn.size())).asString();
+- HIGH_MSG("Generated tkn '%s'", tkn.c_str());
+- }
++
+ // Handle upgrade to websocket if the output supports it
+ std::string upgradeHeader = H.GetHeader("Upgrade");
+ Util::stringToLower(upgradeHeader);
+--
+2.25.1
+
+
+From fb6de1742bc04cf53ef49008166cfecc36b5e96c Mon Sep 17 00:00:00 2001
+From: Gijs Peskens <gijs@peskens.net>
+Date: Tue, 20 Sep 2022 15:40:08 +0200
+Subject: [PATCH 32/38] Add building via meson
+
+---
+ generated/make_html.cpp | 39 +++++++++
+ generated/meson.build | 40 ++++++++++
+ lib/meson.build | 135 +++++++++++++++++++++++++++++++
+ lsp/meson.build | 26 ++++++
+ meson.build | 157 +++++++++++++++++++++++++++++++++++++
+ meson_options.txt | 30 +++++++
+ mist/meson.build | 5 ++
+ src/analysers/meson.build | 39 +++++++++
+ src/controller/meson.build | 21 +++++
+ src/input/meson.build | 85 ++++++++++++++++++++
+ src/meson.build | 19 +++++
+ src/output/meson.build | 133 +++++++++++++++++++++++++++++++
+ src/process/meson.build | 50 ++++++++++++
+ src/utils/meson.build | 28 +++++++
+ test/meson.build | 18 +++++
+ 15 files changed, 825 insertions(+)
+ create mode 100644 generated/make_html.cpp
+ create mode 100644 generated/meson.build
+ create mode 100644 lib/meson.build
+ create mode 100644 lsp/meson.build
+ create mode 100644 meson.build
+ create mode 100644 meson_options.txt
+ create mode 100644 mist/meson.build
+ create mode 100644 src/analysers/meson.build
+ create mode 100644 src/controller/meson.build
+ create mode 100644 src/input/meson.build
+ create mode 100644 src/meson.build
+ create mode 100644 src/output/meson.build
+ create mode 100644 src/process/meson.build
+ create mode 100644 src/utils/meson.build
+ create mode 100644 test/meson.build
+
+diff --git a/generated/make_html.cpp b/generated/make_html.cpp
+new file mode 100644
+index 00000000..0d986eb6
+--- /dev/null
++++ b/generated/make_html.cpp
+@@ -0,0 +1,39 @@
++#include <cstdlib>
++#include <ios>
++#include <iostream>
++#include <fstream>
++#include <ostream>
++
++int main(int argc, char* argv[]) {
++ if (argc < 6) {
++ std::cerr << "ERROR EXPECTED MORE FILES" << std::endl;
++ return 1;
++ }
++
++ std::ofstream output(argv[1]);
++
++ std::ifstream header(argv[2]);
++ std::ifstream css(argv[3]);
++ std::ifstream footer(argv[4]);
++
++ if (!header.is_open() || !css.is_open() || !footer.is_open()) {
++ std::cerr << "ERROR Couldn't open file" << std::endl;
++ return 1;
++ }
++
++ output << header.rdbuf();
++ output << "<script>";
++ for (int i = 5; i < argc; i++) {
++ std::ifstream script(argv[i]);
++ if (!script.is_open()) {
++ std::cerr << "ERROR Couldn't open file" << std::endl;
++ return 1;
++ }
++ output << script.rdbuf();
++ }
++ output << "</script>";
++ output << "<style>" << css.rdbuf() << "</style>";
++ output << footer.rdbuf();
++ std::flush(output);
++ return 0;
++}
+\ No newline at end of file
+diff --git a/generated/meson.build b/generated/meson.build
+new file mode 100644
+index 00000000..30296c35
+--- /dev/null
++++ b/generated/meson.build
+@@ -0,0 +1,40 @@
++
++sourcery = executable('sourcery', '../src/sourcery.cpp', native: true)
++
++make_html = executable('make_html', 'make_html.cpp', native: true)
++
++gen_html = custom_target('gen_html', output: 'server.html', input: html_files, command: [make_html, '@OUTPUT@', '@INPUT@'])
++
++embed_files = [
++ {'infile': '../embed/min/player.js', 'variable': 'player_js', 'outfile': 'player.js.h'},
++ {'infile': '../embed/min/wrappers/html5.js', 'variable': 'html5_js', 'outfile': 'html5.js.h'},
++ {'infile': '../embed/min/wrappers/flash_strobe.js', 'variable': 'flash_strobe_js', 'outfile': 'flash_strobe.js.h'},
++ {'infile': '../embed/min/wrappers/dashjs.js', 'variable': 'dash_js', 'outfile': 'dashjs.js.h'},
++ {'infile': '../embed/min/wrappers/videojs.js', 'variable': 'video_js', 'outfile': 'videojs.js.h'},
++ {'infile': '../embed/min/wrappers/webrtc.js', 'variable': 'webrtc_js', 'outfile': 'webrtc.js.h'},
++ {'infile': '../embed/min/wrappers/mews.js', 'variable': 'mews_js', 'outfile': 'mews.js.h'},
++ {'infile': '../embed/min/wrappers/flv.js', 'variable': 'flv_js', 'outfile': 'flv.js.h'},
++ {'infile': '../embed/min/wrappers/hlsjs.js', 'variable': 'hlsjs_js', 'outfile': 'hlsjs.js.h'},
++ {'infile': '../embed/min/wrappers/rawws.js', 'variable': 'rawws_js', 'outfile': 'rawws.js.h'},
++ {'infile': '../embed/players/dash.js.license.js', 'variable': 'player_dash_lic_js','outfile': 'player_dash_lic.js.h'},
++ {'infile': '../embed/players/dash.all.min.js', 'variable': 'player_dash_js', 'outfile': 'player_dash.js.h'},
++ {'infile': '../embed/players/video.min.js', 'variable': 'player_video_js', 'outfile': 'player_video.js.h'},
++ {'infile': '../embed/players/webrtc.js', 'variable': 'player_webrtc_js', 'outfile': 'player_webrtc.js.h'},
++ {'infile': '../embed/players/flv.min.js', 'variable': 'player_flv_js', 'outfile': 'player_flv.js.h'},
++ {'infile': '../embed/players/hls.js', 'variable': 'player_hlsjs_js', 'outfile': 'player_hlsjs.js.h'},
++ {'infile': '../embed/players/libde265.min.js', 'variable': 'player_libde265_js','outfile': 'player_libde265.js.h'},
++ {'infile': '../embed/min/skins/default.css', 'variable': 'skin_default_css', 'outfile': 'skin_default.css.h'},
++ {'infile': '../embed/min/skins/dev.css', 'variable': 'skin_dev_css', 'outfile': 'skin_dev.css.h'},
++ {'infile': '../embed/skins/video-js.css', 'variable': 'skin_videojs_css', 'outfile': 'skin_videojs.css.h'},
++ {'infile': '../src/output/noffmpeg.jpg', 'variable': 'noffmpeg', 'outfile': 'noffmpeg.h'},
++ {'infile': '../src/output/noh264.jpg', 'variable': 'noh264', 'outfile': 'noh264.h'},
++]
++
++embed_tgts = []
++
++foreach e : embed_files
++ embed_tgts += custom_target('embed_'+e.get('outfile'), output: e.get('outfile'), input: e.get('infile'), command: [sourcery, '@INPUT@', e.get('variable'), '@OUTPUT@'])
++endforeach
++
++
++server_html = custom_target('embed_server.html', output: 'server.html.h', input: gen_html, command: [sourcery, '@INPUT@', 'server_html', '@OUTPUT@'])
+diff --git a/lib/meson.build b/lib/meson.build
+new file mode 100644
+index 00000000..02a40350
+--- /dev/null
++++ b/lib/meson.build
+@@ -0,0 +1,135 @@
++
++headers = [
++ 'adts.h',
++ 'amf.h',
++ 'auth.h',
++ 'encode.h',
++ 'bitfields.h',
++ 'bitstream.h',
++ 'certificate.h',
++ 'checksum.h',
++ 'cmaf.h',
++ 'comms.h',
++ 'config.h',
++ 'defines.h',
++ 'dtls_srtp_handshake.h',
++ 'dtsc.h',
++ 'encryption.h',
++ 'flv_tag.h',
++ 'h264.h',
++ 'h265.h',
++ 'hls_support.h',
++ 'http_parser.h',
++ 'downloader.h',
++ 'json.h',
++ 'langcodes.h',
++ 'mp4_adobe.h',
++ 'mp4_dash.h',
++ 'mp4_encryption.h',
++ 'mp4_generic.h',
++ 'mp4.h',
++ 'mp4_ms.h',
++ 'mpeg.h',
++ 'nal.h',
++ 'ogg.h',
++ 'procs.h',
++ 'rtmpchunks.h',
++ 'rtp_fec.h',
++ 'rtp.h',
++ 'sdp.h',
++ 'sdp_media.h',
++ 'shared_memory.h',
++ 'socket.h',
++ 'srtp.h',
++ 'stream.h',
++ 'stun.h',
++ 'theora.h',
++ 'timing.h',
++ 'tinythread.h',
++ 'ts_packet.h',
++ 'ts_stream.h',
++ 'util.h',
++ 'vorbis.h',
++ 'triggers.h',
++ 'opus.h',
++ 'riff.h',
++ 'ebml.h',
++ 'ebml_socketglue.h',
++ 'websocket.h',
++ 'url.h',
++ 'urireader.h',
++]
++
++if have_srt
++ headers += 'socket_srt.h'
++endif
++
++install_headers(headers, subdir: 'mist')
++
++extra_code = []
++
++if usessl
++ extra_code += ['dtls_srtp_handshake.cpp', 'stun.cpp', 'certificate.cpp', 'encryption.cpp', 'srtp.cpp',]
++endif
++
++libmist = library('mist',
++ 'adts.cpp',
++ 'amf.cpp',
++ 'auth.cpp',
++ 'encode.cpp',
++ 'bitfields.cpp',
++ 'bitstream.cpp',
++ 'cmaf.cpp',
++ 'comms.cpp',
++ 'config.cpp',
++ 'dtsc.cpp',
++ 'flv_tag.cpp',
++ 'h264.cpp',
++ 'h265.cpp',
++ 'hls_support.cpp',
++ 'http_parser.cpp',
++ 'downloader.cpp',
++ 'json.cpp',
++ 'langcodes.cpp',
++ 'mp4_adobe.cpp',
++ 'mp4.cpp',
++ 'mp4_dash.cpp',
++ 'mp4_encryption.cpp',
++ 'mp4_generic.cpp',
++ 'mp4_ms.cpp',
++ 'mpeg.cpp',
++ 'nal.cpp',
++ 'ogg.cpp',
++ 'procs.cpp',
++ 'rtmpchunks.cpp',
++ 'rtp_fec.cpp',
++ 'rtp.cpp',
++ 'sdp.cpp',
++ 'sdp_media.cpp',
++ 'shared_memory.cpp',
++ 'socket.cpp',
++ 'stream.cpp',
++ 'theora.cpp',
++ 'timing.cpp',
++ 'tinythread.cpp',
++ 'ts_packet.cpp',
++ 'ts_stream.cpp',
++ 'util.cpp',
++ 'vorbis.cpp',
++ 'triggers.cpp',
++ 'opus.cpp',
++ 'riff.cpp',
++ 'ebml.cpp',
++ 'ebml_socketglue.cpp',
++ 'url.cpp',
++ 'urireader.cpp',
++ 'websocket.cpp',
++ extra_code,
++ include_directories: incroot,
++ dependencies: ssl_deps,
++ install: true,
++)
++
++if have_srt
++ libmist_srt = library('mist_srt', 'socket_srt.cpp', include_directories: incroot, link_with: libmist, dependencies: libsrt, install: true)
++endif
+\ No newline at end of file
+diff --git a/lsp/meson.build b/lsp/meson.build
+new file mode 100644
+index 00000000..e5a6b36a
+--- /dev/null
++++ b/lsp/meson.build
+@@ -0,0 +1,26 @@
++minified = files('minified.js')
++
++if get_option('LSP_MINIFY')
++ java = find_program('java', required: true)
++
++ closure_compiler = files('closure-compiler.jar')
++
++ minified = custom_target('lsp_gen_minified',output: 'minified.js', input: ['plugins/md5.js', 'plugins/cattablesort.js', 'mist.js'], command: [java, '-jar', closure_compiler, '--warning_level', 'QUIET', '@INPUT@'], capture: true)
++endif
++
++html_list = ['header.html',
++ 'main.css',
++ 'footer.html',
++ 'plugins/jquery.js',
++ 'plugins/jquery.flot.min.js',
++ 'plugins/jquery.flot.time.min.js',
++ 'plugins/jquery.qrcode.min.js',
++ ]
++
++html_files = files(html_list)
++html_files += minified
++
++if not get_option('NOGA')
++ html_files += files('analytics.js')
++endif
++
+diff --git a/meson.build b/meson.build
+new file mode 100644
+index 00000000..38bd29c3
+--- /dev/null
++++ b/meson.build
+@@ -0,0 +1,157 @@
++project('mistserver', 'cpp', default_options: ['cpp_std=gnu++03'])
++
++ccpp = meson.get_compiler('cpp')
++
++incroot = include_directories('.')
++
++release = get_option('RELEASE')
++if release.contains('DEFAULT')
++ release = 'Generic_'+target_machine.cpu_family()
++endif
++release = release.strip()
++
++rv = run_command('git', 'describe', '--tags', check: false)
++version = rv.stdout().strip()
++if rv.returncode() != 0
++ version = 'Unknown'
++endif
++
++add_project_arguments(['-funsigned-char', '-D_LARGEFILE_SOURCE','-Wno-sign-compare', '-Wparentheses', '-Wno-non-virtual-dtor', '-Wno-strict-aliasing'], language: 'cpp')
++
++string_opt = '-D@0@="@1@"'
++int_opt = '-D@0@=@1@'
++
++option_defines = [
++ string_opt.format('APPNAME', get_option('APPNAME')),
++ int_opt.format('DEBUG', get_option('DEBUG')),
++ string_opt.format('RELEASE' ,release),
++ string_opt.format('PACKAGE_VERSION' ,version),
++ int_opt.format('SHM_DATASIZE', get_option('DATASIZE')),
++ int_opt.format('STAT_CUTOFF', get_option('STAT_CUTOFF')),
++ int_opt.format('STATS_DELAY', get_option('STATS_DELAY')),
++ string_opt.format('UDP_API_HOST' ,get_option('UDP_API_HOST')),
++ int_opt.format('UDP_API_PORT', get_option('UDP_API_PORT')),
++]
++
++if not get_option('NOSHM')
++ option_defines += '-DSHM_ENABLED=1'
++else
++ message('Shared memory use is turned OFF')
++endif
++
++if not get_option('FILLER_DATA').contains('DEFAULT') and not get_option('SHARED_SECRET').contains('DEFAULT') and not get_option('SUPER_SECRET').contains('DEFAULT')
++ option_defines += [
++ string_opt.format('FILLER_DATA' ,get_option('FILLER_DATA')),
++ string_opt.format('SHARED_SECRET' ,get_option('SHARED_SECRET')),
++ string_opt.format('SUPER_SECRET',get_option('SUPER_SECRET')),
++ ]
++endif
++
++#Since a lot of checks in the code are in #ifdef format (vs simple #if) we unfortunately have to do a lot of conditionals here.. otherwise it would be a one liner like this:
++#option_defines += int_opt.format('GEOIP', get_option('GEOIP').to_int())
++if get_option('GEOIP')
++ option_defines += '-DGEOIP=1'
++endif
++
++usessl = true
++if get_option('NOSSL')
++ message('SSL/TLS support is turned OFF')
++ usessl = false
++ option_defines += '-DNOSSL=1'
++else
++ option_defines += '-DSSL=1'
++endif
++
++if not get_option('NOUPDATE')
++ option_defines += '-DUPDATER=1'
++endif
++
++if not get_option('PERPETUAL')
++ option_defines += '-DLICENSING=1'
++endif
++
++if get_option('NOAUTH')
++ option_defines += '-DNOAUTH=1'
++endif
++
++if get_option('KILLONEXIT')
++ option_defines += '-DKILLONEXIT=true'
++endif
++
++if not get_option('DISKSERIAL').contains('DEFAULT')
++ option_defines +=string_opt.format('DISKSERIAL',get_option('DISKSERIAL'))
++endif
++
++if get_option('WITH_THREADNAMES')
++ option_defines += '-DWITH_THREADNAMES=1'
++endif
++
++if get_option('NOCRASHCHECK')
++ option_defines += '-DNOCRASHCHECK=1'
++endif
++
++if get_option('NOLLHLS')
++ option_defines += '-DNOLLHLS=1'
++endif
++
++message('Builing release @0@ for version @1@ @ debug level @2@'.format(release, version, get_option('DEBUG')))
++
++libsrt = false
++if not get_option('NOSRT')
++ libsrt = dependency('srt', required: false)
++endif
++librist = false
++if not get_option('NORIST')
++ librist = dependency('librist', required: false)
++endif
++
++ssl_deps = []
++
++have_librist = not get_option('NORIST') and librist.found()
++have_srt = not get_option('NOSRT') and libsrt.found()
++
++if usessl
++ mbedtls = ccpp.find_library('mbedtls')
++ mbedx509 = ccpp.find_library('mbedx509')
++ mbedcrypto = ccpp.find_library('mbedcrypto')
++ srtp2 = dependency('libsrtp2')
++ ssl_deps = [mbedtls, mbedx509, mbedcrypto, srtp2]
++endif
++
++add_project_arguments(option_defines, language: 'cpp')
++
++executables = []
++
++subdir('lib')
++subdir('mist')#This "generates" the headers..
++subdir('lsp')
++subdir('generated')
++subdir('src')
++subdir('test')
++
++exec_tgts = []
++
++##This makes sure all (installable) executables are build in top level directory
++##Needed to make testing simple, since MistController expects it's binaries to be next to it
++foreach exec : executables
++ exec_tgts += executable(
++ exec.get('name'),
++ exec.get('sources'),
++ link_with: exec.get('link'),
++ dependencies: exec.get('deps'),
++ cpp_args: exec.get('defines'),
++ install: true,
++ )
++endforeach
++
++doxygen = find_program('doxygen', required: false)
++if doxygen.found()
++ doxyfile = configure_file(output: 'Doxyfile', input: 'Doxyfile.in', configuration: {
++ 'PACKAGE_VERSION': version,
++ 'RELEASE' : release,
++ })
++ layout = custom_target('copy_layout', input: 'DoxygenLayout.xml', output: 'DoxygenLayout.xml', command: ['cp', '@INPUT@', '@OUTPUT@'])
++ run_target('docs', command: [doxygen, doxyfile], depends: layout)
++endif
++
++#docs = custom_target('docs', )
+diff --git a/meson_options.txt b/meson_options.txt
+new file mode 100644
+index 00000000..23600e85
+--- /dev/null
++++ b/meson_options.txt
+@@ -0,0 +1,30 @@
++option('NOSHM', description: 'Disabled shared memory (falling back to shared temporary files)', type : 'boolean', value : false)
++option('GEOIP', description: 'Enable GeoIP capabilities (deprecated)', type : 'boolean', value : false)
++option('NOSSL', description: 'Disable SSL/TLS support', type : 'boolean', value : false)
++option('NOUPDATE', description: 'Disable the updater', type : 'boolean', value : false)
++option('PERPETUAL', description: 'Disable the licensing system', type : 'boolean', value : false)
++option('NOAUTH', description: 'Disable API authentication entirely (insecure!)', type : 'boolean', value : false)
++option('KILLONEXIT', description: 'Kill all processes on exit, ensuring nothing is running anymore (disables rolling restart/update support)', type : 'boolean', value : false)
++option('WITH_THREADNAMES', description: 'Enable fancy names for threads (not supported on all platforms)', type : 'boolean', value : false)
++option('NOCRASHCHECK', description: 'Disables the crash check in the controller stats and input userpages. Prevents killing processes that are stalled/stuck.', type : 'boolean', value : false)
++option('NOLLHLS', description: 'Disable LLHLS', type : 'boolean', value : false)
++option('FILLER_DATA', type: 'string', value: 'DEFAULT')
++option('SHARED_SECRET', type: 'string', value: 'DEFAULT')
++option('SUPER_SECRET', type: 'string', value: 'DEFAULT')
++option('UDP_API_HOST', type: 'string', value: 'localhost')
++option('UDP_API_PORT', type: 'integer', value: 4242)
++option('DISKSERIAL', description: 'Lock application to given disk serial number', type: 'string', value: 'DEFAULT')
++option('STATS_DELAY', type: 'integer', value: 15)
++option('APPNAME', description: 'Name of the application', type: 'string', value: 'MistServer')
++option('DATASIZE', type: 'integer', value: 40)
++option('STAT_CUTOFF', type: 'integer', value: 600)
++option('NORIST', description: 'Disable building RIST support, regardless of library being present (by default RIST is enabled if libraries are installed)', type : 'boolean', value : false)
++option('NOSRT', description: 'Disable building SRT support, regardless of library being present (by default SRT is enabled if libraries are installed)', type : 'boolean', value : false)
++option('RELEASE', type: 'string', value: 'DEFAULT')
++option('DEBUG', type: 'integer', value: 4)
++option('NOGA', description: 'Disables Google Analytics entirely in the LSP', type: 'boolean', value: false)
++option('LOAD_BALANCE', description: 'Build the load balancer', type: 'boolean', value: false)
++option('WITH_AV', description: 'Build a generic libav-based input (not distributable!)', type: 'boolean', value: false)
++option('WITH_JPG', description: 'Build JPG thumbnailer output support', type: 'boolean', value: false)
++option('WITH_SANITY', description: 'Enable MistOutSanityCheck output for testing purposes', type: 'boolean', value: false)
++option('LSP_MINIFY', description: 'Try to minify LSP JS via java closure-compiler, generally not needed unless changing JS code', type: 'boolean', value: false)
+\ No newline at end of file
+diff --git a/mist/meson.build b/mist/meson.build
+new file mode 100644
+index 00000000..fe6c5863
+--- /dev/null
++++ b/mist/meson.build
+@@ -0,0 +1,5 @@
++header_tgts = []
++
++foreach header : headers
++ header_tgts += custom_target('copy'+header, input: join_paths('../lib', header), output: header, command: ['cp', '@INPUT@', '@OUTPUT@'])
++endforeach
+\ No newline at end of file
+diff --git a/src/analysers/meson.build b/src/analysers/meson.build
+new file mode 100644
+index 00000000..64f74271
+--- /dev/null
++++ b/src/analysers/meson.build
+@@ -0,0 +1,39 @@
++
++analysers = [
++ {'name': 'RTMP', 'format': 'rtmp'},
++ {'name': 'FLV', 'format': 'flv'},
++ {'name': 'DTSC', 'format': 'dtsc'},
++ {'name': 'OGG', 'format': 'ogg'},
++ {'name': 'EBML', 'format': 'ebml'},
++ {'name': 'TS', 'format': 'ts'},
++ {'name': 'MP4', 'format': 'mp4'},
++ {'name': 'H264', 'format': 'h264'},
++ {'name': 'HLS', 'format': 'hls'},
++ {'name': 'RIFF', 'format': 'riff'},
++ {'name': 'RTSP', 'format': 'rtsp'},
++]
++
++foreach analyser : analysers
++ executables += { 'name': 'MistAnalyser'+analyser.get('name'),
++ 'sources' : [
++ files('mist_analyse.cpp',
++ 'analyser.cpp',
++ 'analyser_'+analyser.get('format')+'.cpp'),
++ header_tgts,
++ ],
++ 'link': libmist,
++ 'defines': [
++ string_opt.format('ANALYSERHEADER', 'analyser_'+analyser.get('format')+'.h'),
++ '-DANALYSERTYPE=Analyser'+analyser.get('name')
++ ],
++ 'deps': []
++ }
++endforeach
++
++executables += {
++ 'name': 'MistTranslateH264',
++ 'sources' : [files('h264_translate.cpp'), header_tgts],
++ 'link': libmist,
++ 'defines': [],
++ 'deps' : []
++}
+\ No newline at end of file
+diff --git a/src/controller/meson.build b/src/controller/meson.build
+new file mode 100644
+index 00000000..757dcd3d
+--- /dev/null
++++ b/src/controller/meson.build
+@@ -0,0 +1,21 @@
++
++executables += {
++ 'name': 'MistController',
++ 'sources' : [
++ files( 'controller.cpp',
++ 'controller_updater.cpp',
++ 'controller_streams.cpp',
++ 'controller_storage.cpp',
++ 'controller_connectors.cpp',
++ 'controller_statistics.cpp',
++ 'controller_limits.cpp',
++ 'controller_capabilities.cpp',
++ 'controller_uplink.cpp',
++ 'controller_api.cpp',
++ 'controller_push.cpp'),
++ header_tgts,
++ server_html],
++ 'link': libmist,
++ 'defines': [],
++ 'deps' : []
++}
+\ No newline at end of file
+diff --git a/src/input/meson.build b/src/input/meson.build
+new file mode 100644
+index 00000000..02d6891c
+--- /dev/null
++++ b/src/input/meson.build
+@@ -0,0 +1,85 @@
++inputs = [
++ {'name' : 'HLS', 'format' : 'hls'},
++ {'name' : 'DTSC', 'format' : 'dtsc'},
++ {'name' : 'MP3', 'format' : 'mp3'},
++ {'name' : 'FLV', 'format' : 'flv'},
++ {'name' : 'OGG', 'format' : 'ogg'},
++ {'name' : 'Buffer', 'format' : 'buffer'},
++ {'name' : 'H264', 'format' : 'h264'},
++ {'name' : 'EBML', 'format' : 'ebml'},
++ {'name' : 'ISMV', 'format' : 'ismv'},
++ {'name' : 'MP4', 'format' : 'mp4'},
++ {'name' : 'TS', 'format' : 'ts'},
++ {'name' : 'Folder', 'format' : 'folder'},
++ {'name' : 'Playlist', 'format' : 'playlist'},
++ {'name' : 'Balancer', 'format' : 'balancer'},
++ {'name' : 'RTSP', 'format' : 'rtsp'},
++ {'name' : 'SRT', 'format' : 'srt'},
++ {'name' : 'SDP', 'format' : 'sdp'},
++ #{'name' : 'AAC', 'format' : 'aac'},
++]
++
++#Referenced by process targets
++input_ebml_cpp = files('input_ebml.cpp')
++input_cpp = files('input.cpp')
++
++if have_librist
++ inputs += {'name' : 'TSRIST', 'format' : 'tsrist', 'extra' : 'with_rist'}
++endif
++
++if have_srt
++ inputs += {'name' : 'TSSRT', 'format' : 'tssrt', 'extra' : 'with_srt'}
++endif
++
++av_libs = []
++
++if get_option('WITH_AV')
++ inputs += {'name' : 'AV', 'format' : 'av'}
++ av_libs = [
++ dependency('libavformat'),
++ dependency('libavcodec'),
++ dependency('libavutil'),
++ ]
++endif
++
++inputs_tgts = []
++
++foreach input : inputs
++ link_libs = [libmist]
++ deps = []
++
++ if input.has_key('extra')
++ if input.get('extra').contains('with_rist')
++ deps += librist
++ endif
++ if input.get('extra').contains('with_srt')
++ link_libs += libmist_srt
++ deps += libsrt
++ endif
++ endif
++ if input.get('name').contains('AV')
++ deps += av_libs
++ endif
++ if input.get('name').contains('HLS')
++ deps += ssl_deps
++ endif
++
++ executables += {
++ 'name' : 'MistIn'+input.get('name'),
++ 'sources' : [
++ files(
++ 'mist_in.cpp',
++ 'input.cpp',
++ 'input_'+input.get('format')+'.cpp',
++ ),
++ io_cpp,
++ header_tgts
++ ],
++ 'link' : link_libs,
++ 'deps' : deps,
++ 'inc': [],
++ 'defines': [
++ string_opt.format('INPUTTYPE', 'input_'+input.get('format')+'.h')
++ ]
++ }
++endforeach
+diff --git a/src/meson.build b/src/meson.build
+new file mode 100644
+index 00000000..af2730d7
+--- /dev/null
++++ b/src/meson.build
+@@ -0,0 +1,19 @@
++io_cpp = files('io.cpp')
++
++subdir('analysers')
++subdir('utils')
++subdir('input')
++subdir('output')
++subdir('process')
++subdir('controller')
++
++executables += {
++ 'name': 'relaccxsampler',
++ 'sources': [
++ files('relaccxsampler.cpp'),
++ header_tgts,
++ ],
++ 'link': libmist,
++ 'deps': [],
++ 'defines': [],
++}
+\ No newline at end of file
+diff --git a/src/output/meson.build b/src/output/meson.build
+new file mode 100644
+index 00000000..aa46fac3
+--- /dev/null
++++ b/src/output/meson.build
+@@ -0,0 +1,133 @@
++outputs = [
++ {'name' : 'RTMP', 'format' : 'rtmp'},
++ {'name' : 'DTSC', 'format' : 'dtsc'},
++ {'name' : 'OGG', 'format' : 'ogg', 'extra': ['http']},
++ {'name' : 'FLV', 'format' : 'flv', 'extra': ['http'] },
++ {'name' : 'HTTPMinimalServer', 'format' : 'http_minimalserver', 'extra': ['http']},
++ {'name' : 'MP4', 'format' : 'mp4', 'extra': ['http']},
++ {'name' : 'AAC', 'format' : 'aac', 'extra': ['http']},
++ {'name' : 'MP3', 'format' : 'mp3', 'extra': ['http']},
++ {'name' : 'H264', 'format' : 'h264', 'extra': ['http']},
++ {'name' : 'HDS', 'format' : 'hds', 'extra': ['http']},
++ {'name' : 'SRT', 'format' : 'srt', 'extra': ['http']},
++ {'name' : 'JSON', 'format' : 'json', 'extra': ['http']},
++ {'name' : 'TS', 'format' : 'ts', 'extra': ['ts']},
++ {'name' : 'HTTPTS', 'format' : 'httpts', 'extra': ['http', 'ts']},
++ {'name' : 'HLS', 'format' : 'hls', 'extra': ['http', 'ts']},
++ {'name' : 'CMAF', 'format' : 'cmaf', 'extra': ['http']},
++ {'name' : 'EBML', 'format' : 'ebml', 'extra': ['http']},
++ {'name' : 'RTSP', 'format' : 'rtsp'},
++ {'name' : 'WAV', 'format' : 'wav', 'extra': ['http']},
++ {'name' : 'SDP', 'format' : 'sdp', 'extra': ['http']},
++]
++
++#Referenced by targets in process
++output_ebml_cpp = files('output_ebml.cpp')
++output_http_cpp = files('output_http.cpp')
++output_ts_base_cpp = files('output_ts_base.cpp')
++output_cpp = files('output.cpp')
++
++if have_librist
++ outputs += {'name' : 'TSRIST', 'format' : 'tsrist', 'extra': ['ts', 'debased', 'with_rist']}
++endif
++
++if have_srt
++ outputs += {'name' : 'TSSRT', 'format' : 'tssrt', 'extra': ['ts', 'debased', 'with_srt']}
++endif
++
++if get_option('WITH_JPG')
++ outputs += {'name' : 'JPG', 'format' : 'jpg', 'extra': ['http','jpg']}
++endif
++
++if usessl
++ outputs += [
++ {'name' : 'HTTPS', 'format' : 'https'},
++ {'name' : 'WebRTC', 'format' : 'webrtc', 'extra': ['http','jpg']}
++ ]
++endif
++
++if get_option('WITH_SANITY')
++ outputs += {'name' : 'SanityCheck', 'format' : 'sanitycheck'}
++endif
++
++outputs_tgts = []
++
++foreach output : outputs
++ link_libs = [libmist]
++ deps = []
++ base = files('mist_out.cpp')
++ tsBaseClass = 'Output'
++ extra_tgt_dep = []
++
++ sources = [
++ files('output.cpp',
++ 'output_'+output.get('format')+'.cpp'),
++ io_cpp
++ ]
++
++ if output.has_key('extra')
++ extra = output.get('extra')
++ if not extra.contains('debased')
++ sources += base
++ endif
++ if extra.contains('http')
++ sources += files('output_http.cpp')
++ if extra.contains('ts')
++ tsBaseClass = 'HTTPOutput'
++ endif
++ endif
++ if extra.contains('jpg')
++ extra_tgt_dep = embed_tgts
++ endif
++ if extra.contains('ts')
++ sources += files('output_ts_base.cpp')
++ endif
++ if extra.contains('with_rist')
++ deps += librist
++ endif
++ if extra.contains('with_srt')
++ link_libs += libmist_srt
++ deps += libsrt
++ endif
++ else
++ sources += base
++ endif
++ if output.get('name').contains('HTTPS')
++ deps += ssl_deps
++ endif
++
++ executables += {
++ 'name': 'MistOut'+output.get('name'),
++ 'sources' : [
++ sources,
++ extra_tgt_dep,
++ header_tgts
++ ],
++ 'link' : link_libs,
++ 'deps' : deps,
++ 'defines' : [
++ string_opt.format('OUTPUTTYPE', 'output_'+output.get('format')+'.h'),
++ '-DTS_BASECLASS='+tsBaseClass
++ ]
++ }
++endforeach
++
++executables += {
++ 'name' : 'MistOutHTTP',
++ 'sources' : [
++ files(
++ 'mist_out.cpp',
++ 'output.cpp',
++ 'output_http.cpp',
++ 'output_http_internal.cpp',
++ ),
++ io_cpp,
++ header_tgts,
++ embed_tgts,
++ ],
++ 'link' : libmist,
++ 'defines' :[
++ string_opt.format('OUTPUTTYPE', 'output_http_internal.h')
++ ],
++ 'deps' : []
++}
+diff --git a/src/process/meson.build b/src/process/meson.build
+new file mode 100644
+index 00000000..d36e9fa9
+--- /dev/null
++++ b/src/process/meson.build
+@@ -0,0 +1,50 @@
++process_common = static_library('mist_process_common',
++ output_ebml_cpp,
++ input_ebml_cpp,
++ input_cpp,
++ output_http_cpp,
++ output_cpp,
++ io_cpp,
++ header_tgts,
++ include_directories: incroot,
++ install: false,
++)
++
++executables += {
++ 'name' : 'MistProcFFMPEG',
++ 'sources' : [
++ files('process_ffmpeg.cpp'),
++ header_tgts
++ ],
++ 'link' : [libmist, process_common],
++ 'deps' :[],
++ 'defines': [],
++}
++
++executables += {
++ 'name' : 'MistProcMKVExec',
++ 'sources' : [
++ files('process_exec.cpp'),
++ header_tgts
++ ],
++ 'link' : [libmist, process_common],
++ 'deps' :[],
++ 'defines': [],
++}
++
++executables += {
++ 'name' : 'MistProcLivepeer',
++ 'sources' : [
++ files('process_livepeer.cpp'),
++ input_cpp,
++ output_http_cpp,
++ output_ts_base_cpp,
++ output_cpp,
++ io_cpp,
++ header_tgts
++ ],
++ 'link' : [libmist],
++ 'deps' :[],
++ 'defines': [],
++}
++
+diff --git a/src/utils/meson.build b/src/utils/meson.build
+new file mode 100644
+index 00000000..cd765f7f
+--- /dev/null
++++ b/src/utils/meson.build
+@@ -0,0 +1,28 @@
++
++utils = [
++# {'name': 'Stats', 'file': 'stats'},
++ {'name': 'META', 'file': 'meta'},
++ {'name': 'RAX', 'file': 'rax'},
++ {'name': 'AMF', 'file': 'amf'},
++ {'name': 'Certbot', 'file': 'certbot'},
++ {'name': 'Nuke', 'file': 'nuke'},
++]
++
++if get_option('LOAD_BALANCE')
++ utils += {'name': 'Load', 'file': 'load'}
++endif
++
++utils_tgts = []
++
++foreach util : utils
++ executables += {
++ 'name': 'MistUtil'+util.get('name'),
++ 'sources' : [
++ files('util_'+util.get('file')+'.cpp'),
++ header_tgts
++ ],
++ 'link' : libmist,
++ 'deps' : [],
++ 'defines' :[],
++ }
++endforeach
+\ No newline at end of file
+diff --git a/test/meson.build b/test/meson.build
+new file mode 100644
+index 00000000..9b653992
+--- /dev/null
++++ b/test/meson.build
+@@ -0,0 +1,18 @@
++urltest = executable('urltest', 'url.cpp', include_directories: incroot, link_with: libmist)
++logtest = executable('logtest', 'log.cpp', include_directories: incroot, link_with: libmist)
++downloadertest = executable('downloadertest', 'downloader.cpp', include_directories: incroot, link_with: libmist)
++urireadertest = executable('urireadertest', 'urireader.cpp', include_directories: incroot, link_with: libmist, dependencies: ssl_deps)
++jsontest = executable('jsontest', 'json.cpp', include_directories: incroot, link_with: libmist)
++resolvetest = executable('resolvetest', 'resolve.cpp', include_directories: incroot, link_with: libmist, dependencies: ssl_deps)
++bitwritertest = executable('bitwritertest', 'bitwriter.cpp', include_directories: incroot, link_with: libmist)
++streamstatustest = executable('streamstatustest', 'status.cpp', include_directories: incroot, link_with: libmist)
++websockettest = executable('websockettest', 'websocket.cpp', include_directories: incroot, link_with: libmist)
++test('URLTest', urltest)
++test('LOGTest', logtest)
++test('DownloaderTest', downloadertest)
++test('URIReaderTest', urireadertest)
++test('JSONTest', jsontest)
++test('ResolveTest', resolvetest)
++test('BitWriterTest', bitwritertest)
++test('StreamStatusTest', streamstatustest)
++test('WebSocketTest', websockettest)
+--
+2.25.1
+
+
+From 6950726ee52e134d781d637721bb8d2efc755b02 Mon Sep 17 00:00:00 2001
+From: Gijs Peskens <gijs@peskens.net>
+Date: Tue, 20 Sep 2022 15:40:48 +0200
+Subject: [PATCH 33/38] Fix building with NOSSL
+
+---
+ lib/websocket.cpp | 2 ++
+ src/input/input.cpp | 7 +++++++
+ src/input/input.h | 2 ++
+ 3 files changed, 11 insertions(+)
+
+diff --git a/lib/websocket.cpp b/lib/websocket.cpp
+index c82b4cba..11a4449e 100644
+--- a/lib/websocket.cpp
++++ b/lib/websocket.cpp
+@@ -8,6 +8,7 @@
+ #include "mbedtls/sha1.h"
+ #endif
+
++#if SSL
+ // Takes the data from a Sec-WebSocket-Key header, and returns the corresponding data for a Sec-WebSocket-Accept header
+ static std::string calculateKeyAccept(std::string client_key){
+ client_key += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+@@ -18,6 +19,7 @@ static std::string calculateKeyAccept(std::string client_key){
+ mbedtls_sha1_finish(&ctx, outdata);
+ return Encodings::Base64::encode(std::string((const char *)outdata, 20));
+ }
++#endif
+
+ namespace HTTP{
+
+diff --git a/src/input/input.cpp b/src/input/input.cpp
+index 782dc4ef..5257184c 100644
+--- a/src/input/input.cpp
++++ b/src/input/input.cpp
+@@ -1388,17 +1388,24 @@ namespace Mist{
+ encryption = M.getEncryption(idx);
+ std::string encryptionKey =
+ Encodings::Hex::decode(encryption.substr(encryption.find(":") + 1));
++#if SSL//FIXME!!!
+ aesCipher.setEncryptKey(encryptionKey.c_str());
++#endif
+ }
+ if (encryption.substr(0, encryption.find('/')) == "CTR128"){
++#if SSL
+ DTSC::Packet encPacket = aesCipher.encryptPacketCTR(
+ M, thisPacket, M.getIvec(idx) + M.getPartIndex(thisTime, idx), idx);
+ thisPacket = encPacket;
++#endif
++
+ }else if (encryption.substr(0, encryption.find('/')) == "CBC128"){
+ char ivec[] ={0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+ Bit::htobll(ivec + 8, M.getIvec(idx) + M.getPartIndex(thisTime, idx));
++#if SSL
+ DTSC::Packet encPacket = aesCipher.encryptPacketCBC(M, thisPacket, ivec, idx);
+ thisPacket = encPacket;
++#endif
+ }
+ }else{
+ thisPacket = DTSC::Packet(thisPacket, idx);
+diff --git a/src/input/input.h b/src/input/input.h
+index ce7686c1..0565ece8 100644
+--- a/src/input/input.h
++++ b/src/input/input.h
+@@ -83,7 +83,9 @@ namespace Mist{
+ Comms::Users users;
+ size_t connectedUsers;
+
++#if SSL
+ Encryption::AES aesCipher;
++#endif
+
+ IPC::sharedPage streamStatus;
+
+--
+2.25.1
+
+
+From c16755d08509d26593e9816408de4ea5b477ace0 Mon Sep 17 00:00:00 2001
+From: Gijs Peskens <gijs@peskens.net>
+Date: Tue, 20 Sep 2022 15:40:29 +0200
+Subject: [PATCH 34/38] Fix building against modern ffmpeg
+
+---
+ src/input/input_av.cpp | 11 ++++++++++-
+ 1 file changed, 10 insertions(+), 1 deletion(-)
+
+diff --git a/src/input/input_av.cpp b/src/input/input_av.cpp
+index aa7cd28c..ee05727d 100644
+--- a/src/input/input_av.cpp
++++ b/src/input/input_av.cpp
+@@ -21,9 +21,16 @@ namespace Mist{
+ capa["source_file"] = "$source";
+ capa["priority"] = 1;
+ capa["codecs"].null();
++#if (LIBAVFORMAT_VERSION_MAJOR < 59)
+ av_register_all();
+- AVCodec *cInfo = 0;
++#endif
++ const AVCodec *cInfo = 0;
++#if (LIBAVCODEC_VERSION_MAJOR < 59)
+ while ((cInfo = av_codec_next(cInfo)) != 0){
++#else
++ void *i = 0;
++ while ((cInfo = av_codec_iterate(&i))) {
++#endif
+ if (cInfo->type == AVMEDIA_TYPE_VIDEO){capa["codecs"]["video"].append(cInfo->name);}
+ if (cInfo->type == AVMEDIA_TYPE_AUDIO){capa["codecs"]["audio"].append(cInfo->name);}
+ if (cInfo->type == AVMEDIA_TYPE_SUBTITLE){capa["codecs"]["subtitle"].append(cInfo->name);}
+@@ -57,7 +64,9 @@ namespace Mist{
+ // make sure all av inputs are registered properly, just in case
+ // the constructor already does this, but under windows it doesn't remember that it has.
+ // Very sad, that. We may need to get windows some medication for it.
++#if (LIBAVFORMAT_VERSION_MAJOR < 59)
+ av_register_all();
++#endif
+
+ // close any already open files
+ if (pFormatCtx){
+--
+2.25.1
+
+
+From c2bb6d3087b6a4919be0f42b503317309000c027 Mon Sep 17 00:00:00 2001
+From: Gijs Peskens <gijs@peskens.net>
+Date: Fri, 23 Sep 2022 10:41:38 +0200
+Subject: [PATCH 35/38] Skip DIR and non files when gathering list of
+ executables
+
+---
+ lib/config.cpp | 10 +++++++++-
+ 1 file changed, 9 insertions(+), 1 deletion(-)
+
+diff --git a/lib/config.cpp b/lib/config.cpp
+index 5e9ce11a..c88e89ab 100644
+--- a/lib/config.cpp
++++ b/lib/config.cpp
+@@ -34,6 +34,7 @@
+ #include <stdarg.h> // for va_list
+ #include <stdlib.h>
+ #include <sys/types.h>
++#include <sys/stat.h>
+ #include <unistd.h>
+
+ bool Util::Config::is_active = false;
+@@ -784,7 +785,14 @@ void Util::getMyExec(std::deque<std::string> &execs){
+ do{
+ errno = 0;
+ if ((dp = readdir(d))){
+- if (strncmp(dp->d_name, "Mist", 4) == 0){execs.push_back(dp->d_name);}
++ if (dp->d_type != DT_DIR && strncmp(dp->d_name, "Mist", 4) == 0){
++ if (dp->d_type != DT_REG) {
++ struct stat st = {};
++ stat(dp->d_name, &st);
++ if (!S_ISREG(st.st_mode))
++ continue;
++ }
++ execs.push_back(dp->d_name);}
+ }
+ }while (dp != NULL);
+ closedir(d);
+--
+2.25.1
+
+
+From 7e3f5f39e07d903bfbaa6845f787a49192ac912e Mon Sep 17 00:00:00 2001
+From: Gijs Peskens <gijs@peskens.net>
+Date: Fri, 14 Oct 2022 11:02:47 +0200
+Subject: [PATCH 36/38] Fix building TSRist without SRT
+
+---
+ src/input/input_tsrist.cpp | 1 -
+ 1 file changed, 1 deletion(-)
+
+diff --git a/src/input/input_tsrist.cpp b/src/input/input_tsrist.cpp
+index 33f1155e..e697ae77 100644
+--- a/src/input/input_tsrist.cpp
++++ b/src/input/input_tsrist.cpp
+@@ -11,7 +11,6 @@
+ #include <mist/flv_tag.h>
+ #include <mist/http_parser.h>
+ #include <mist/mp4_generic.h>
+-#include <mist/socket_srt.h>
+ #include <mist/stream.h>
+ #include <mist/timing.h>
+ #include <mist/ts_packet.h>
+--
+2.25.1
+
+
+From bfc869b5101a62399d5d3b329fce6ec3df21cfe6 Mon Sep 17 00:00:00 2001
+From: Gijs Peskens <gijs@peskens.net>
+Date: Mon, 2 May 2022 15:24:02 +0200
+Subject: [PATCH 37/38] Upstream MBED-TLS integrated SRTP support, allow
+ building against it
+
+---
+ lib/dtls_srtp_handshake.cpp | 69 ++++++++++++++++++++++++++++++++++++-
+ lib/dtls_srtp_handshake.h | 14 ++++++++
+ lib/socket.h | 6 +++-
+ meson.build | 34 ++++++++++++++++++
+ src/output/output_https.h | 4 +++
+ 5 files changed, 125 insertions(+), 2 deletions(-)
+
+diff --git a/lib/dtls_srtp_handshake.cpp b/lib/dtls_srtp_handshake.cpp
+index ea092d13..8c3fa1c9 100644
+--- a/lib/dtls_srtp_handshake.cpp
++++ b/lib/dtls_srtp_handshake.cpp
+@@ -1,6 +1,7 @@
+ #include "defines.h"
+ #include "dtls_srtp_handshake.h"
+ #include <algorithm>
++#include <mbedtls/ssl.h>
+ #include <string.h>
+
+ /* Write mbedtls into a log file. */
+@@ -11,6 +12,30 @@
+
+ /* ----------------------------------------- */
+
++#if HAVE_UPSTREAM_MBEDTLS_SRTP
++int DTLSSRTPHandshake::dtlsExtractKeyData( void *p_expkey,
++ const unsigned char *ms,
++ const unsigned char *,
++ size_t,
++ size_t,
++ size_t,
++ const unsigned char client_random[32],
++ const unsigned char server_random[32],
++ mbedtls_tls_prf_types tls_prf_type )
++{
++ DTLSSRTPHandshake *handshake = static_cast<DTLSSRTPHandshake *>(p_expkey);
++ memcpy(handshake->master_secret, ms, sizeof(handshake->master_secret));
++ memcpy(handshake->randbytes, client_random, 32);
++ memcpy(handshake->randbytes + 32, server_random, 32);
++ handshake->tls_prf_type = tls_prf_type;
++ return 0;
++}
++//It breaks if not defined outside of the function
++static mbedtls_ssl_srtp_profile srtp_profiles[] ={MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_80,
++ MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_32,
++ MBEDTLS_TLS_SRTP_UNSET};
++#endif
++
+ static void print_mbedtls_error(int r);
+ static void print_mbedtls_debug_message(void *ctx, int level, const char *file, int line, const char *str);
+ static int on_mbedtls_wants_to_read(void *user, unsigned char *buf,
+@@ -33,8 +58,10 @@ int DTLSSRTPHandshake::init(mbedtls_x509_crt *certificate, mbedtls_pk_context *p
+ int (*writeCallback)(const uint8_t *data, int *nbytes)){
+
+ int r = 0;
++#if !HAVE_UPSTREAM_MBEDTLS_SRTP
+ mbedtls_ssl_srtp_profile srtp_profiles[] ={MBEDTLS_SRTP_AES128_CM_HMAC_SHA1_80,
+ MBEDTLS_SRTP_AES128_CM_HMAC_SHA1_32};
++#endif
+
+ if (!writeCallback){
+ FAIL_MSG("No writeCallack function given.");
+@@ -90,14 +117,22 @@ int DTLSSRTPHandshake::init(mbedtls_x509_crt *certificate, mbedtls_pk_context *p
+ mbedtls_debug_set_threshold(10);
+
+ /* enable SRTP */
++#if !HAVE_UPSTREAM_MBEDTLS_SRTP
+ r = mbedtls_ssl_conf_dtls_srtp_protection_profiles(&ssl_conf, srtp_profiles,
+ sizeof(srtp_profiles) / sizeof(srtp_profiles[0]));
++#else
++ r = mbedtls_ssl_conf_dtls_srtp_protection_profiles(&ssl_conf, srtp_profiles);
++#endif
+ if (0 != r){
+ print_mbedtls_error(r);
+ r = -40;
+ goto error;
+ }
+
++#if HAVE_UPSTREAM_MBEDTLS_SRTP
++ mbedtls_ssl_conf_export_keys_ext_cb( &ssl_conf, dtlsExtractKeyData, this);
++#endif
++
+ /* cert certificate chain + key, so we can verify the client-hello signed data */
+ r = mbedtls_ssl_conf_own_cert(&ssl_conf, cert, key);
+ if (0 != r){
+@@ -266,6 +301,7 @@ int DTLSSRTPHandshake::resetSession(){
+ */
+ int DTLSSRTPHandshake::extractKeyingMaterial(){
+
++#if !HAVE_UPSTREAM_MBEDTLS_SRTP
+ int r = 0;
+ uint8_t keying_material[MBEDTLS_DTLS_SRTP_MAX_KEY_MATERIAL_LENGTH] ={};
+ size_t keying_material_len = sizeof(keying_material);
+@@ -275,8 +311,14 @@ int DTLSSRTPHandshake::extractKeyingMaterial(){
+ print_mbedtls_error(r);
+ return -1;
+ }
+-
++#else
++ uint8_t keying_material[MBEDTLS_TLS_SRTP_MAX_MKI_LENGTH] ={};
++ mbedtls_dtls_srtp_info info = {};
++ info.chosen_dtls_srtp_profile = 999;
++ mbedtls_ssl_get_dtls_srtp_negotiation_result(&ssl_ctx, &info);
++#endif
+ /* @todo following code is for server mode only */
++#if !HAVE_UPSTREAM_MBEDTLS_SRTP
+ mbedtls_ssl_srtp_profile srtp_profile = mbedtls_ssl_get_dtls_srtp_protection_profile(&ssl_ctx);
+ switch (srtp_profile){
+ case MBEDTLS_SRTP_AES128_CM_HMAC_SHA1_80:{
+@@ -292,6 +334,31 @@ int DTLSSRTPHandshake::extractKeyingMaterial(){
+ return -6;
+ }
+ }
++#else
++ if (mbedtls_ssl_tls_prf(tls_prf_type, master_secret, sizeof(master_secret), "EXTRACTOR-dtls_srtp", randbytes,sizeof( randbytes ),
++ keying_material, sizeof( keying_material )) != 0) {
++ ERROR_MSG("mbedtls_ssl_tls_prf failed to create keying_material");
++ return -6;
++ }
++ switch (info.chosen_dtls_srtp_profile){
++ case MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_80:{
++ cipher = "SRTP_AES128_CM_SHA1_80";
++ break;
++ }
++ case MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_32:{
++ cipher = "SRTP_AES128_CM_SHA1_32";
++ break;
++ }
++ case MBEDTLS_TLS_SRTP_UNSET: {
++ ERROR_MSG("Wasn't able to negotiate the use of DTLS-SRTP");
++ return -6;
++ }
++ default:{
++ ERROR_MSG("Unhandled SRTP profile: %hu, cannot extract keying material.", info.chosen_dtls_srtp_profile);
++ return -6;
++ }
++ }
++#endif
+
+ remote_key.assign((char *)(&keying_material[0]) + 0, 16);
+ local_key.assign((char *)(&keying_material[0]) + 16, 16);
+diff --git a/lib/dtls_srtp_handshake.h b/lib/dtls_srtp_handshake.h
+index 7250167e..d93fc2f5 100644
+--- a/lib/dtls_srtp_handshake.h
++++ b/lib/dtls_srtp_handshake.h
+@@ -40,6 +40,20 @@ private:
+ mbedtls_ssl_config ssl_conf;
+ mbedtls_ssl_cookie_ctx cookie_ctx;
+ mbedtls_timing_delay_context timer_ctx;
++#if HAVE_UPSTREAM_MBEDTLS_SRTP
++ unsigned char master_secret[48];
++ unsigned char randbytes[64];
++ mbedtls_tls_prf_types tls_prf_type;
++ static int dtlsExtractKeyData( void *p_expkey,
++ const unsigned char *ms,
++ const unsigned char *,
++ size_t,
++ size_t,
++ size_t,
++ const unsigned char client_random[32],
++ const unsigned char server_random[32],
++ mbedtls_tls_prf_types tls_prf_type );
++#endif
+
+ public:
+ int (*write_callback)(const uint8_t *data, int *nbytes);
+diff --git a/lib/socket.h b/lib/socket.h
+index b369ac84..85f20f99 100644
+--- a/lib/socket.h
++++ b/lib/socket.h
+@@ -22,7 +22,11 @@
+ #include "mbedtls/debug.h"
+ #include "mbedtls/entropy.h"
+ #include "mbedtls/error.h"
+-#include "mbedtls/net.h"
++#if !HAVE_UPSTREAM_MBEDTLS_SRTP
++#include <mbedtls/net.h>
++#else
++#include "mbedtls/net_sockets.h"
++#endif
+ #include "mbedtls/ssl.h"
+ #endif
+
+diff --git a/meson.build b/meson.build
+index 38bd29c3..964ed660 100644
+--- a/meson.build
++++ b/meson.build
+@@ -116,6 +116,40 @@ if usessl
+ mbedcrypto = ccpp.find_library('mbedcrypto')
+ srtp2 = dependency('libsrtp2')
+ ssl_deps = [mbedtls, mbedx509, mbedcrypto, srtp2]
++
++ ##This currently only works for MbedTLS < 3
++ code_upstream = '''
++ #include <mbedtls/ssl.h>
++ static mbedtls_ssl_srtp_profile srtp_profiles[] ={MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_80,
++ MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_32,
++ MBEDTLS_TLS_SRTP_UNSET};
++ static int test()
++ {
++ mbedtls_ssl_config ssl_conf;
++ mbedtls_ssl_conf_dtls_srtp_protection_profiles(&ssl_conf, srtp_profiles);
++ return 0;
++ }
++ '''
++ code_ddvtech = '''
++ #include <mbedtls/ssl.h>
++ mbedtls_ssl_srtp_profile srtp_profiles[] ={MBEDTLS_SRTP_AES128_CM_HMAC_SHA1_80,
++ MBEDTLS_SRTP_AES128_CM_HMAC_SHA1_32};
++ static int test()
++ {
++ mbedtls_ssl_config ssl_conf;
++ mbedtls_ssl_conf_dtls_srtp_protection_profiles(&ssl_conf, srtp_profiles,
++ sizeof(srtp_profiles) / sizeof(srtp_profiles[0]));
++ return 0;
++ }
++ '''
++ have_upstream_mbedtls_srtp = ccpp.compiles(code_upstream, dependencies: ssl_deps, name: 'MbedTLS SRTP is upstream')
++ if not have_upstream_mbedtls_srtp
++ ddvtech_mbedtls = ccpp.compiles(code_ddvtech, dependencies: ssl_deps, name: 'MbedTLS SRTP is DDVTECH')
++ if not ddvtech_mbedtls
++ error('MbedTLS SRTP Support is required for SSL Build')
++ endif
++ endif
++ option_defines += int_opt.format('HAVE_UPSTREAM_MBEDTLS_SRTP', have_upstream_mbedtls_srtp.to_int())
+ endif
+
+ add_project_arguments(option_defines, language: 'cpp')
+diff --git a/src/output/output_https.h b/src/output/output_https.h
+index 386cd1f9..6d95e51c 100644
+--- a/src/output/output_https.h
++++ b/src/output/output_https.h
+@@ -3,7 +3,11 @@
+ #include <mbedtls/certs.h>
+ #include <mbedtls/ctr_drbg.h>
+ #include <mbedtls/entropy.h>
++#if !HAVE_UPSTREAM_MBEDTLS_SRTP
+ #include <mbedtls/net.h>
++#else
++#include <mbedtls/net_sockets.h>
++#endif
+ #include <mbedtls/ssl.h>
+ #include <mbedtls/timing.h>
+ #include <mbedtls/x509.h>
+--
+2.25.1
+
+
+From 395d91254b8abec645ce44fbd225fbc41427a0f1 Mon Sep 17 00:00:00 2001
+From: Gijs Peskens <gijs@peskens.net>
+Date: Fri, 23 Sep 2022 19:26:10 +0200
+Subject: [PATCH 38/38] MBED-TLS 3 compat
+
+---
+ lib/certificate.cpp | 8 ++++++--
+ lib/certificate.h | 8 +++++++-
+ lib/dtls_srtp_handshake.cpp | 36 ++++++++++++++++++++++++++++++------
+ lib/dtls_srtp_handshake.h | 19 +++++++++++++++++--
+ src/output/output_https.cpp | 4 ++++
+ src/output/output_https.h | 4 ++++
+ 6 files changed, 68 insertions(+), 11 deletions(-)
+
+diff --git a/lib/certificate.cpp b/lib/certificate.cpp
+index b504d55a..cd6672e8 100644
+--- a/lib/certificate.cpp
++++ b/lib/certificate.cpp
+@@ -6,12 +6,12 @@
+ Certificate::Certificate(){
+ mbedtls_pk_init(&key);
+ mbedtls_x509_crt_init(&cert);
++ mbedtls_ctr_drbg_init(&rand_ctx);
+ }
+
+ int Certificate::init(const std::string &countryName, const std::string &organization,
+ const std::string &commonName){
+
+- mbedtls_ctr_drbg_context rand_ctx ={};
+ mbedtls_entropy_context entropy_ctx ={};
+ mbedtls_x509write_cert write_cert ={};
+ mbedtls_rsa_context *rsa_ctx;
+@@ -48,7 +48,6 @@ int Certificate::init(const std::string &countryName, const std::string &organiz
+ }
+
+ // initialize random number generator
+- mbedtls_ctr_drbg_init(&rand_ctx);
+ mbedtls_entropy_init(&entropy_ctx);
+ r = mbedtls_ctr_drbg_seed(&rand_ctx, mbedtls_entropy_func, &entropy_ctx,
+ (const unsigned char *)personalisation, strlen(personalisation));
+@@ -204,6 +203,7 @@ error:
+ Certificate::~Certificate(){
+ mbedtls_pk_free(&key);
+ mbedtls_x509_crt_free(&cert);
++ mbedtls_ctr_drbg_free(&rand_ctx);
+ }
+
+ /// Loads a single file into the certificate. Returns true on success.
+@@ -215,7 +215,11 @@ bool Certificate::loadCert(const std::string & certFile){
+ /// Loads a single key. Returns true on success.
+ bool Certificate::loadKey(const std::string & keyFile){
+ if (!keyFile.size()){return true;}
++#if MBEDTLS_VERSION_MAJOR > 2
++ return mbedtls_pk_parse_keyfile(&key, keyFile.c_str(), NULL, mbedtls_ctr_drbg_random, &rand_ctx) == 0;
++#else
+ return mbedtls_pk_parse_keyfile(&key, keyFile.c_str(), 0) == 0;
++#endif
+ }
+
+ /// Calculates SHA256 fingerprint over the loaded certificate(s)
+diff --git a/lib/certificate.h b/lib/certificate.h
+index a8ccf747..350fc665 100644
+--- a/lib/certificate.h
++++ b/lib/certificate.h
+@@ -9,8 +9,12 @@
+ communication. This certificate uses a 2048 bits RSA key.
+
+ */
+-
++#include <mbedtls/version.h>
++#if MBEDTLS_VERSION_MAJOR > 2
++#include <mbedtls/build_info.h>
++#else
+ #include <mbedtls/config.h>
++#endif
+ #include <mbedtls/ctr_drbg.h>
+ #include <mbedtls/entropy.h>
+ #include <mbedtls/error.h>
+@@ -32,4 +36,6 @@ public:
+ public:
+ mbedtls_x509_crt cert;
+ mbedtls_pk_context key; /* key context, stores private and public key. */
++private:
++ mbedtls_ctr_drbg_context rand_ctx;
+ };
+diff --git a/lib/dtls_srtp_handshake.cpp b/lib/dtls_srtp_handshake.cpp
+index 8c3fa1c9..70c89066 100644
+--- a/lib/dtls_srtp_handshake.cpp
++++ b/lib/dtls_srtp_handshake.cpp
+@@ -13,7 +13,16 @@
+ /* ----------------------------------------- */
+
+ #if HAVE_UPSTREAM_MBEDTLS_SRTP
+-int DTLSSRTPHandshake::dtlsExtractKeyData( void *p_expkey,
++#if MBEDTLS_VERSION_MAJOR > 2
++void DTLSSRTPHandshake::dtlsExtractKeyData( void *user,
++ mbedtls_ssl_key_export_type type,
++ const unsigned char *ms,
++ size_t,
++ const unsigned char client_random[32],
++ const unsigned char server_random[32],
++ mbedtls_tls_prf_types tls_prf_type )
++#else
++int DTLSSRTPHandshake::dtlsExtractKeyData( void *user,
+ const unsigned char *ms,
+ const unsigned char *,
+ size_t,
+@@ -22,13 +31,16 @@ int DTLSSRTPHandshake::dtlsExtractKeyData( void *p_expkey,
+ const unsigned char client_random[32],
+ const unsigned char server_random[32],
+ mbedtls_tls_prf_types tls_prf_type )
++#endif
+ {
+- DTLSSRTPHandshake *handshake = static_cast<DTLSSRTPHandshake *>(p_expkey);
++ DTLSSRTPHandshake *handshake = static_cast<DTLSSRTPHandshake *>(user);
+ memcpy(handshake->master_secret, ms, sizeof(handshake->master_secret));
+ memcpy(handshake->randbytes, client_random, 32);
+ memcpy(handshake->randbytes + 32, server_random, 32);
+ handshake->tls_prf_type = tls_prf_type;
++#if MBEDTLS_VERSION_MAJOR == 2
+ return 0;
++#endif
+ }
+ //It breaks if not defined outside of the function
+ static mbedtls_ssl_srtp_profile srtp_profiles[] ={MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_80,
+@@ -129,7 +141,7 @@ int DTLSSRTPHandshake::init(mbedtls_x509_crt *certificate, mbedtls_pk_context *p
+ goto error;
+ }
+
+-#if HAVE_UPSTREAM_MBEDTLS_SRTP
++#if HAVE_UPSTREAM_MBEDTLS_SRTP && MBEDTLS_VERSION_MAJOR == 2
+ mbedtls_ssl_conf_export_keys_ext_cb( &ssl_conf, dtlsExtractKeyData, this);
+ #endif
+
+@@ -160,6 +172,10 @@ int DTLSSRTPHandshake::init(mbedtls_x509_crt *certificate, mbedtls_pk_context *p
+ goto error;
+ }
+
++#if MBEDTLS_VERSION_MAJOR > 2
++ mbedtls_ssl_set_export_keys_cb(&ssl_ctx, dtlsExtractKeyData, this);
++#endif
++
+ /* set bio handlers */
+ mbedtls_ssl_set_bio(&ssl_ctx, (void *)this, on_mbedtls_wants_to_write, on_mbedtls_wants_to_read, NULL);
+
+@@ -223,7 +239,11 @@ int DTLSSRTPHandshake::parse(const uint8_t *data, size_t nbytes){
+ return -2;
+ }
+
++#if MBEDTLS_VERSION_MAJOR > 2
++ if (mbedtls_ssl_is_handshake_over(&ssl_ctx)) {
++#else
+ if (MBEDTLS_SSL_HANDSHAKE_OVER == ssl_ctx.state){
++#endif
+ ERROR_MSG("Already finished the handshake.");
+ return -3;
+ }
+@@ -314,7 +334,6 @@ int DTLSSRTPHandshake::extractKeyingMaterial(){
+ #else
+ uint8_t keying_material[MBEDTLS_TLS_SRTP_MAX_MKI_LENGTH] ={};
+ mbedtls_dtls_srtp_info info = {};
+- info.chosen_dtls_srtp_profile = 999;
+ mbedtls_ssl_get_dtls_srtp_negotiation_result(&ssl_ctx, &info);
+ #endif
+ /* @todo following code is for server mode only */
+@@ -340,7 +359,12 @@ int DTLSSRTPHandshake::extractKeyingMaterial(){
+ ERROR_MSG("mbedtls_ssl_tls_prf failed to create keying_material");
+ return -6;
+ }
+- switch (info.chosen_dtls_srtp_profile){
++#if MBEDTLS_VERSION_MAJOR > 2
++ mbedtls_ssl_srtp_profile chosen_profile = info.private_chosen_dtls_srtp_profile;
++#else
++ mbedtls_ssl_srtp_profile chosen_profile = info.chosen_dtls_srtp_profile;
++#endif
++ switch (chosen_profile){
+ case MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_80:{
+ cipher = "SRTP_AES128_CM_SHA1_80";
+ break;
+@@ -354,7 +378,7 @@ int DTLSSRTPHandshake::extractKeyingMaterial(){
+ return -6;
+ }
+ default:{
+- ERROR_MSG("Unhandled SRTP profile: %hu, cannot extract keying material.", info.chosen_dtls_srtp_profile);
++ ERROR_MSG("Unhandled SRTP profile: %hu, cannot extract keying material.", chosen_profile);
+ return -6;
+ }
+ }
+diff --git a/lib/dtls_srtp_handshake.h b/lib/dtls_srtp_handshake.h
+index d93fc2f5..f9e9d432 100644
+--- a/lib/dtls_srtp_handshake.h
++++ b/lib/dtls_srtp_handshake.h
+@@ -1,8 +1,14 @@
+ #pragma once
+
+ #include <deque>
++#include <mbedtls/version.h>
++#if MBEDTLS_VERSION_MAJOR == 2
+ #include <mbedtls/certs.h>
+ #include <mbedtls/config.h>
++#else
++#include <mbedtls/build_info.h>
++//#include <mbedtls/pk.h>
++#endif
+ #include <mbedtls/ctr_drbg.h>
+ #include <mbedtls/debug.h>
+ #include <mbedtls/entropy.h>
+@@ -44,7 +50,16 @@ private:
+ unsigned char master_secret[48];
+ unsigned char randbytes[64];
+ mbedtls_tls_prf_types tls_prf_type;
+- static int dtlsExtractKeyData( void *p_expkey,
++#if MBEDTLS_VERSION_MAJOR > 2
++ static void dtlsExtractKeyData( void *user,
++ mbedtls_ssl_key_export_type type,
++ const unsigned char *ms,
++ size_t,
++ const unsigned char client_random[32],
++ const unsigned char server_random[32],
++ mbedtls_tls_prf_types tls_prf_type );
++#else
++ static int dtlsExtractKeyData( void *user,
+ const unsigned char *ms,
+ const unsigned char *,
+ size_t,
+@@ -54,7 +69,7 @@ private:
+ const unsigned char server_random[32],
+ mbedtls_tls_prf_types tls_prf_type );
+ #endif
+-
++#endif
+ public:
+ int (*write_callback)(const uint8_t *data, int *nbytes);
+ std::deque<uint8_t> buffer; /* Accessed from BIO callbback. We copy the bytes you pass into `parse()` into this
+diff --git a/src/output/output_https.cpp b/src/output/output_https.cpp
+index 59fa0bfd..f6fb0054 100644
+--- a/src/output/output_https.cpp
++++ b/src/output/output_https.cpp
+@@ -239,7 +239,11 @@ namespace Mist{
+ }
+
+ // Read key from cmdline option
++#if MBEDTLS_VERSION_MAJOR > 2
++ ret = mbedtls_pk_parse_keyfile(&pkey, config->getString("key").c_str(), NULL, mbedtls_ctr_drbg_random, &ctr_drbg);
++#else
+ ret = mbedtls_pk_parse_keyfile(&pkey, config->getString("key").c_str(), 0);
++#endif
+ if (ret != 0){
+ FAIL_MSG("Could not load any keys from file: %s", config->getString("key").c_str());
+ return;
+diff --git a/src/output/output_https.h b/src/output/output_https.h
+index 6d95e51c..e3811216 100644
+--- a/src/output/output_https.h
++++ b/src/output/output_https.h
+@@ -1,6 +1,9 @@
+ #pragma once
+ #include "output.h"
++#include <mbedtls/version.h>
++#if MBEDTLS_VERSION_MAJOR == 2
+ #include <mbedtls/certs.h>
++#endif
+ #include <mbedtls/ctr_drbg.h>
+ #include <mbedtls/entropy.h>
+ #if !HAVE_UPSTREAM_MBEDTLS_SRTP
+@@ -33,6 +36,7 @@ namespace Mist{
+ static mbedtls_ssl_config sslConf;
+ static mbedtls_x509_crt srvcert;
+ static mbedtls_pk_context pkey;
++
+ };
+ }// namespace Mist
+
+--
+2.25.1
+
diff --git a/legacy/mistserver/link-execinfo.patch b/legacy/mistserver/link-execinfo.patch
new file mode 100644
index 000000000..ed5c088bc
--- /dev/null
+++ b/legacy/mistserver/link-execinfo.patch
@@ -0,0 +1,11 @@
+diff -ur a/CMakeLists.txt b/CMakeLists.txt
+--- a/CMakeLists.txt 2022-02-11 00:15:16.610459631 +0000
++++ b/CMakeLists.txt 2022-02-11 00:15:01.221072666 +0000
+@@ -191,6 +191,7 @@
+ endif()
+ target_link_libraries(mist
+ -lpthread
++ -lexecinfo
+ ${LIBRT}
+ )
+ if (NOT DEFINED NOSSL )
diff --git a/legacy/mistserver/mistserver.confd b/legacy/mistserver/mistserver.confd
new file mode 100644
index 000000000..e0c085d5f
--- /dev/null
+++ b/legacy/mistserver/mistserver.confd
@@ -0,0 +1,7 @@
+# Configuration for mistserver
+#
+# Config file location
+#MIST_CONFIG=/etc/mistserver.conf
+
+# Log file location
+#MIST_LOG=/var/log/mistserver.log
diff --git a/legacy/mistserver/mistserver.initd b/legacy/mistserver/mistserver.initd
new file mode 100755
index 000000000..9f2765c40
--- /dev/null
+++ b/legacy/mistserver/mistserver.initd
@@ -0,0 +1,29 @@
+#!/sbin/openrc-run
+
+description="MistServer multimedia streaming service"
+
+: ${MIST_CONFIG:=/etc/mistserver.conf}
+: ${MIST_LOG:=/var/log/mistserver.log}
+
+depend() {
+ need net
+ use logger dns
+ after sshd firewall
+}
+
+start() {
+ ebegin "Starting ${SVCNAME}"
+ start-stop-daemon --start \
+ --pidfile ${PIDFILE} \
+ --exec MistController \
+ -- -L ${MIST_LOG} -c ${MIST_CONFIG}
+ eend $?
+}
+
+stop() {
+ ebegin "Stopping ${SVCNAME}"
+ start-stop-daemon --stop \
+ --pidfile ${PIDFILE} \
+ --exec MistController
+ eend $?
+}
diff --git a/legacy/mistserver/mistserver.pre-install b/legacy/mistserver/mistserver.pre-install
new file mode 100644
index 000000000..9ef5b5f2e
--- /dev/null
+++ b/legacy/mistserver/mistserver.pre-install
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+groupadd -r mistserver 2>/dev/null
+useradd -c mistserver -s /sbin/nologin -g mistserver \
+ -m -r mistserver 2>/dev/null
+
+exit 0