| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * This file is part of the Falcon Player (FPP) and is Copyright (C) | ||
| 3 | * 2013-2026 by the Falcon Player Developers. | ||
| 4 | * | ||
| 5 | * The Falcon Player (FPP) is free software, and is covered under | ||
| 6 | * multiple Open Source licenses. Please see the included 'LICENSES' | ||
| 7 | * file for descriptions of what files are covered by each license. | ||
| 8 | * | ||
| 9 | * This source file is covered under the LGPL v2.1 as described in the | ||
| 10 | * included LICENSE.LGPL file. | ||
| 11 | */ | ||
| 12 | |||
| 13 | #include "fpp-pch.h" | ||
| 14 | |||
| 15 | #include <mutex> | ||
| 16 | #include <set> | ||
| 17 | |||
| 18 | #include "fpp-json.h" | ||
| 19 | #include "fpphttp.h" // drogon/HTTP helpers used here; see fpphttp_types.h | ||
| 20 | |||
| 21 | #include "Warnings.h" // WarningHolder -- needed directly for NOPCH builds | ||
| 22 | |||
| 23 | #include "GStreamerOut.h" | ||
| 24 | #include "PipeWireGraphConfig.h" | ||
| 25 | #include "RTSPOutputManager.h" | ||
| 26 | |||
| 27 | #ifdef HAS_RTSP_OUTPUT_GSTREAMER | ||
| 28 | |||
| 29 | #include <fstream> | ||
| 30 | #include <sstream> | ||
| 31 | |||
| 32 | #include "common.h" | ||
| 33 | #include "log.h" | ||
| 34 | #include "settings.h" | ||
| 35 | |||
| 36 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 37 | // Singleton instance | ||
| 38 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 39 | static RTSPOutputManager s_rtspOutputManager; | ||
| 40 | RTSPOutputManager& RTSPOutputManager::INSTANCE = s_rtspOutputManager; | ||
| 41 | |||
| 42 | 2346 | RTSPOutputManager::RTSPOutputManager() { | |
| 43 |
2/4✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
|
4692 | m_configPath = FPP_DIR_MEDIA("/config/pipewire-rtsp-outputs.json"); |
| 44 |
2/4✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
|
2346 | m_consumersPath = FPP_DIR_MEDIA("/config/pipewire-video-consumers.json"); |
| 45 | 2346 | } | |
| 46 | |||
| 47 | 916 | RTSPOutputManager::~RTSPOutputManager() { | |
| 48 | 916 | Shutdown(); | |
| 49 | 916 | } | |
| 50 | |||
| 51 | 8 | std::string RTSPOutputMount::AudioChannel() const { | |
| 52 | 8 | return audioNodeName + "_ch"; | |
| 53 | } | ||
| 54 | |||
| 55 | 47 | std::string RTSPOutputMount::ResolveSourceNode() const { | |
| 56 |
2/2✓ Branch 0 taken 10 times.
✓ Branch 1 taken 37 times.
|
47 | if (!sourceNode.empty()) { |
| 57 | 10 | return sourceNode; | |
| 58 | } | ||
| 59 |
1/2✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
|
37 | if (!streamSlots.empty()) { |
| 60 | // A group aimed at a playback stream slot has no named input source. | ||
| 61 | // The slot's intervideo channel exists whether or not anything is | ||
| 62 | // playing, and intervideosrc holds the mount up on black until it is, | ||
| 63 | // which is what a client watching a stream between songs should see. | ||
| 64 | // Only the first slot can be served: one mount is one channel. | ||
| 65 |
1/2✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
|
37 | return "fppd_video_stream_" + std::to_string(streamSlots.front()); |
| 66 | } | ||
| 67 | ✗ | return ""; | |
| 68 | } | ||
| 69 | |||
| 70 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 71 | // Helpers | ||
| 72 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 73 | |||
| 74 | // Quote a config-supplied value for a gst_parse_launch description. | ||
| 75 | // | ||
| 76 | // Same hazard GstQuote() covers in VideoInputManager: these strings come from | ||
| 77 | // a JSON file the web UI writes, and a bare space or quote in one of them does | ||
| 78 | // not fail cleanly -- it re-parses as extra pipeline syntax. | ||
| 79 | 24 | static std::string RtspGstQuote(const std::string& value) { | |
| 80 |
1/2✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
|
24 | std::string out = "\""; |
| 81 |
2/2✓ Branch 0 taken 428 times.
✓ Branch 1 taken 24 times.
|
452 | for (char c : value) { |
| 82 |
2/4✓ Branch 0 taken 428 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 428 times.
|
428 | if (c == '\\' || c == '"') { |
| 83 | ✗ | out += '\\'; | |
| 84 | } | ||
| 85 |
1/2✓ Branch 0 taken 428 times.
✗ Branch 1 not taken.
|
428 | out += c; |
| 86 | } | ||
| 87 |
1/2✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
|
24 | out += '"'; |
| 88 | 24 | return out; | |
| 89 | ✗ | } | |
| 90 | |||
| 91 | // A mount point has to start with '/' and carry nothing that would change the | ||
| 92 | // meaning of the URL. An empty or malformed one is replaced rather than | ||
| 93 | // rejected, so a half-finished config still serves something addressable. | ||
| 94 | 30 | static std::string NormaliseMountPoint(const std::string& raw, int id) { | |
| 95 | 30 | std::string mp; | |
| 96 |
2/2✓ Branch 0 taken 114 times.
✓ Branch 1 taken 30 times.
|
144 | for (char c : raw) { |
| 97 |
3/8✓ Branch 0 taken 24 times.
✓ Branch 1 taken 90 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 24 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
114 | if (isalnum(static_cast<unsigned char>(c)) || c == '/' || c == '_' || c == '-') { |
| 98 |
1/2✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
|
114 | mp += c; |
| 99 | } | ||
| 100 | } | ||
| 101 |
6/8✓ Branch 0 taken 24 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 24 times.
✓ Branch 6 taken 6 times.
✓ Branch 7 taken 24 times.
|
30 | if (mp.empty() || mp == "/") { |
| 102 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | mp = "/stream" + std::to_string(id); |
| 103 | } | ||
| 104 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 30 times.
|
30 | if (mp[0] != '/') { |
| 105 | ✗ | mp = "/" + mp; | |
| 106 | } | ||
| 107 | 30 | return mp; | |
| 108 | ✗ | } | |
| 109 | |||
| 110 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 111 | // Config | ||
| 112 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 113 | // The warnings the last apply raised, retracted by the next: saving the | ||
| 114 | // config again, or the Restart that follows freeing the port, is how a user | ||
| 115 | // retries, and they used to stay up until fppd restarted. RemoveWarning() | ||
| 116 | // needs the exact text, so it is kept as raised. | ||
| 117 | static std::mutex s_rtspWarningsLock; | ||
| 118 | static std::set<std::string> s_rtspWarnings; | ||
| 119 | |||
| 120 | 1 | static void RaiseRtspWarning(const std::string& w) { | |
| 121 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | std::lock_guard<std::mutex> l(s_rtspWarningsLock); |
| 122 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | s_rtspWarnings.insert(w); |
| 123 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | WarningHolder::AddWarning(31, w); |
| 124 | 1 | } | |
| 125 | |||
| 126 | 1891 | static void RetractRtspWarnings() { | |
| 127 |
1/2✓ Branch 0 taken 1891 times.
✗ Branch 1 not taken.
|
1891 | std::lock_guard<std::mutex> l(s_rtspWarningsLock); |
| 128 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1891 times.
|
1892 | for (const auto& w : s_rtspWarnings) { |
| 129 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
2 | WarningHolder::RemoveWarning(31, w); |
| 130 | } | ||
| 131 | 1891 | s_rtspWarnings.clear(); | |
| 132 | 1891 | } | |
| 133 | |||
| 134 | 3778 | bool RTSPOutputManager::LoadConfig() { | |
| 135 | 3778 | RTSPOutputConfig fresh; | |
| 136 | |||
| 137 |
3/4✓ Branch 0 taken 3778 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3746 times.
✓ Branch 3 taken 32 times.
|
3778 | if (!FileExists(m_configPath)) { |
| 138 |
1/2✓ Branch 0 taken 3746 times.
✗ Branch 1 not taken.
|
3746 | LogDebug(VB_MEDIAOUT, "RTSPOutputManager: No config at %s\n", m_configPath.c_str()); |
| 139 |
1/2✓ Branch 0 taken 3746 times.
✗ Branch 1 not taken.
|
3746 | std::unique_lock<std::mutex> lock(m_configMutex); |
| 140 | 3746 | m_config = std::move(fresh); | |
| 141 | 3746 | return false; | |
| 142 | 3746 | } | |
| 143 | |||
| 144 |
1/2✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
|
32 | std::ifstream ifs(m_configPath); |
| 145 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
|
32 | if (!ifs.is_open()) { |
| 146 | ✗ | LogWarn(VB_MEDIAOUT, "RTSPOutputManager: Cannot open %s\n", m_configPath.c_str()); | |
| 147 | ✗ | return false; | |
| 148 | } | ||
| 149 | |||
| 150 |
1/2✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
|
32 | Json::Value root; |
| 151 |
1/2✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
|
32 | Json::CharReaderBuilder builder; |
| 152 | 32 | std::string errors; | |
| 153 |
2/4✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 32 times.
|
32 | if (!Json::parseFromStream(builder, ifs, &root, &errors)) { |
| 154 | ✗ | LogWarn(VB_MEDIAOUT, "RTSPOutputManager: JSON parse error: %s\n", errors.c_str()); | |
| 155 | ✗ | return false; | |
| 156 | } | ||
| 157 | |||
| 158 | // asBool()/asInt() throw on a value of the wrong type ("enabled": "yes"), | ||
| 159 | // and this runs at startup with nothing above to catch it: fppd crash | ||
| 160 | // looped on a hand-edited file. Keep the last good config instead. | ||
| 161 | try { | ||
| 162 |
4/6✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✓ Branch 5 taken 2 times.
|
36 | fresh.enabled = root.get("enabled", false).asBool(); |
| 163 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
|
30 | fresh.port = root.get("port", RTSPOutput::DEFAULT_PORT).asInt(); |
| 164 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
|
30 | fresh.requireGroupSource = root.get("requireGroupSource", true).asBool(); |
| 165 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
|
30 | fresh.latencyMs = root.get("latencyMs", RTSPOutput::DEFAULT_LATENCY_MS).asInt(); |
| 166 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | } catch (const std::exception& ex) { |
| 167 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | LogWarn(VB_MEDIAOUT, "RTSPOutputManager: ignoring %s, a value has the wrong type: %s\n", |
| 168 | m_configPath.c_str(), ex.what()); | ||
| 169 | 2 | return false; | |
| 170 | 2 | } | |
| 171 |
2/4✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 30 times.
|
30 | if (fresh.port < 1024 || fresh.port > 65535) { |
| 172 | ✗ | LogWarn(VB_MEDIAOUT, "RTSPOutputManager: port %d out of range, using %d\n", | |
| 173 | fresh.port, RTSPOutput::DEFAULT_PORT); | ||
| 174 | ✗ | fresh.port = RTSPOutput::DEFAULT_PORT; | |
| 175 | } | ||
| 176 |
4/4✓ Branch 0 taken 28 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 26 times.
|
30 | if (fresh.latencyMs < 0 || fresh.latencyMs > 5000) { |
| 177 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | LogWarn(VB_MEDIAOUT, "RTSPOutputManager: latencyMs %d out of range, using %d\n", |
| 178 | fresh.latencyMs, RTSPOutput::DEFAULT_LATENCY_MS); | ||
| 179 | 4 | fresh.latencyMs = RTSPOutput::DEFAULT_LATENCY_MS; | |
| 180 | } | ||
| 181 | |||
| 182 |
1/2✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
|
30 | LoadMounts(fresh); |
| 183 | |||
| 184 |
1/2✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
|
30 | std::unique_lock<std::mutex> lock(m_configMutex); |
| 185 | 30 | m_config = std::move(fresh); | |
| 186 | 30 | return true; | |
| 187 | 3778 | } | |
| 188 | |||
| 189 | // Streams come from the Video Output Groups, flattened by the web UI into the | ||
| 190 | // same consumer list VideoOutputManager reads. Taking them from there rather | ||
| 191 | // than from our own config is the whole point: a group owns its video source, | ||
| 192 | // so an RTSP stream inherits it instead of naming it a second time where the | ||
| 193 | // two could drift apart -- and it picks up stream-slot groups for free. | ||
| 194 | 30 | void RTSPOutputManager::LoadMounts(RTSPOutputConfig& cfg) { | |
| 195 |
2/4✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 30 times.
|
30 | if (!FileExists(m_consumersPath)) { |
| 196 | ✗ | LogDebug(VB_MEDIAOUT, "RTSPOutputManager: no consumer config at %s\n", m_consumersPath.c_str()); | |
| 197 | ✗ | return; | |
| 198 | } | ||
| 199 |
1/2✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
|
30 | std::ifstream ifs(m_consumersPath); |
| 200 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 30 times.
|
30 | if (!ifs.is_open()) { |
| 201 | ✗ | LogWarn(VB_MEDIAOUT, "RTSPOutputManager: cannot open %s\n", m_consumersPath.c_str()); | |
| 202 | ✗ | return; | |
| 203 | } | ||
| 204 | |||
| 205 |
1/2✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
|
30 | Json::Value root; |
| 206 |
1/2✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
|
30 | Json::CharReaderBuilder builder; |
| 207 | 30 | std::string errors; | |
| 208 |
2/4✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 30 times.
|
30 | if (!Json::parseFromStream(builder, ifs, &root, &errors)) { |
| 209 | ✗ | LogWarn(VB_MEDIAOUT, "RTSPOutputManager: consumer JSON parse error: %s\n", errors.c_str()); | |
| 210 | ✗ | return; | |
| 211 | } | ||
| 212 |
2/4✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 30 times.
|
30 | if (!root.isArray()) { |
| 213 | ✗ | return; | |
| 214 | } | ||
| 215 | |||
| 216 |
7/12✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 30 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 60 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 30 times.
✓ Branch 11 taken 30 times.
|
60 | for (const auto& entry : root) { |
| 217 |
1/2✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
|
30 | RTSPOutputMount m; |
| 218 | // as in LoadConfig(): a value of the wrong type skips that stream | ||
| 219 | try { | ||
| 220 |
11/28✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 30 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 30 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 30 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 30 times.
✓ Branch 14 taken 30 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 30 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 30 times.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✓ Branch 21 taken 30 times.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
|
30 | if (!entry.isObject() || entry.get("type", "").asString() != "rtsp") { |
| 221 | ✗ | continue; | |
| 222 | } | ||
| 223 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
|
30 | m.name = entry.get("name", "").asString(); |
| 224 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
|
60 | m.mountPoint = NormaliseMountPoint(entry.get("mountPoint", "").asString(), |
| 225 |
1/2✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
|
60 | static_cast<int>(cfg.mounts.size() + 1)); |
| 226 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
|
30 | m.sourceNode = entry.get("sourceNode", "").asString(); |
| 227 |
8/12✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✓ Branch 3 taken 6 times.
✓ Branch 4 taken 24 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 24 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 24 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 24 times.
✓ Branch 11 taken 6 times.
|
30 | if (entry.isMember("streamSlots") && entry["streamSlots"].isArray()) { |
| 228 |
7/12✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 24 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 24 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 48 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 24 times.
✓ Branch 11 taken 24 times.
|
48 | for (const auto& sl : entry["streamSlots"]) { |
| 229 |
3/6✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 24 times.
✗ Branch 5 not taken.
|
24 | m.streamSlots.push_back(sl.asInt()); |
| 230 | } | ||
| 231 | } | ||
| 232 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
|
30 | m.width = entry.get("width", RTSPOutput::DEFAULT_WIDTH).asInt(); |
| 233 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
|
30 | m.height = entry.get("height", RTSPOutput::DEFAULT_HEIGHT).asInt(); |
| 234 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
|
30 | m.framerate = entry.get("framerate", RTSPOutput::DEFAULT_FRAMERATE).asInt(); |
| 235 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
|
30 | m.videoEncoding = entry.get("videoEncoding", "h264").asString(); |
| 236 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
|
30 | m.videoBitrate = entry.get("videoBitrate", RTSPOutput::DEFAULT_VIDEO_BITRATE).asInt(); |
| 237 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
|
30 | m.audioEnabled = entry.get("audioEnabled", false).asBool(); |
| 238 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
|
30 | m.audioNodeName = entry.get("audioNodeName", "").asString(); |
| 239 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
|
30 | m.audioBitrate = entry.get("audioBitrate", RTSPOutput::DEFAULT_AUDIO_BITRATE).asInt(); |
| 240 | ✗ | } catch (const std::exception& ex) { | |
| 241 | ✗ | LogWarn(VB_MEDIAOUT, "RTSPOutputManager: skipping a stream in %s, a value has the wrong type: %s\n", | |
| 242 | m_consumersPath.c_str(), ex.what()); | ||
| 243 | ✗ | continue; | |
| 244 | ✗ | } | |
| 245 | |||
| 246 |
2/4✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 30 times.
|
30 | if (m.ResolveSourceNode().empty()) { |
| 247 | // The group names neither a video source nor a stream slot, so | ||
| 248 | // there is no channel to read and the mount would serve nothing. | ||
| 249 | ✗ | LogWarn(VB_MEDIAOUT, "RTSPOutputManager: '%s' has no video source, skipping\n", | |
| 250 | m.name.c_str()); | ||
| 251 | ✗ | continue; | |
| 252 | } | ||
| 253 |
3/6✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 30 times.
|
30 | if (m.width <= 0 || m.height <= 0 || m.framerate <= 0) { |
| 254 | ✗ | LogWarn(VB_MEDIAOUT, "RTSPOutputManager: '%s' has an invalid size/rate (%dx%d@%d), " | |
| 255 | "using defaults\n", | ||
| 256 | m.name.c_str(), m.width, m.height, m.framerate); | ||
| 257 | ✗ | m.width = RTSPOutput::DEFAULT_WIDTH; | |
| 258 | ✗ | m.height = RTSPOutput::DEFAULT_HEIGHT; | |
| 259 | ✗ | m.framerate = RTSPOutput::DEFAULT_FRAMERATE; | |
| 260 | } | ||
| 261 |
4/6✓ Branch 0 taken 7 times.
✓ Branch 1 taken 23 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 30 times.
|
30 | if (m.audioEnabled && m.audioNodeName.empty()) { |
| 262 | ✗ | LogWarn(VB_MEDIAOUT, "RTSPOutputManager: '%s' wants audio but has no node name, " | |
| 263 | "serving video only\n", | ||
| 264 | m.name.c_str()); | ||
| 265 | ✗ | m.audioEnabled = false; | |
| 266 | } | ||
| 267 |
1/2✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
|
30 | cfg.mounts.push_back(std::move(m)); |
| 268 |
1/2✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
|
30 | } |
| 269 |
4/8✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 30 times.
✗ Branch 7 not taken.
|
30 | } |
| 270 | |||
| 271 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 272 | // Pipeline descriptions | ||
| 273 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 274 | 16 | std::string RTSPOutputManager::BuildFactoryLaunch(const RTSPOutputMount& mount) const { | |
| 275 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | std::ostringstream oss; |
| 276 | |||
| 277 | // Video: read the intervideo channel the rest of the video plane already | ||
| 278 | // publishes on, exactly as the HDMI and RTP consumers do. timeout is the | ||
| 279 | // same 5s the other consumers use -- it must not overflow gint64 once | ||
| 280 | // converted to microseconds (see VideoOutputManager::StartConsumer). | ||
| 281 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
32 | oss << "( intervideosrc timeout=5000000000 channel=" << RtspGstQuote(mount.ResolveSourceNode()) |
| 282 | << " ! videoconvert ! videoscale ! videorate" | ||
| 283 |
6/12✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 16 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 16 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 16 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 16 times.
✗ Branch 11 not taken.
|
32 | << " ! video/x-raw,width=" << mount.width |
| 284 |
2/4✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
|
16 | << ",height=" << mount.height |
| 285 | << ",pixel-aspect-ratio=1/1" | ||
| 286 |
3/6✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 16 times.
✗ Branch 5 not taken.
|
16 | << ",framerate=" << mount.framerate << "/1" |
| 287 |
2/4✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
|
16 | << " ! queue max-size-buffers=2 leaky=downstream "; |
| 288 | |||
| 289 | // Encoders are all configured for live streaming rather than filesize: | ||
| 290 | // zerolatency/ultrafast, no B-frames, and a keyframe roughly every two | ||
| 291 | // seconds so a client that joins mid-stream gets a picture quickly. | ||
| 292 | 16 | const int keyIntMax = mount.framerate * 2; | |
| 293 |
2/4✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 16 times.
|
16 | if (mount.videoEncoding == "h265") { |
| 294 | oss << "! x265enc tune=zerolatency speed-preset=ultrafast" | ||
| 295 | ✗ | << " bitrate=" << mount.videoBitrate | |
| 296 | ✗ | << " key-int-max=" << keyIntMax | |
| 297 | ✗ | << " ! rtph265pay name=pay0 pt=" << RTSPOutput::VIDEO_PAYLOAD_TYPE | |
| 298 | ✗ | << " config-interval=1 "; | |
| 299 |
3/4✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 2 times.
|
16 | } else if (mount.videoEncoding == "mjpeg") { |
| 300 | // Motion JPEG has no inter-frame prediction, so every frame is a | ||
| 301 | // keyframe and none of the GOP settings above apply. | ||
| 302 | oss << "! jpegenc quality=80" | ||
| 303 |
2/4✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
|
14 | << " ! rtpjpegpay name=pay0 pt=26 "; |
| 304 | } else { | ||
| 305 | // h264 is the default and the only encoding every client understands. | ||
| 306 | oss << "! x264enc tune=zerolatency speed-preset=ultrafast bframes=0" | ||
| 307 |
3/6✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
|
2 | << " bitrate=" << mount.videoBitrate |
| 308 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | << " key-int-max=" << keyIntMax |
| 309 | << " ! video/x-h264,profile=baseline" | ||
| 310 |
3/6✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
|
2 | << " ! rtph264pay name=pay0 pt=" << RTSPOutput::VIDEO_PAYLOAD_TYPE |
| 311 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | << " config-interval=1 "; |
| 312 | } | ||
| 313 | |||
| 314 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 12 times.
|
16 | if (mount.audioEnabled) { |
| 315 | // Audio arrives over an interaudio channel rather than straight from | ||
| 316 | // PipeWire, so the PipeWire node lives in the always-running bridge | ||
| 317 | // pipeline instead of in here. A GstRTSPMedia only exists while a | ||
| 318 | // client is connected, and a node that came and went with client | ||
| 319 | // connections could not be wired up in the mix bus UI ahead of time. | ||
| 320 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
8 | oss << " interaudiosrc channel=" << RtspGstQuote(mount.AudioChannel()) |
| 321 | << " ! audioconvert ! audioresample" | ||
| 322 |
6/12✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 4 times.
✗ Branch 11 not taken.
|
8 | << " ! audio/x-raw,rate=" << RTSPOutput::AUDIO_RATE |
| 323 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | << ",channels=" << RTSPOutput::AUDIO_CHANNELS |
| 324 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | << " ! opusenc bitrate=" << mount.audioBitrate |
| 325 |
3/6✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
|
4 | << " ! rtpopuspay name=pay1 pt=" << RTSPOutput::AUDIO_PAYLOAD_TYPE << " "; |
| 326 | } | ||
| 327 | |||
| 328 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | oss << ")"; |
| 329 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
32 | return oss.str(); |
| 330 | 16 | } | |
| 331 | |||
| 332 | 4 | bool RTSPOutputManager::StartAudioBridge(const RTSPOutputMount& mount, bool requireGroupSource, | |
| 333 | std::vector<AudioBridge>& bridges) { | ||
| 334 | 4 | AudioBridge bridge; | |
| 335 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | bridge.mountPoint = mount.mountPoint; |
| 336 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | bridge.nodeName = mount.audioNodeName; |
| 337 | |||
| 338 |
2/8✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 4 times.
|
4 | if (requireGroupSource && !PipeWireGraphFeedsNode(mount.audioNodeName)) { |
| 339 | // Nothing routes into this node yet, and a pipewiresrc with | ||
| 340 | // node.autoconnect=false and nothing linked cannot preroll: starting | ||
| 341 | // it would block for 30 seconds and then report a failure for a stream | ||
| 342 | // the user has not finished wiring up. Leave it for the next apply, | ||
| 343 | // which the Audio Output Groups page performs anyway. | ||
| 344 | ✗ | LogInfo(VB_MEDIAOUT, "RTSPOutputManager: audio for '%s' is waiting for an Audio " | |
| 345 | "Output Group to feed '%s'\n", | ||
| 346 | mount.name.c_str(), mount.audioNodeName.c_str()); | ||
| 347 | ✗ | bridge.waitingForSource = true; | |
| 348 | ✗ | bridges.push_back(bridge); | |
| 349 | ✗ | return false; | |
| 350 | } | ||
| 351 | |||
| 352 | // pipewiresrc -> interaudiosink: holds the PipeWire node open for the | ||
| 353 | // whole life of the config so routing can be set up before any client | ||
| 354 | // connects, and does only format conversion. The encode stays in the | ||
| 355 | // RTSP media, where it runs only while somebody is watching. | ||
| 356 | // | ||
| 357 | // The audioconvert/audioresample/audioconvert/audiorate split mirrors | ||
| 358 | // OpusRTPManager::CreateSendPipeline: the graph does not necessarily run | ||
| 359 | // at 48000, and without a real resampler negotiation never completes and | ||
| 360 | // set_state() blocks rather than failing cleanly. | ||
| 361 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | std::ostringstream oss; |
| 362 | oss << "pipewiresrc name=pwsrc min-buffers=2 do-timestamp=true" | ||
| 363 | << " ! queue max-size-buffers=0 max-size-bytes=0 max-size-time=200000000 leaky=downstream" | ||
| 364 | << " ! audioconvert ! audioresample ! audioconvert ! audiorate" | ||
| 365 |
5/10✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
|
4 | << " ! audio/x-raw,rate=" << RTSPOutput::AUDIO_RATE |
| 366 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | << ",channels=" << RTSPOutput::AUDIO_CHANNELS |
| 367 |
4/8✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
|
4 | << " ! interaudiosink sync=false channel=" << RtspGstQuote(mount.AudioChannel()); |
| 368 | |||
| 369 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | std::string desc = oss.str(); |
| 370 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | LogInfo(VB_MEDIAOUT, "RTSPOutputManager: audio bridge for '%s': %s\n", |
| 371 | mount.name.c_str(), desc.c_str()); | ||
| 372 | |||
| 373 | 4 | GError* error = nullptr; | |
| 374 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | GstElement* pipeline = gst_parse_launch(desc.c_str(), &error); |
| 375 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | if (!pipeline) { |
| 376 | ✗ | LogErr(VB_MEDIAOUT, "RTSPOutputManager: audio bridge for '%s' failed to build: %s\n", | |
| 377 | mount.name.c_str(), error ? error->message : "unknown error"); | ||
| 378 | ✗ | if (error) { | |
| 379 | ✗ | g_error_free(error); | |
| 380 | } | ||
| 381 | ✗ | return false; | |
| 382 | } | ||
| 383 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | if (error) { |
| 384 | ✗ | LogWarn(VB_MEDIAOUT, "RTSPOutputManager: audio bridge warning for '%s': %s\n", | |
| 385 | mount.name.c_str(), error->message); | ||
| 386 | ✗ | g_error_free(error); | |
| 387 | } | ||
| 388 | |||
| 389 | // Inline GstStructure values containing '/' (node.latency=1024/48000) | ||
| 390 | // crash gst_value_deserialize, so these are set after the parse -- the | ||
| 391 | // same reason OpusRTPManager sets them here rather than in the string. | ||
| 392 |
3/6✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
|
4 | GstElement* pwsrc = gst_bin_get_by_name(GST_BIN(pipeline), "pwsrc"); |
| 393 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | if (pwsrc) { |
| 394 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | std::string desc2 = "FPP RTSP Audio: " + mount.name; |
| 395 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | GstStructure* props = gst_structure_new("props", |
| 396 | "node.name", G_TYPE_STRING, mount.audioNodeName.c_str(), | ||
| 397 | "node.description", G_TYPE_STRING, desc2.c_str(), | ||
| 398 | "node.autoconnect", G_TYPE_BOOLEAN, FALSE, | ||
| 399 | "node.latency", G_TYPE_STRING, "1024/48000", | ||
| 400 | NULL); | ||
| 401 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | g_object_set(pwsrc, "stream-properties", props, NULL); |
| 402 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | gst_structure_free(props); |
| 403 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | gst_object_unref(pwsrc); |
| 404 | 4 | } | |
| 405 | |||
| 406 | // On the system clock, not the GstPipeWireClock pipewiresrc offers: for | ||
| 407 | // a follower stream that clock stops advancing once the graph's real | ||
| 408 | // driver takes the node over (it reads the stream's time, whose rate is | ||
| 409 | // then 0), and pipewiresrc waits on it before every buffer. Linked while | ||
| 410 | // it was starting -- an audio group targeting the node links it as soon | ||
| 411 | // as it appears -- the bridge passed a buffer or two and then nothing, so | ||
| 412 | // RTSP clients heard silence. | ||
| 413 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | GstClock* sysClock = gst_system_clock_obtain(); |
| 414 |
3/6✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
|
4 | gst_pipeline_use_clock(GST_PIPELINE(pipeline), sysClock); |
| 415 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | gst_object_unref(sysClock); |
| 416 | |||
| 417 | // Started on its own thread. A pipewiresrc whose node nothing feeds | ||
| 418 | // (requireGroupSource off) takes 30 s to fail the state change, and the | ||
| 419 | // caller is ApplyConfig() -- fppd's main loop for reloadRTSPOutputs, and | ||
| 420 | // startup. The thread holds its own ref; the bridge entry owns the | ||
| 421 | // pipeline and StopAudioBridges() tears it down whether or not the start | ||
| 422 | // has finished. | ||
| 423 | 4 | bridge.pipeline = pipeline; | |
| 424 |
3/6✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
|
4 | GstElement* startRef = GST_ELEMENT(gst_object_ref(pipeline)); |
| 425 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | std::string mountName = mount.name; |
| 426 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
12 | std::thread([startRef, mountName]() { |
| 427 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | SetThreadName("FPP-RTSPAudio"); |
| 428 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
4 | if (gst_element_set_state(startRef, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) { |
| 429 | ✗ | LogErr(VB_MEDIAOUT, "RTSPOutputManager: audio bridge for '%s' failed to start\n", | |
| 430 | mountName.c_str()); | ||
| 431 | ✗ | RaiseRtspWarning("RTSP output '" + mountName + "': audio bridge failed to start"); | |
| 432 | } | ||
| 433 | 2 | gst_object_unref(startRef); | |
| 434 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
6 | }).detach(); |
| 435 | |||
| 436 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | bridges.push_back(bridge); |
| 437 | 4 | return true; | |
| 438 | 4 | } | |
| 439 | |||
| 440 | 4216 | void RTSPOutputManager::StopAudioBridges() { | |
| 441 | // Taken out under m_configMutex: GetStatus() walks the vector on HTTP | ||
| 442 | // threads, and clearing it under only the apply lock freed elements out | ||
| 443 | // from under it. Stopped with the lock released. | ||
| 444 | 4216 | std::vector<AudioBridge> bridges; | |
| 445 | { | ||
| 446 |
1/2✓ Branch 0 taken 4216 times.
✗ Branch 1 not taken.
|
4216 | std::unique_lock<std::mutex> lock(m_configMutex); |
| 447 | 4216 | bridges.swap(m_audioBridges); | |
| 448 | 4216 | } | |
| 449 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4216 times.
|
4220 | for (auto& b : bridges) { |
| 450 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | if (b.pipeline) { |
| 451 | // Bounded: a bridge whose start is still stuck in pipewiresrc | ||
| 452 | // holds the state lock, and this runs on the main loop and in | ||
| 453 | // Shutdown(). A wedged one is left to the reaper. | ||
| 454 |
3/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
|
4 | if (!GStreamerOutput::SetPipelineToNullBounded(b.pipeline, 2000)) { |
| 455 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | LogWarn(VB_MEDIAOUT, "RTSPOutputManager: audio bridge for '%s' did not stop, abandoning it\n", |
| 456 | b.mountPoint.c_str()); | ||
| 457 | } | ||
| 458 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | gst_object_unref(b.pipeline); |
| 459 | 4 | b.pipeline = nullptr; | |
| 460 | } | ||
| 461 | } | ||
| 462 | 4216 | } | |
| 463 | |||
| 464 | // Concurrent RTSP sessions. One shared encoder serves them all, but each | ||
| 465 | // still gets its own transport. | ||
| 466 | static constexpr guint RTSP_MAX_SESSIONS = 32; | ||
| 467 | |||
| 468 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 469 | // Server lifecycle | ||
| 470 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 471 | 16 | bool RTSPOutputManager::StartServer() { | |
| 472 | 16 | GStreamerOutput::EnsureGStreamerInit(); | |
| 473 | |||
| 474 | 16 | setenv("PIPEWIRE_RUNTIME_DIR", "/run/pipewire-fpp", 0); | |
| 475 | |||
| 476 | // gst-rtsp-server services its clients from a GMainContext, and FPP runs | ||
| 477 | // no main loop of its own. Give the server a private context rather than | ||
| 478 | // the default one, so nothing else that later wants the default context | ||
| 479 | // (a plugin, a future subsystem) has to share a loop with it. | ||
| 480 | 16 | m_mainContext = g_main_context_new(); | |
| 481 | 16 | m_mainLoop = g_main_loop_new(m_mainContext, FALSE); | |
| 482 | |||
| 483 | 16 | m_server = gst_rtsp_server_new(); | |
| 484 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | gst_rtsp_server_set_service(m_server, std::to_string(m_config.port).c_str()); |
| 485 | |||
| 486 | 16 | GstRTSPMountPoints* mounts = gst_rtsp_server_get_mount_points(m_server); | |
| 487 | 16 | int mounted = 0; | |
| 488 |
2/2✓ Branch 0 taken 16 times.
✓ Branch 1 taken 16 times.
|
32 | for (const auto& mount : m_config.mounts) { |
| 489 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | std::string launch = BuildFactoryLaunch(mount); |
| 490 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | LogInfo(VB_MEDIAOUT, "RTSPOutputManager: mount %s -> %s\n", |
| 491 | mount.mountPoint.c_str(), launch.c_str()); | ||
| 492 | |||
| 493 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | GstRTSPMediaFactory* factory = gst_rtsp_media_factory_new(); |
| 494 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | gst_rtsp_media_factory_set_launch(factory, launch.c_str()); |
| 495 | // Shared: every client is served from ONE pipeline, so the encoder | ||
| 496 | // runs once no matter how many receivers connect. Without this each | ||
| 497 | // client would get its own x264enc, which a Pi cannot afford. | ||
| 498 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | gst_rtsp_media_factory_set_shared(factory, TRUE); |
| 499 | // The sources are live: never let a client's disconnect send EOS and | ||
| 500 | // tear the shared media down under the others. | ||
| 501 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | gst_rtsp_media_factory_set_eos_shutdown(factory, FALSE); |
| 502 | // Without this each media keeps gst-rtsp-server's 200ms default, which | ||
| 503 | // a viewer feels as lag for no benefit on a live local source. | ||
| 504 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | gst_rtsp_media_factory_set_latency(factory, m_config.latencyMs); |
| 505 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | gst_rtsp_mount_points_add_factory(mounts, mount.mountPoint.c_str(), factory); |
| 506 | 16 | mounted++; | |
| 507 | 16 | } | |
| 508 | 16 | g_object_unref(mounts); | |
| 509 | |||
| 510 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
|
16 | if (mounted == 0) { |
| 511 | ✗ | LogInfo(VB_MEDIAOUT, "RTSPOutputManager: no enabled mounts, not starting the server\n"); | |
| 512 | ✗ | StopServer(); | |
| 513 | ✗ | return false; | |
| 514 | } | ||
| 515 | |||
| 516 | 16 | m_serverSourceId = gst_rtsp_server_attach(m_server, m_mainContext); | |
| 517 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 15 times.
|
16 | if (m_serverSourceId == 0) { |
| 518 | 1 | LogErr(VB_MEDIAOUT, "RTSPOutputManager: could not bind RTSP port %d " | |
| 519 | "(in use, or not permitted)\n", | ||
| 520 | m_config.port); | ||
| 521 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
1 | RaiseRtspWarning("RTSP output: could not bind port " + std::to_string(m_config.port)); |
| 522 | 1 | StopServer(); | |
| 523 | 1 | return false; | |
| 524 | } | ||
| 525 | |||
| 526 | // A UDP viewer that vanishes without a TEARDOWN (a phone app killed, a | ||
| 527 | // dropped network) leaves its session behind, and the server goes on | ||
| 528 | // sending its RTP into the void. The session times out (60s by | ||
| 529 | // default), but only once someone runs the pool's cleanup -- which | ||
| 530 | // gst-rtsp-server leaves to the application. The cap bounds what a | ||
| 531 | // crowd of viewers, or one client opening sessions in a loop, can cost. | ||
| 532 |
1/2✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
|
15 | if (GstRTSPSessionPool* pool = gst_rtsp_server_get_session_pool(m_server)) { |
| 533 | 15 | gst_rtsp_session_pool_set_max_sessions(pool, RTSP_MAX_SESSIONS); | |
| 534 | 15 | g_object_unref(pool); | |
| 535 | } | ||
| 536 | 15 | m_sessionCleanup = g_timeout_source_new_seconds(2); | |
| 537 |
1/2✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
|
15 | g_source_set_callback( |
| 538 | m_sessionCleanup, | ||
| 539 | ✗ | [](gpointer data) -> gboolean { | |
| 540 | 39 | GstRTSPSessionPool* pool = gst_rtsp_server_get_session_pool(static_cast<GstRTSPServer*>(data)); | |
| 541 |
1/2✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
|
39 | if (pool) { |
| 542 | 39 | guint expired = gst_rtsp_session_pool_cleanup(pool); | |
| 543 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 38 times.
|
39 | if (expired) { |
| 544 | 1 | LogInfo(VB_MEDIAOUT, "RTSPOutputManager: expired %u session(s) whose client went away\n", expired); | |
| 545 | } | ||
| 546 | 39 | g_object_unref(pool); | |
| 547 | } | ||
| 548 | 39 | return G_SOURCE_CONTINUE; | |
| 549 | }, | ||
| 550 |
0/2✗ Branch 0 not taken.
✗ Branch 1 not taken.
|
15 | m_server, nullptr); |
| 551 | 15 | g_source_attach(m_sessionCleanup, m_mainContext); | |
| 552 | |||
| 553 | 15 | m_loopThread = std::thread([this]() { | |
| 554 | // The loop must run with the same context the server was attached to, | ||
| 555 | // and pushing it as this thread's default keeps any source the server | ||
| 556 | // creates on our loop rather than the process default. | ||
| 557 | 15 | g_main_context_push_thread_default(m_mainContext); | |
| 558 | 15 | g_main_loop_run(m_mainLoop); | |
| 559 | 15 | g_main_context_pop_thread_default(m_mainContext); | |
| 560 |
1/2✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
|
15 | }); |
| 561 | |||
| 562 | 15 | LogInfo(VB_MEDIAOUT, "RTSPOutputManager: serving %d mount(s) on rtsp://<host>:%d\n", | |
| 563 | mounted, m_config.port); | ||
| 564 | 15 | return true; | |
| 565 | } | ||
| 566 | |||
| 567 | 4217 | void RTSPOutputManager::StopServer() { | |
| 568 |
2/2✓ Branch 0 taken 16 times.
✓ Branch 1 taken 4201 times.
|
4217 | if (m_server) { |
| 569 | // Close the connected clients and their sessions first, while the loop | ||
| 570 | // that services them still runs. Quitting the loop and dropping the | ||
| 571 | // server left them connected: their media (intervideosrc -> x264enc) | ||
| 572 | // kept encoding through every reload and shutdown, a viewer stayed on | ||
| 573 | // the old config, and its context held the old listening socket. | ||
| 574 |
2/4✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
|
16 | g_list_free_full(gst_rtsp_server_client_filter( |
| 575 | m_server, | ||
| 576 | 4 | [](GstRTSPServer*, GstRTSPClient*, gpointer) { return GST_RTSP_FILTER_REMOVE; }, | |
| 577 | nullptr), | ||
| 578 | g_object_unref); | ||
| 579 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | if (GstRTSPSessionPool* pool = gst_rtsp_server_get_session_pool(m_server)) { |
| 580 |
2/4✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
|
16 | g_list_free_full(gst_rtsp_session_pool_filter( |
| 581 | pool, | ||
| 582 | 39 | [](GstRTSPSessionPool*, GstRTSPSession*, gpointer) { return GST_RTSP_FILTER_REMOVE; }, | |
| 583 | nullptr), | ||
| 584 | g_object_unref); | ||
| 585 | 16 | g_object_unref(pool); | |
| 586 | } | ||
| 587 | } | ||
| 588 |
2/2✓ Branch 0 taken 16 times.
✓ Branch 1 taken 4201 times.
|
4217 | if (m_mainLoop) { |
| 589 | 16 | g_main_loop_quit(m_mainLoop); | |
| 590 | } | ||
| 591 |
2/2✓ Branch 0 taken 15 times.
✓ Branch 1 taken 4202 times.
|
4217 | if (m_loopThread.joinable()) { |
| 592 | 15 | m_loopThread.join(); | |
| 593 | } | ||
| 594 |
2/2✓ Branch 0 taken 15 times.
✓ Branch 1 taken 4202 times.
|
4217 | if (m_sessionCleanup) { |
| 595 | // It points at m_server, which goes below. | ||
| 596 | 15 | g_source_destroy(m_sessionCleanup); | |
| 597 | 15 | g_source_unref(m_sessionCleanup); | |
| 598 | 15 | m_sessionCleanup = nullptr; | |
| 599 | } | ||
| 600 |
2/2✓ Branch 0 taken 15 times.
✓ Branch 1 taken 4202 times.
|
4217 | if (m_serverSourceId != 0) { |
| 601 | // Only safe now that the loop has stopped: removing the server's | ||
| 602 | // source closes its listening socket. | ||
| 603 | 15 | GSource* src = g_main_context_find_source_by_id(m_mainContext, m_serverSourceId); | |
| 604 |
1/2✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
|
15 | if (src) { |
| 605 | 15 | g_source_destroy(src); | |
| 606 | } | ||
| 607 | 15 | m_serverSourceId = 0; | |
| 608 | } | ||
| 609 |
2/2✓ Branch 0 taken 16 times.
✓ Branch 1 taken 4201 times.
|
4217 | if (m_server) { |
| 610 | 16 | g_object_unref(m_server); | |
| 611 | 16 | m_server = nullptr; | |
| 612 | } | ||
| 613 |
2/2✓ Branch 0 taken 16 times.
✓ Branch 1 taken 4201 times.
|
4217 | if (m_mainLoop) { |
| 614 | 16 | g_main_loop_unref(m_mainLoop); | |
| 615 | 16 | m_mainLoop = nullptr; | |
| 616 | } | ||
| 617 |
2/2✓ Branch 0 taken 16 times.
✓ Branch 1 taken 4201 times.
|
4217 | if (m_mainContext) { |
| 618 | 16 | g_main_context_unref(m_mainContext); | |
| 619 | 16 | m_mainContext = nullptr; | |
| 620 | } | ||
| 621 | 4217 | } | |
| 622 | |||
| 623 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 624 | // Lifecycle | ||
| 625 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 626 | 1887 | bool RTSPOutputManager::Init() { | |
| 627 | 1887 | LoadConfig(); | |
| 628 | 1887 | return true; | |
| 629 | } | ||
| 630 | |||
| 631 | 1891 | bool RTSPOutputManager::ApplyConfig() { | |
| 632 |
1/2✓ Branch 0 taken 1891 times.
✗ Branch 1 not taken.
|
1891 | std::unique_lock<std::mutex> applyLock(m_applyMutex); |
| 633 | |||
| 634 | // Before anything below builds a pipeline. The audio bridges are parsed | ||
| 635 | // before StartServer(), and at boot nothing has initialised GStreamer by | ||
| 636 | // then: gst_parse_launch() crashed in gst_value_deserialize(), and the | ||
| 637 | // crash handler's exit() then hung in this object's destructor on the | ||
| 638 | // apply lock until the crash watchdog killed fppd -- on every boot. | ||
| 639 |
1/2✓ Branch 0 taken 1891 times.
✗ Branch 1 not taken.
|
1891 | GStreamerOutput::EnsureGStreamerInit(); |
| 640 | |||
| 641 |
1/2✓ Branch 0 taken 1891 times.
✗ Branch 1 not taken.
|
1891 | StopServer(); |
| 642 |
1/2✓ Branch 0 taken 1891 times.
✗ Branch 1 not taken.
|
1891 | StopAudioBridges(); |
| 643 | 1891 | m_active = false; | |
| 644 |
1/2✓ Branch 0 taken 1891 times.
✗ Branch 1 not taken.
|
1891 | RetractRtspWarnings(); |
| 645 | |||
| 646 |
1/2✓ Branch 0 taken 1891 times.
✗ Branch 1 not taken.
|
1891 | LoadConfig(); |
| 647 | |||
| 648 | 1891 | RTSPOutputConfig cfg; | |
| 649 | { | ||
| 650 |
1/2✓ Branch 0 taken 1891 times.
✗ Branch 1 not taken.
|
1891 | std::unique_lock<std::mutex> lock(m_configMutex); |
| 651 |
1/2✓ Branch 0 taken 1891 times.
✗ Branch 1 not taken.
|
1891 | cfg = m_config; |
| 652 | 1891 | } | |
| 653 |
2/2✓ Branch 0 taken 1875 times.
✓ Branch 1 taken 16 times.
|
1891 | if (!cfg.enabled) { |
| 654 |
1/2✓ Branch 0 taken 1875 times.
✗ Branch 1 not taken.
|
1875 | LogDebug(VB_MEDIAOUT, "RTSPOutputManager: disabled\n"); |
| 655 | 1875 | return true; | |
| 656 | } | ||
| 657 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
|
16 | if (cfg.mounts.empty()) { |
| 658 | ✗ | LogInfo(VB_MEDIAOUT, "RTSPOutputManager: enabled but no mounts configured\n"); | |
| 659 | ✗ | return true; | |
| 660 | } | ||
| 661 | |||
| 662 | // Bridges first: the RTSP media's interaudiosrc has nothing to read until | ||
| 663 | // its bridge is publishing, and a client can connect the moment the | ||
| 664 | // server's socket is up. Built without m_configMutex (see | ||
| 665 | // StartAudioBridge()). | ||
| 666 | 16 | std::vector<AudioBridge> bridges; | |
| 667 |
2/2✓ Branch 0 taken 16 times.
✓ Branch 1 taken 16 times.
|
32 | for (const auto& mount : cfg.mounts) { |
| 668 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 12 times.
|
16 | if (mount.audioEnabled) { |
| 669 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | StartAudioBridge(mount, cfg.requireGroupSource, bridges); |
| 670 | } | ||
| 671 | } | ||
| 672 | |||
| 673 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | std::unique_lock<std::mutex> lock(m_configMutex); |
| 674 | 16 | m_audioBridges = std::move(bridges); | |
| 675 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | bool ok = StartServer(); |
| 676 | 16 | m_active = ok; | |
| 677 | 16 | return ok; | |
| 678 | 1891 | } | |
| 679 | |||
| 680 | 11 | bool RTSPOutputManager::ServesStreamSlot(int slot) { | |
| 681 |
1/2✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
|
11 | std::unique_lock<std::mutex> lock(m_configMutex); |
| 682 |
2/2✓ Branch 0 taken 10 times.
✓ Branch 1 taken 1 times.
|
11 | if (!m_config.enabled) { |
| 683 | 10 | return false; | |
| 684 | } | ||
| 685 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | std::string channel = "fppd_video_stream_" + std::to_string(slot); |
| 686 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | for (const auto& mount : m_config.mounts) { |
| 687 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
1 | if (mount.ResolveSourceNode() == channel) { |
| 688 | 1 | return true; | |
| 689 | } | ||
| 690 | } | ||
| 691 | ✗ | return false; | |
| 692 | 11 | } | |
| 693 | |||
| 694 | 2325 | void RTSPOutputManager::Shutdown() { | |
| 695 |
1/2✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
|
2325 | std::unique_lock<std::mutex> applyLock(m_applyMutex); |
| 696 |
1/2✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
|
2325 | StopServer(); |
| 697 |
1/2✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
|
2325 | StopAudioBridges(); |
| 698 | 2325 | m_active = false; | |
| 699 | 2325 | } | |
| 700 | |||
| 701 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 702 | // Status | ||
| 703 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 704 | 20 | RTSPOutputManager::Status RTSPOutputManager::GetStatus() { | |
| 705 | 20 | Status st; | |
| 706 |
1/2✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
|
20 | std::unique_lock<std::mutex> lock(m_configMutex); |
| 707 | |||
| 708 | 20 | st.serverRunning = m_active.load(); | |
| 709 | 20 | st.port = m_config.port; | |
| 710 | 20 | st.latencyMs = m_config.latencyMs; | |
| 711 | |||
| 712 |
2/2✓ Branch 0 taken 15 times.
✓ Branch 1 taken 20 times.
|
35 | for (const auto& mount : m_config.mounts) { |
| 713 | 15 | Status::MountStatus ms; | |
| 714 |
1/2✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
|
15 | ms.name = mount.name; |
| 715 |
1/2✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
|
15 | ms.mountPoint = mount.mountPoint; |
| 716 | 15 | ms.enabled = true; | |
| 717 | 15 | ms.audioEnabled = mount.audioEnabled; | |
| 718 |
1/2✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
|
15 | ms.audioNodeName = mount.audioNodeName; |
| 719 | // Host-relative on purpose: fppd does not know which of its addresses | ||
| 720 | // the person reading this reached it on, and the UI does. | ||
| 721 |
2/4✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
|
15 | ms.url = "rtsp://<host>:" + std::to_string(m_config.port) + mount.mountPoint; |
| 722 | |||
| 723 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 15 times.
|
16 | for (const auto& b : m_audioBridges) { |
| 724 |
3/6✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
|
1 | if (b.mountPoint == mount.mountPoint && b.waitingForSource) { |
| 725 | ✗ | ms.audioWaitingForSource = true; | |
| 726 | ✗ | ms.note = "Audio is waiting for an Audio Output Group to feed '" + mount.audioNodeName + "'. Video is unaffected."; | |
| 727 | } | ||
| 728 | } | ||
| 729 |
1/2✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
|
15 | st.mounts.push_back(std::move(ms)); |
| 730 | 15 | } | |
| 731 | 20 | return st; | |
| 732 | 20 | } | |
| 733 | |||
| 734 | 20 | HttpResponsePtr RTSPOutputManager::render_GET(const HttpRequestPtr& req) { | |
| 735 |
2/4✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
|
20 | std::string url(req->path()); |
| 736 | |||
| 737 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
|
20 | if (url.find("/rtspoutput/") == 0) { |
| 738 | ✗ | url = url.substr(12); | |
| 739 |
2/4✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
|
20 | } else if (url == "/rtspoutput") { |
| 740 |
1/2✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
|
20 | url = "status"; |
| 741 | } | ||
| 742 | |||
| 743 |
2/4✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
|
20 | if (url == "status") { |
| 744 |
1/2✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
|
20 | Status st = GetStatus(); |
| 745 |
1/2✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
|
20 | Json::Value result; |
| 746 |
2/4✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
|
20 | result["active"] = st.serverRunning; |
| 747 |
2/4✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
|
20 | result["port"] = st.port; |
| 748 |
2/4✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
|
20 | result["latencyMs"] = st.latencyMs; |
| 749 | |||
| 750 |
1/2✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
|
20 | Json::Value mounts(Json::arrayValue); |
| 751 |
2/2✓ Branch 0 taken 15 times.
✓ Branch 1 taken 20 times.
|
35 | for (const auto& m : st.mounts) { |
| 752 |
1/2✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
|
15 | Json::Value mj; |
| 753 |
2/4✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
|
15 | mj["name"] = m.name; |
| 754 |
2/4✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
|
15 | mj["mountPoint"] = m.mountPoint; |
| 755 |
2/4✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
|
15 | mj["url"] = m.url; |
| 756 |
2/4✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
|
15 | mj["enabled"] = m.enabled; |
| 757 |
2/4✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
|
15 | mj["audioEnabled"] = m.audioEnabled; |
| 758 |
2/2✓ Branch 0 taken 10 times.
✓ Branch 1 taken 5 times.
|
15 | if (m.audioEnabled) { |
| 759 |
2/4✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
|
10 | mj["audioNodeName"] = m.audioNodeName; |
| 760 | } | ||
| 761 | // Kept distinct from an error for the same reason Opus RTP does: | ||
| 762 | // the UI renders a failure in red and this as guidance. | ||
| 763 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
|
15 | if (m.audioWaitingForSource) { |
| 764 | ✗ | mj["audioWaitingForSource"] = true; | |
| 765 | } | ||
| 766 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
|
15 | if (!m.note.empty()) { |
| 767 | ✗ | mj["note"] = m.note; | |
| 768 | } | ||
| 769 |
1/2✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
|
15 | mounts.append(mj); |
| 770 | 15 | } | |
| 771 |
2/4✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
|
20 | result["mounts"] = mounts; |
| 772 | |||
| 773 |
1/2✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
|
20 | Json::StreamWriterBuilder wbuilder; |
| 774 |
3/6✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20 times.
✗ Branch 5 not taken.
|
60 | wbuilder["indentation"] = ""; |
| 775 |
3/6✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20 times.
✗ Branch 5 not taken.
|
20 | return makeStringResponse(Json::writeString(wbuilder, result), 200, "application/json"); |
| 776 | 20 | } | |
| 777 | |||
| 778 | ✗ | return makeStringResponse("{\"error\":\"unknown endpoint\"}", 404, "application/json"); | |
| 779 | 20 | } | |
| 780 | |||
| 781 | #endif // HAS_RTSP_OUTPUT_GSTREAMER | ||
| 782 |