| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * This file is part of the Falcon Player (FPP) and is Copyright (C) | ||
| 3 | * 2013-2025 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 "fpp-json.h" | ||
| 16 | #include "fpphttp.h" // drogon/HTTP helpers used here; no longer pulled transitively (see fpphttp_types.h) | ||
| 17 | |||
| 18 | #include "Warnings.h" // WarningHolder -- needed directly for NOPCH builds | ||
| 19 | |||
| 20 | #include "OpusRTPManager.h" | ||
| 21 | #include "GStreamerOut.h" | ||
| 22 | #include "PipeWireGraphConfig.h" | ||
| 23 | |||
| 24 | #ifdef HAS_OPUS_RTP_GSTREAMER | ||
| 25 | |||
| 26 | #include <gst/gst.h> | ||
| 27 | |||
| 28 | #include <arpa/inet.h> | ||
| 29 | #include <cctype> | ||
| 30 | #include <ifaddrs.h> | ||
| 31 | #include <net/if.h> | ||
| 32 | #include <netinet/in.h> | ||
| 33 | #include <sys/socket.h> | ||
| 34 | |||
| 35 | #include <algorithm> | ||
| 36 | #include <chrono> | ||
| 37 | #include <cstring> | ||
| 38 | #include <sstream> | ||
| 39 | |||
| 40 | #include "common.h" | ||
| 41 | #include "log.h" | ||
| 42 | #include "mediaoutput.h" | ||
| 43 | #include "settings.h" | ||
| 44 | |||
| 45 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 46 | // Singleton instance | ||
| 47 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 48 | static OpusRTPManager s_opusRTPManager; | ||
| 49 | OpusRTPManager& OpusRTPManager::INSTANCE = s_opusRTPManager; | ||
| 50 | |||
| 51 | // Set a send pipeline's master volume element (see SetMasterVolume()). | ||
| 52 | 3 | static void SetPipelineMasterVolume(GstElement* pipeline, int volume) { | |
| 53 | 3 | GstElement* vol = gst_bin_get_by_name(GST_BIN(pipeline), "mastervol"); | |
| 54 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | if (vol) { |
| 55 | 3 | g_object_set(vol, "volume", masterVolumeGain(volume), nullptr); | |
| 56 | 3 | gst_object_unref(vol); | |
| 57 | } | ||
| 58 | 3 | } | |
| 59 | |||
| 60 | 2346 | OpusRTPManager::OpusRTPManager() { | |
| 61 |
2/4✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
|
2346 | m_configPath = getFPPMediaDir("/config/pipewire-opus-rtp-instances.json"); |
| 62 | 2346 | } | |
| 63 | |||
| 64 | 916 | OpusRTPManager::~OpusRTPManager() { | |
| 65 | 916 | Shutdown(); | |
| 66 | 916 | } | |
| 67 | |||
| 68 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 69 | // Lifecycle | ||
| 70 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 71 | 3761 | bool OpusRTPManager::Init() { | |
| 72 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 3761 times.
|
3761 | if (m_initialized.load()) { |
| 73 | ✗ | return true; | |
| 74 | } | ||
| 75 | |||
| 76 | // Opus RTP uses pipewiresrc/pipewiresink which require the full PipeWire graph. | ||
| 77 | // Only proceed when MediaBackend is "pipewire" (the advanced PipeWire mode). | ||
| 78 | // - ALSA / no backend: PipeWire daemon is not running; gst_parse_launch with | ||
| 79 | // pipewiresrc crashes in gst_value_deserialize. | ||
| 80 | // - pipewire-simple: the graph lacks the node connections needed for audio | ||
| 81 | // format negotiation, causing the state change to block indefinitely. | ||
| 82 |
2/4✓ Branch 0 taken 3761 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3761 times.
✗ Branch 3 not taken.
|
3761 | std::string mediaBackend = toLowerCopy(getSetting("MediaBackend")); |
| 83 |
8/10✓ Branch 0 taken 3761 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3749 times.
✓ Branch 3 taken 12 times.
✓ Branch 4 taken 3749 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3610 times.
✓ Branch 7 taken 139 times.
✓ Branch 8 taken 3622 times.
✓ Branch 9 taken 139 times.
|
3761 | if (!isPipeWireBackend() || mediaBackend != "pipewire") { |
| 84 |
1/2✓ Branch 0 taken 3622 times.
✗ Branch 1 not taken.
|
3622 | LogDebug(VB_MEDIAOUT, "OpusRTPManager: MediaBackend='%s' (need 'pipewire'), skipping init\n", |
| 85 | mediaBackend.c_str()); | ||
| 86 | 3622 | return true; | |
| 87 | } | ||
| 88 | |||
| 89 |
3/4✓ Branch 0 taken 139 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 126 times.
✓ Branch 3 taken 13 times.
|
139 | if (!FileExists(m_configPath)) { |
| 90 |
1/2✓ Branch 0 taken 126 times.
✗ Branch 1 not taken.
|
126 | LogDebug(VB_MEDIAOUT, "OpusRTPManager: No config file at %s, skipping init\n", |
| 91 | m_configPath.c_str()); | ||
| 92 | 126 | return true; | |
| 93 | } | ||
| 94 | |||
| 95 | // Shared with the playback path and the video managers so gst_init() | ||
| 96 | // happens exactly once, under one lock, whichever subsystem gets here | ||
| 97 | // first. | ||
| 98 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | GStreamerOutput::EnsureGStreamerInit(); |
| 99 | |||
| 100 | // Set PipeWire env vars | ||
| 101 | 13 | setenv("PIPEWIRE_RUNTIME_DIR", "/run/pipewire-fpp", 0); | |
| 102 | 13 | setenv("XDG_RUNTIME_DIR", "/run/pipewire-fpp", 0); | |
| 103 | 13 | setenv("PULSE_RUNTIME_PATH", "/run/pipewire-fpp/pulse", 0); | |
| 104 | |||
| 105 | 13 | m_initialized.store(true); | |
| 106 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | LogInfo(VB_MEDIAOUT, "OpusRTPManager: Initialized\n"); |
| 107 | 13 | return true; | |
| 108 | 3761 | } | |
| 109 | |||
| 110 | 2325 | void OpusRTPManager::Shutdown() { | |
| 111 |
2/2✓ Branch 0 taken 2312 times.
✓ Branch 1 taken 13 times.
|
2325 | if (!m_initialized.load()) { |
| 112 | 2312 | return; | |
| 113 | } | ||
| 114 | |||
| 115 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | LogInfo(VB_MEDIAOUT, "OpusRTPManager: Shutting down\n"); |
| 116 | |||
| 117 | // Nothing may apply from here on, including a watchdog rebuild that is | ||
| 118 | // already scheduled: ApplyConfig() would re-Init() and bring back the | ||
| 119 | // pipelines this is about to tear down. | ||
| 120 | 13 | m_stopping.store(true); | |
| 121 | 13 | m_applyGeneration++; | |
| 122 | 13 | m_initialized.store(false); | |
| 123 | |||
| 124 | // Join the rebuild thread BEFORE taking m_applyMutex: that thread calls | ||
| 125 | // ApplyConfig(), which itself takes the lock -- holding it across the | ||
| 126 | // join would deadlock. Taken out under m_rebuildMutex: the watchdog | ||
| 127 | // thread assigns it. | ||
| 128 | 13 | std::thread rebuild; | |
| 129 | { | ||
| 130 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | std::lock_guard<std::mutex> rl(m_rebuildMutex); |
| 131 | 13 | rebuild = std::move(m_rebuildThread); | |
| 132 | 13 | } | |
| 133 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 12 times.
|
13 | if (rebuild.joinable()) { |
| 134 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | rebuild.join(); |
| 135 | } | ||
| 136 | |||
| 137 | // Only now take the apply lock -- it blocks until any in-flight | ||
| 138 | // ApplyConfig() (e.g. from a command thread) has finished. | ||
| 139 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | std::lock_guard<std::mutex> applyLock(m_applyMutex); |
| 140 | |||
| 141 | 13 | m_watchdogRunning.store(false); | |
| 142 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 7 times.
|
13 | if (m_watchdogThread.joinable()) { |
| 143 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | m_watchdogThread.join(); |
| 144 | } | ||
| 145 | |||
| 146 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | StopAllPipelines(); |
| 147 | |||
| 148 | 13 | m_active.store(false); | |
| 149 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | LogInfo(VB_MEDIAOUT, "OpusRTPManager: Shutdown complete\n"); |
| 150 | 13 | } | |
| 151 | |||
| 152 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 153 | // Config loading | ||
| 154 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 155 | 584 | OpusRTPConfig OpusRTPManager::GetConfigSnapshot() { | |
| 156 |
1/2✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
|
584 | std::lock_guard<std::mutex> lock(m_configMutex); |
| 157 |
1/2✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
|
1168 | return m_config; |
| 158 | 584 | } | |
| 159 | |||
| 160 | |||
| 161 | // Values that go into a gst_parse_launch() description unquoted. Anything | ||
| 162 | // outside these characters is refused at load: a space or '!' in a | ||
| 163 | // destination address re-parses as extra pipeline elements (a udpsrc feeding | ||
| 164 | // a filesink is a root file write), and no real address or interface name | ||
| 165 | // needs them. | ||
| 166 | 96 | static bool IsPipelineSafeHost(const std::string& host) { | |
| 167 |
3/6✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 96 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 96 times.
|
96 | if (host.empty() || host.size() > 253) { |
| 168 | ✗ | return false; | |
| 169 | } | ||
| 170 |
2/2✓ Branch 0 taken 864 times.
✓ Branch 1 taken 96 times.
|
960 | for (unsigned char c : host) { |
| 171 |
3/8✓ Branch 0 taken 288 times.
✓ Branch 1 taken 576 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 288 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
864 | if (!std::isalnum(c) && c != '.' && c != '-' && c != ':') { |
| 172 | ✗ | return false; | |
| 173 | } | ||
| 174 | } | ||
| 175 | 96 | return true; | |
| 176 | } | ||
| 177 | 96 | static bool IsPipelineSafeInterface(const std::string& iface) { | |
| 178 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 96 times.
|
96 | if (iface.size() > 15) { // IFNAMSIZ - 1 |
| 179 | ✗ | return false; | |
| 180 | } | ||
| 181 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 96 times.
|
96 | for (unsigned char c : iface) { |
| 182 | ✗ | if (!std::isalnum(c) && c != '.' && c != '-' && c != '_' && c != '@') { | |
| 183 | ✗ | return false; | |
| 184 | } | ||
| 185 | } | ||
| 186 | 96 | return true; | |
| 187 | } | ||
| 188 | |||
| 189 | // The pipeline-start warnings. Kept as constants because RemoveWarning() | ||
| 190 | // matches the exact text: an apply where every stream of a kind started | ||
| 191 | // retracts that kind's warning, and an apply with nothing to run or Cleanup() | ||
| 192 | // retracts both -- before this, nothing ever did, and the banner outlived the | ||
| 193 | // fix until fppd restarted. | ||
| 194 | static const char* const OPUS_WARNING_SEND_FAILED = "Opus RTP: audio send stream failed to start"; | ||
| 195 | static const char* const OPUS_WARNING_RECV_FAILED = "Opus RTP: audio receive stream failed to start"; | ||
| 196 | // Raised when an apply can't read the config, so nothing runs; the next apply | ||
| 197 | // (any outcome) or Cleanup() retracts it. | ||
| 198 | static const char* const OPUS_WARNING_CONFIG_UNREADABLE = "Opus RTP: pipewire-opus-rtp-instances.json could not be read, so no stream is running"; | ||
| 199 | 73 | static void ClearOpusPipelineWarnings(bool send, bool recv) { | |
| 200 |
2/2✓ Branch 0 taken 45 times.
✓ Branch 1 taken 28 times.
|
73 | if (send) { |
| 201 |
3/6✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 45 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 45 times.
✗ Branch 5 not taken.
|
180 | WarningHolder::RemoveWarning(44, OPUS_WARNING_SEND_FAILED); |
| 202 | } | ||
| 203 |
2/2✓ Branch 0 taken 65 times.
✓ Branch 1 taken 8 times.
|
73 | if (recv) { |
| 204 |
3/6✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 65 times.
✗ Branch 5 not taken.
|
260 | WarningHolder::RemoveWarning(44, OPUS_WARNING_RECV_FAILED); |
| 205 | } | ||
| 206 | 73 | } | |
| 207 | |||
| 208 | 71 | bool OpusRTPManager::LoadConfig() { | |
| 209 |
1/2✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
|
71 | Json::Value root; |
| 210 |
3/4✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 69 times.
|
71 | if (!LoadJsonFromFile(m_configPath, root, JsonRoot::Object)) { |
| 211 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | LogWarn(VB_MEDIAOUT, "OpusRTPManager: Failed to load config from %s\n", |
| 212 | m_configPath.c_str()); | ||
| 213 | 2 | return false; | |
| 214 | } | ||
| 215 | |||
| 216 | // Parse into a local config and publish it in one locked swap at the end. | ||
| 217 | // Filling m_config in place would let a status query on another thread | ||
| 218 | // iterate the vector while push_back() reallocates it. | ||
| 219 | 69 | OpusRTPConfig cfg; | |
| 220 |
3/4✓ Branch 0 taken 12 times.
✓ Branch 1 taken 57 times.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
|
69 | static const OpusRTPConfig kDefault; |
| 221 | // Values of the wrong JSON type threw from here to std::terminate; such | ||
| 222 | // a field is left at its default, such an instance skipped. | ||
| 223 | try { | ||
| 224 | 69 | cfg.requireGroupSource = | |
| 225 |
3/6✓ Branch 0 taken 69 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 69 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 69 times.
✗ Branch 5 not taken.
|
69 | root.get("requireGroupSource", kDefault.requireGroupSource).asBool(); |
| 226 | ✗ | } catch (const std::exception& e) { | |
| 227 | ✗ | WarningHolder::InvalidConfigEntry("pipewire-opus-rtp-instances.json", e.what()); | |
| 228 | ✗ | } | |
| 229 | |||
| 230 |
6/12✓ Branch 0 taken 69 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 69 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 69 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 69 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 69 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 69 times.
✗ Branch 11 not taken.
|
69 | if (root.isMember("instances") && root["instances"].isArray()) { |
| 231 |
8/14✓ Branch 0 taken 69 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 69 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 69 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 97 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 97 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 166 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 97 times.
✓ Branch 13 taken 69 times.
|
166 | for (const auto& instJson : root["instances"]) { |
| 232 |
1/2✓ Branch 0 taken 97 times.
✗ Branch 1 not taken.
|
97 | OpusRTPInstance inst; |
| 233 | try { | ||
| 234 |
4/6✓ Branch 0 taken 97 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 97 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96 times.
✓ Branch 5 taken 1 times.
|
99 | inst.id = instJson.get("id", 0).asInt(); |
| 235 |
3/6✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96 times.
✗ Branch 5 not taken.
|
96 | inst.name = instJson.get("name", "Opus RTP").asString(); |
| 236 |
3/6✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96 times.
✗ Branch 5 not taken.
|
96 | inst.enabled = instJson.get("enabled", true).asBool(); |
| 237 |
3/6✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96 times.
✗ Branch 5 not taken.
|
96 | inst.mode = instJson.get("mode", "send").asString(); |
| 238 |
3/6✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96 times.
✗ Branch 5 not taken.
|
96 | inst.destIP = instJson.get("destIP", OpusRTP::DEFAULT_DEST_IP).asString(); |
| 239 |
3/6✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96 times.
✗ Branch 5 not taken.
|
96 | inst.port = instJson.get("port", OpusRTP::DEFAULT_PORT).asInt(); |
| 240 |
3/6✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96 times.
✗ Branch 5 not taken.
|
96 | inst.channels = instJson.get("channels", OpusRTP::DEFAULT_CHANNELS).asInt(); |
| 241 |
3/6✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96 times.
✗ Branch 5 not taken.
|
96 | inst.interface = instJson.get("interface", "").asString(); |
| 242 |
3/6✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96 times.
✗ Branch 5 not taken.
|
96 | inst.latency = instJson.get("latency", OpusRTP::DEFAULT_LATENCY_MS).asInt(); |
| 243 |
3/6✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96 times.
✗ Branch 5 not taken.
|
96 | inst.bitrate = instJson.get("bitrate", OpusRTP::DEFAULT_BITRATE).asInt(); |
| 244 |
3/6✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96 times.
✗ Branch 5 not taken.
|
96 | inst.dtx = instJson.get("dtx", false).asBool(); |
| 245 |
3/6✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96 times.
✗ Branch 5 not taken.
|
96 | inst.fec = instJson.get("fec", true).asBool(); |
| 246 |
3/6✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 96 times.
✗ Branch 5 not taken.
|
96 | inst.packetLoss = instJson.get("packetLoss", OpusRTP::DEFAULT_PACKET_LOSS).asInt(); |
| 247 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | } catch (const std::exception& e) { |
| 248 |
3/6✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
|
3 | WarningHolder::InvalidConfigEntry("pipewire-opus-rtp-instances.json", e.what()); |
| 249 | 1 | continue; | |
| 250 | 1 | } | |
| 251 | |||
| 252 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 96 times.
|
96 | if (!OpusRTP::IsValidBitrate(inst.bitrate)) { |
| 253 | ✗ | inst.bitrate = OpusRTP::DEFAULT_BITRATE; | |
| 254 | } | ||
| 255 | |||
| 256 |
3/6✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 96 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 96 times.
|
96 | if (!IsPipelineSafeHost(inst.destIP) || !IsPipelineSafeInterface(inst.interface)) { |
| 257 | ✗ | LogErr(VB_MEDIAOUT, "OpusRTPManager: instance '%s' has an invalid address or interface, skipping it\n", | |
| 258 | inst.name.c_str()); | ||
| 259 | ✗ | WarningHolder::AddWarningTimeout(300, 44, "Opus RTP: stream '" + inst.name + | |
| 260 | ✗ | "' has an invalid address or interface and was not started"); | |
| 261 | ✗ | continue; | |
| 262 | } | ||
| 263 | |||
| 264 |
1/2✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
|
96 | cfg.instances.push_back(inst); |
| 265 |
2/2✓ Branch 0 taken 96 times.
✓ Branch 1 taken 1 times.
|
97 | } |
| 266 | } | ||
| 267 | |||
| 268 |
1/2✓ Branch 0 taken 69 times.
✗ Branch 1 not taken.
|
69 | LogInfo(VB_MEDIAOUT, "OpusRTPManager: Loaded config with %d instances\n", |
| 269 | (int)cfg.instances.size()); | ||
| 270 | |||
| 271 | { | ||
| 272 |
1/2✓ Branch 0 taken 69 times.
✗ Branch 1 not taken.
|
69 | std::lock_guard<std::mutex> lock(m_configMutex); |
| 273 | 69 | m_config = std::move(cfg); | |
| 274 | 69 | } | |
| 275 | 69 | return true; | |
| 276 | 71 | } | |
| 277 | |||
| 278 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 279 | // ApplyConfig | ||
| 280 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 281 | 1945 | bool OpusRTPManager::ApplyConfig() { | |
| 282 | // Serialize against concurrent ApplyConfig()/Shutdown()/Cleanup() calls - | ||
| 283 | // see m_applyMutex. Without this, two callers can both get past the | ||
| 284 | // watchdog join below and both reach the std::thread assignment at the | ||
| 285 | // end, where assigning over a joinable thread calls std::terminate(). | ||
| 286 |
1/2✓ Branch 0 taken 1945 times.
✗ Branch 1 not taken.
|
1945 | std::lock_guard<std::mutex> applyLock(m_applyMutex); |
| 287 |
1/2✓ Branch 0 taken 1945 times.
✗ Branch 1 not taken.
|
3890 | return ApplyConfigLocked(); |
| 288 | 1945 | } | |
| 289 | |||
| 290 | 1946 | bool OpusRTPManager::ApplyConfigLocked() { | |
| 291 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1946 times.
|
1946 | if (m_stopping.load()) { |
| 292 | ✗ | return false; | |
| 293 | } | ||
| 294 | // Any rebuild scheduled before this apply is now redundant. | ||
| 295 | 1946 | m_applyGeneration++; | |
| 296 | |||
| 297 |
2/2✓ Branch 0 taken 1874 times.
✓ Branch 1 taken 72 times.
|
1946 | if (!m_initialized.load()) { |
| 298 |
2/4✓ Branch 0 taken 1874 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1874 times.
|
1874 | if (!Init()) { |
| 299 | ✗ | return false; | |
| 300 | } | ||
| 301 | // Init() may return true but skip initialization (e.g. wrong backend). | ||
| 302 | // If m_initialized is still false, there's nothing to do. | ||
| 303 |
1/2✓ Branch 0 taken 1874 times.
✗ Branch 1 not taken.
|
1874 | if (!m_initialized.load()) { |
| 304 | 1874 | return true; | |
| 305 | } | ||
| 306 | } | ||
| 307 | |||
| 308 | // Stop existing watchdog and pipelines | ||
| 309 | 72 | m_watchdogRunning.store(false); | |
| 310 | // Guard against ApplyConfig() being re-entered from its own watchdog | ||
| 311 | // thread (see WatchdogLoop()) -- joining the calling thread would | ||
| 312 | // throw std::system_error ("Resource deadlock avoided"), which | ||
| 313 | // propagates out of the thread function and crashes fppd via | ||
| 314 | // std::terminate(). The watchdog's rebuild path already stops the | ||
| 315 | // watchdog loop and hands off to a short-lived helper thread before | ||
| 316 | // calling ApplyConfig(), but keep this check as a safety net for any | ||
| 317 | // other re-entrant caller. | ||
| 318 |
4/4✓ Branch 0 taken 58 times.
✓ Branch 1 taken 14 times.
✓ Branch 2 taken 58 times.
✓ Branch 3 taken 14 times.
|
130 | if (m_watchdogThread.joinable() && |
| 319 |
1/2✓ Branch 0 taken 58 times.
✗ Branch 1 not taken.
|
58 | m_watchdogThread.get_id() != std::this_thread::get_id()) { |
| 320 |
1/2✓ Branch 0 taken 58 times.
✗ Branch 1 not taken.
|
58 | m_watchdogThread.join(); |
| 321 | } | ||
| 322 |
1/2✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
|
72 | StopAllPipelines(); |
| 323 |
3/6✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 72 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 72 times.
✗ Branch 5 not taken.
|
216 | WarningHolder::RemoveWarning(44, OPUS_WARNING_CONFIG_UNREADABLE); |
| 324 | |||
| 325 |
3/4✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 71 times.
|
72 | if (!FileExists(m_configPath)) { |
| 326 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | LogInfo(VB_MEDIAOUT, "OpusRTPManager: No config file, nothing to apply\n"); |
| 327 | // No stream is meant to run, so none is failing to start. | ||
| 328 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | ClearOpusPipelineWarnings(true, true); |
| 329 | 1 | m_active.store(false); | |
| 330 | 1 | return true; | |
| 331 | } | ||
| 332 | |||
| 333 |
3/4✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 69 times.
|
71 | if (!LoadConfig()) { |
| 334 | // The old pipelines are stopped and none replaces them, so nothing | ||
| 335 | // is failing to start: it is the config that is wrong, and that is | ||
| 336 | // what the banner says instead. | ||
| 337 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | ClearOpusPipelineWarnings(true, true); |
| 338 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
6 | WarningHolder::AddWarning(44, OPUS_WARNING_CONFIG_UNREADABLE); |
| 339 | 2 | m_active.store(false); | |
| 340 | 2 | return false; | |
| 341 | } | ||
| 342 | |||
| 343 | 69 | int enabledCount = 0; | |
| 344 |
2/2✓ Branch 0 taken 96 times.
✓ Branch 1 taken 69 times.
|
165 | for (const auto& inst : m_config.instances) { |
| 345 |
2/2✓ Branch 0 taken 94 times.
✓ Branch 1 taken 2 times.
|
96 | if (inst.enabled) enabledCount++; |
| 346 | } | ||
| 347 | |||
| 348 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 65 times.
|
69 | if (enabledCount == 0) { |
| 349 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | LogInfo(VB_MEDIAOUT, "OpusRTPManager: No enabled instances\n"); |
| 350 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | ClearOpusPipelineWarnings(true, true); |
| 351 | 4 | m_active.store(false); | |
| 352 | 4 | return true; | |
| 353 | } | ||
| 354 | |||
| 355 | // Create pipelines for each enabled instance | ||
| 356 | // | ||
| 357 | // Senders held idle because nothing feeds them -- published to | ||
| 358 | // m_deferredSenders below, once the whole pass has run. | ||
| 359 | 65 | std::map<int, std::string> deferred; | |
| 360 | 65 | bool sendFailed = false; | |
| 361 | 65 | bool recvFailed = false; | |
| 362 | 65 | int failedStarts = 0; | |
| 363 | |||
| 364 |
2/2✓ Branch 0 taken 94 times.
✓ Branch 1 taken 65 times.
|
159 | for (const auto& inst : m_config.instances) { |
| 365 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 94 times.
|
94 | if (!inst.enabled) continue; |
| 366 | |||
| 367 |
5/8✓ Branch 0 taken 94 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✓ Branch 3 taken 53 times.
✓ Branch 4 taken 41 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 41 times.
|
94 | bool wantSend = (inst.mode == "send" || inst.mode == "both"); |
| 368 |
5/8✓ Branch 0 taken 94 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✓ Branch 3 taken 41 times.
✓ Branch 4 taken 53 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 53 times.
|
94 | bool wantRecv = (inst.mode == "receive" || inst.mode == "both"); |
| 369 | |||
| 370 |
2/2✓ Branch 0 taken 53 times.
✓ Branch 1 taken 41 times.
|
94 | if (wantSend) { |
| 371 | // Nothing feeds this sender, so starting it would cost 30 seconds | ||
| 372 | // of blocked apply and end in FAILURE. Hold it instead -- see | ||
| 373 | // OpusRTPConfig::requireGroupSource. | ||
| 374 |
2/4✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✗ Branch 3 not taken.
|
53 | const std::string nodeName = SafeNodeName(inst.name) + "_send"; |
| 375 |
7/8✓ Branch 0 taken 44 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 44 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 43 times.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 52 times.
|
53 | if (m_config.requireGroupSource && !PipeWireGraphFeedsNode(nodeName)) { |
| 376 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | LogInfo(VB_MEDIAOUT, |
| 377 | "Opus RTP send [%d] '%s': nothing in the audio graph feeds %s, " | ||
| 378 | "holding the stream idle. Add it to an Audio Output Group " | ||
| 379 | "and apply that config to start it.\n", | ||
| 380 | inst.id, inst.name.c_str(), nodeName.c_str()); | ||
| 381 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | deferred[inst.id] = |
| 382 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | "Waiting for audio — not a member of any enabled Audio Output Group"; |
| 383 |
3/4✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 49 times.
✓ Branch 3 taken 3 times.
|
52 | } else if (!CreateSendPipeline(inst)) { |
| 384 | 49 | sendFailed = true; | |
| 385 | 49 | failedStarts++; | |
| 386 | } | ||
| 387 | 53 | } | |
| 388 | |||
| 389 |
7/8✓ Branch 0 taken 41 times.
✓ Branch 1 taken 53 times.
✓ Branch 2 taken 41 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✓ Branch 5 taken 33 times.
✓ Branch 6 taken 8 times.
✓ Branch 7 taken 86 times.
|
94 | if (wantRecv && !CreateRecvPipeline(inst)) { |
| 390 | 8 | recvFailed = true; | |
| 391 | 8 | failedStarts++; | |
| 392 | } | ||
| 393 | } | ||
| 394 |
1/2✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
|
65 | ClearOpusPipelineWarnings(!sendFailed, !recvFailed); |
| 395 | |||
| 396 | { | ||
| 397 |
1/2✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
|
65 | std::lock_guard<std::mutex> lock(m_pipelineMutex); |
| 398 | 65 | m_deferredSenders = std::move(deferred); | |
| 399 | 65 | m_failedStarts = failedStarts; | |
| 400 | 65 | } | |
| 401 | |||
| 402 | // Start watchdog thread | ||
| 403 | 65 | m_watchdogRunning.store(true); | |
| 404 |
1/2✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
|
65 | m_watchdogThread = std::thread(&OpusRTPManager::WatchdogLoop, this); |
| 405 | |||
| 406 | 65 | m_active.store(true); | |
| 407 |
1/2✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
|
65 | LogInfo(VB_MEDIAOUT, "OpusRTPManager: Applied config -- %d send, %d receive pipelines\n", |
| 408 | (int)m_sendPipelines.size(), (int)m_recvPipelines.size()); | ||
| 409 | 65 | return true; | |
| 410 | 65 | } | |
| 411 | |||
| 412 | 1 | void OpusRTPManager::ScheduleRebuild() { | |
| 413 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | std::lock_guard<std::mutex> rl(m_rebuildMutex); |
| 414 |
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 (m_stopping.load() || m_rebuildRunning.load()) { |
| 415 | // Shutting down, or a rebuild is already on its way. Never join a | ||
| 416 | // running one here: its ApplyConfig() may be waiting to join the | ||
| 417 | // watchdog thread that is calling this. | ||
| 418 | ✗ | return; | |
| 419 | } | ||
| 420 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (m_rebuildThread.joinable()) { |
| 421 | ✗ | m_rebuildThread.join(); // finished: returns at once | |
| 422 | } | ||
| 423 | 1 | uint64_t generation = m_applyGeneration.load(); | |
| 424 | 1 | m_rebuildRunning.store(true); | |
| 425 | 1 | m_rebuildThread = std::thread([this, generation]() { | |
| 426 | // Brief delay to let the watchdog thread finish exiting and become | ||
| 427 | // joinable. | ||
| 428 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | std::this_thread::sleep_for(std::chrono::milliseconds(500)); |
| 429 | { | ||
| 430 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | std::lock_guard<std::mutex> applyLock(m_applyMutex); |
| 431 | // Nothing to do if anything applied, cleaned up or shut down in | ||
| 432 | // the meantime -- it either did this rebuild's work or undid it | ||
| 433 | // on purpose. | ||
| 434 |
3/6✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
|
2 | if (!m_stopping.load() && generation == m_applyGeneration.load()) { |
| 435 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | ApplyConfigLocked(); |
| 436 | } | ||
| 437 | 1 | } | |
| 438 | 1 | m_rebuildRunning.store(false); | |
| 439 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
2 | }); |
| 440 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | } |
| 441 | |||
| 442 | 1 | void OpusRTPManager::Cleanup() { | |
| 443 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | LogInfo(VB_MEDIAOUT, "OpusRTPManager: Cleanup\n"); |
| 444 | |||
| 445 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | std::lock_guard<std::mutex> applyLock(m_applyMutex); |
| 446 | // Nothing runs after this, so no stream is failing to start. | ||
| 447 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | ClearOpusPipelineWarnings(true, true); |
| 448 |
3/6✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
|
3 | WarningHolder::RemoveWarning(44, OPUS_WARNING_CONFIG_UNREADABLE); |
| 449 | // A watchdog rebuild already scheduled must not bring back what this | ||
| 450 | // tears down. | ||
| 451 | 1 | m_applyGeneration++; | |
| 452 | |||
| 453 | 1 | m_watchdogRunning.store(false); | |
| 454 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | if (m_watchdogThread.joinable()) { |
| 455 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | m_watchdogThread.join(); |
| 456 | } | ||
| 457 | |||
| 458 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | StopAllPipelines(); |
| 459 | 1 | m_active.store(false); | |
| 460 | 1 | } | |
| 461 | |||
| 462 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 463 | // Helpers | ||
| 464 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 465 | 93 | bool OpusRTPManager::IsMulticast(const std::string& ip) { | |
| 466 | struct in_addr addr; | ||
| 467 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 93 times.
|
93 | if (inet_pton(AF_INET, ip.c_str(), &addr) != 1) { |
| 468 | ✗ | return false; | |
| 469 | } | ||
| 470 | // Multicast range: 224.0.0.0 - 239.255.255.255 | ||
| 471 | 93 | uint32_t hostAddr = ntohl(addr.s_addr); | |
| 472 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 93 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
93 | return (hostAddr >= 0xE0000000 && hostAddr <= 0xEFFFFFFF); |
| 473 | } | ||
| 474 | |||
| 475 | 187 | std::string OpusRTPManager::SafeNodeName(const std::string& name) { | |
| 476 |
1/2✓ Branch 0 taken 187 times.
✗ Branch 1 not taken.
|
187 | std::string result = "opusrtp_"; |
| 477 |
2/2✓ Branch 0 taken 378 times.
✓ Branch 1 taken 187 times.
|
565 | for (char c : name) { |
| 478 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 378 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
378 | if (std::isalnum(c) || c == '_') { |
| 479 |
1/2✓ Branch 0 taken 378 times.
✗ Branch 1 not taken.
|
378 | result += std::tolower(c); |
| 480 | } else { | ||
| 481 | ✗ | result += '_'; | |
| 482 | } | ||
| 483 | } | ||
| 484 | 187 | return result; | |
| 485 | ✗ | } | |
| 486 | |||
| 487 | ✗ | std::string OpusRTPManager::GetInterfaceIP(const std::string& iface) { | |
| 488 | ✗ | struct ifaddrs* addrs = nullptr; | |
| 489 | ✗ | std::string result = "0.0.0.0"; | |
| 490 | |||
| 491 | ✗ | if (getifaddrs(&addrs) != 0) { | |
| 492 | ✗ | return result; | |
| 493 | } | ||
| 494 | |||
| 495 | ✗ | for (struct ifaddrs* ifa = addrs; ifa; ifa = ifa->ifa_next) { | |
| 496 | ✗ | if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET) continue; | |
| 497 | ✗ | if (iface.empty() || iface == ifa->ifa_name) { | |
| 498 | ✗ | if (std::string(ifa->ifa_name) == "lo") continue; | |
| 499 | ✗ | struct sockaddr_in* addr = (struct sockaddr_in*)ifa->ifa_addr; | |
| 500 | char buf[INET_ADDRSTRLEN]; | ||
| 501 | ✗ | inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)); | |
| 502 | ✗ | result = buf; | |
| 503 | ✗ | if (!iface.empty()) break; | |
| 504 | } | ||
| 505 | } | ||
| 506 | |||
| 507 | ✗ | freeifaddrs(addrs); | |
| 508 | ✗ | return result; | |
| 509 | ✗ | } | |
| 510 | |||
| 511 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 512 | // Pipeline creation -- Send | ||
| 513 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 514 | 52 | bool OpusRTPManager::CreateSendPipeline(const OpusRTPInstance& inst) { | |
| 515 |
2/4✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
|
52 | std::string nodeName = SafeNodeName(inst.name) + "_send"; |
| 516 |
1/2✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
|
52 | bool multicast = IsMulticast(inst.destIP); |
| 517 | |||
| 518 | // Pipeline: | ||
| 519 | // pipewiresrc stream-properties="props,node.name=<node>,node.autoconnect=false" | ||
| 520 | // ! queue ! audioconvert ! audioresample ! audioconvert ! audiorate | ||
| 521 | // ! audio/x-raw,rate=48000,channels=N | ||
| 522 | // ! opusenc bitrate=<bps> dtx=<bool> inband-fec=<bool> packet-loss-percentage=<int> | ||
| 523 | // ! rtpopuspay pt=96 | ||
| 524 | // ! udpsink host=<ip> port=<port> qos-dscp=34 [multicast options] | ||
| 525 | // | ||
| 526 | // The resampler is not optional, and audiorate is not a substitute for it: | ||
| 527 | // audiorate only inserts/drops samples to patch timestamp gaps, it cannot | ||
| 528 | // change the rate ("audiorate0 can't handle caps rate=48000"). Opus needs | ||
| 529 | // 48 kHz, and the PipeWire graph does not necessarily run there -- it | ||
| 530 | // follows the output card and sits at 44100 by default -- so without a | ||
| 531 | // resampler this chain has nothing in it that can bridge the two. See the | ||
| 532 | // matching note in AES67Manager::CreateSendPipeline(): the failure mode | ||
| 533 | // when the rates do not line up is that negotiation never completes and | ||
| 534 | // set_state() blocks, not a clean error. The split around audioresample | ||
| 535 | // mirrors what was measured to work there; it costs nothing when the graph | ||
| 536 | // already runs at 48000, where the resampler passes through. | ||
| 537 | |||
| 538 |
1/2✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
|
52 | std::ostringstream oss; |
| 539 | oss << "pipewiresrc name=pwsrc min-buffers=2 do-timestamp=true " | ||
| 540 | << "! queue max-size-buffers=0 max-size-bytes=0 max-size-time=200000000 leaky=downstream " | ||
| 541 | << "! audioconvert " | ||
| 542 | << "! audioresample " | ||
| 543 | << "! audioconvert " | ||
| 544 | // The master volume (see SetMasterVolume()). | ||
| 545 |
9/18✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 52 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 52 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 52 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 52 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 52 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 52 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 52 times.
✗ Branch 17 not taken.
|
52 | << "! volume name=mastervol volume=" << masterVolumeGain(getVolume()) << " " |
| 546 | << "! audiorate " | ||
| 547 |
4/8✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 52 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 52 times.
✗ Branch 7 not taken.
|
52 | << "! audio/x-raw,rate=" << OpusRTP::AUDIO_RATE |
| 548 |
2/4✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
|
52 | << ",channels=" << inst.channels << " " |
| 549 |
3/6✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 52 times.
✗ Branch 5 not taken.
|
52 | << "! opusenc bitrate=" << inst.bitrate |
| 550 | << " frame-size=20" | ||
| 551 | 52 | << " dtx=" << (inst.dtx ? "true" : "false") | |
| 552 | 52 | << " inband-fec=" << (inst.fec ? "true" : "false") | |
| 553 |
9/18✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 52 times.
✓ Branch 6 taken 52 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 52 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 52 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 52 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 52 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 52 times.
✗ Branch 17 not taken.
|
52 | << " packet-loss-percentage=" << inst.packetLoss << " " |
| 554 |
3/6✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 52 times.
✗ Branch 5 not taken.
|
52 | << "! rtpopuspay pt=" << OpusRTP::RTP_PAYLOAD_TYPE << " " |
| 555 | 52 | << "! udpsink name=usink host=" << inst.destIP | |
| 556 |
5/10✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 52 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 52 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 52 times.
✗ Branch 9 not taken.
|
52 | << " port=" << inst.port |
| 557 |
2/4✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
|
52 | << " qos-dscp=" << OpusRTP::AUDIO_DSCP |
| 558 |
1/2✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
|
52 | << " sync=false"; |
| 559 | |||
| 560 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 52 times.
|
52 | if (multicast) { |
| 561 | ✗ | oss << " ttl-mc=" << OpusRTP::MULTICAST_TTL | |
| 562 | ✗ | << " auto-multicast=true"; | |
| 563 | ✗ | if (!inst.interface.empty()) { | |
| 564 | ✗ | oss << " multicast-iface=" << inst.interface; | |
| 565 | } | ||
| 566 | } | ||
| 567 | |||
| 568 |
1/2✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
|
52 | std::string pipelineStr = oss.str(); |
| 569 |
1/2✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
|
52 | LogInfo(VB_MEDIAOUT, "Opus RTP send pipeline [%d] %s: %s\n", |
| 570 | inst.id, inst.name.c_str(), pipelineStr.c_str()); | ||
| 571 | |||
| 572 | 52 | GError* error = nullptr; | |
| 573 |
1/2✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
|
52 | GstElement* pipeline = gst_parse_launch(pipelineStr.c_str(), &error); |
| 574 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 52 times.
|
52 | if (error) { |
| 575 | ✗ | LogErr(VB_MEDIAOUT, "Opus RTP send pipeline error [%d]: %s\n", | |
| 576 | inst.id, error->message); | ||
| 577 | ✗ | g_error_free(error); | |
| 578 | // gst_parse_launch() can return a non-null (partial) pipeline even | ||
| 579 | // when it also reports an error -- release it so we don't leak. | ||
| 580 | ✗ | if (pipeline) { | |
| 581 | ✗ | gst_object_unref(pipeline); | |
| 582 | } | ||
| 583 | ✗ | return false; | |
| 584 | } | ||
| 585 | |||
| 586 | // Set stream-properties post-launch — inline GstStructure values | ||
| 587 | // containing '/' (e.g. node.latency=960/48000) crash gst_value_deserialize. | ||
| 588 |
3/6✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 52 times.
✗ Branch 5 not taken.
|
52 | GstElement* pwsrc = gst_bin_get_by_name(GST_BIN(pipeline), "pwsrc"); |
| 589 |
1/2✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
|
52 | if (pwsrc) { |
| 590 |
1/2✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
|
52 | GstStructure* props = gst_structure_new("props", |
| 591 | "node.name", G_TYPE_STRING, nodeName.c_str(), | ||
| 592 | "node.autoconnect", G_TYPE_BOOLEAN, FALSE, | ||
| 593 | "node.latency", G_TYPE_STRING, "960/48000", | ||
| 594 | NULL); | ||
| 595 |
1/2✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
|
52 | g_object_set(pwsrc, "stream-properties", props, NULL); |
| 596 |
1/2✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
|
52 | gst_structure_free(props); |
| 597 |
1/2✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
|
52 | gst_object_unref(pwsrc); |
| 598 | } | ||
| 599 | |||
| 600 | // On the system clock, not pipewiresrc's GstPipeWireClock, which stops | ||
| 601 | // advancing for a follower stream once the graph's real driver takes it | ||
| 602 | // over; pipewiresrc waits on it before every buffer, and the sender went | ||
| 603 | // quiet after its first packets. Same as RTSPOutputManager's audio bridge. | ||
| 604 |
1/2✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
|
52 | GstClock* sysClock = gst_system_clock_obtain(); |
| 605 |
3/6✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 52 times.
✗ Branch 5 not taken.
|
52 | gst_pipeline_use_clock(GST_PIPELINE(pipeline), sysClock); |
| 606 |
1/2✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
|
52 | gst_object_unref(sysClock); |
| 607 | |||
| 608 |
3/6✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 52 times.
✗ Branch 5 not taken.
|
52 | GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline)); |
| 609 | |||
| 610 |
1/2✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
|
52 | GstStateChangeReturn ret = gst_element_set_state(pipeline, GST_STATE_PLAYING); |
| 611 |
2/2✓ Branch 0 taken 49 times.
✓ Branch 1 taken 3 times.
|
52 | if (ret == GST_STATE_CHANGE_FAILURE) { |
| 612 |
1/2✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
|
49 | LogErr(VB_MEDIAOUT, "Opus RTP send pipeline [%d] failed to start\n", inst.id); |
| 613 |
2/4✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 49 times.
✗ Branch 3 not taken.
|
147 | WarningHolder::AddWarning(44, OPUS_WARNING_SEND_FAILED); |
| 614 | // Drop back to NULL before unreffing -- elements may already hold | ||
| 615 | // READY/PAUSED resources (sockets, threads, PipeWire connections) | ||
| 616 | // that gst_object_unref() alone will not release. | ||
| 617 |
1/2✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
|
49 | gst_element_set_state(pipeline, GST_STATE_NULL); |
| 618 |
1/2✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
|
49 | gst_object_unref(bus); |
| 619 |
1/2✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
|
49 | gst_object_unref(pipeline); |
| 620 | 49 | return false; | |
| 621 | } | ||
| 622 | |||
| 623 | { | ||
| 624 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | std::unique_lock<std::mutex> lock(m_pipelineMutex); |
| 625 | // If a pipeline already exists for this instance id (e.g. two | ||
| 626 | // ApplyConfig() runs interleaved), stop it properly instead of | ||
| 627 | // erasing the map entry directly -- a raw erase() would destroy | ||
| 628 | // the OpusRTPPipeline (dropping its GstElement* refs) without ever | ||
| 629 | // calling gst_element_set_state(NULL), leaking a still-PLAYING | ||
| 630 | // pipeline (refs, threads, sockets). | ||
| 631 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | auto node = m_sendPipelines.extract(inst.id); |
| 632 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
|
3 | if (!node.empty()) { |
| 633 | // StopPipeline() can block on a GStreamer state change to | ||
| 634 | // PipeWire -- release the lock while it runs, matching the | ||
| 635 | // no-block-while-locked policy used elsewhere in this file | ||
| 636 | // (see StopAllPipelines()). | ||
| 637 | ✗ | lock.unlock(); | |
| 638 | ✗ | StopPipeline(node.mapped()); | |
| 639 | ✗ | lock.lock(); | |
| 640 | } | ||
| 641 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | auto [it, ok] = m_sendPipelines.try_emplace(inst.id); |
| 642 | 3 | it->second.instanceId = inst.id; | |
| 643 | 3 | it->second.isSend = true; | |
| 644 | 3 | it->second.pipeline = pipeline; | |
| 645 | 3 | it->second.bus = bus; | |
| 646 | 3 | it->second.running = true; | |
| 647 | // The gain in the description is the one when it was built; a change | ||
| 648 | // since then went to the pipelines already in this map, not this one. | ||
| 649 |
2/4✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
|
3 | SetPipelineMasterVolume(pipeline, getVolume()); |
| 650 | 3 | } | |
| 651 | |||
| 652 |
2/4✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
|
3 | LogInfo(VB_MEDIAOUT, "Opus RTP send pipeline [%d] %s started -> %s:%d (%dch, %d kbps%s)\n", |
| 653 | inst.id, inst.name.c_str(), inst.destIP.c_str(), inst.port, | ||
| 654 | inst.channels, inst.bitrate / 1000, | ||
| 655 | multicast ? ", multicast" : ", unicast"); | ||
| 656 | 3 | return true; | |
| 657 | 52 | } | |
| 658 | |||
| 659 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 660 | // Pipeline creation -- Receive | ||
| 661 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 662 | 41 | bool OpusRTPManager::CreateRecvPipeline(const OpusRTPInstance& inst) { | |
| 663 |
2/4✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✗ Branch 3 not taken.
|
41 | std::string nodeName = SafeNodeName(inst.name) + "_recv"; |
| 664 |
1/2✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
|
41 | bool multicast = IsMulticast(inst.destIP); |
| 665 | |||
| 666 | // Pipeline: | ||
| 667 | // udpsrc [multicast-group=<ip>] port=<port> [auto-multicast=true] [multicast-iface=<iface>] | ||
| 668 | // ! application/x-rtp,media=audio,encoding-name=OPUS,payload=96 | ||
| 669 | // ! rtpjitterbuffer latency=<ms> | ||
| 670 | // ! rtpopusdepay | ||
| 671 | // ! opusdec | ||
| 672 | // ! audioconvert | ||
| 673 | // ! pipewiresink stream-properties="props,media.class=Audio/Source,node.name=<node>" | ||
| 674 | |||
| 675 |
1/2✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
|
41 | std::ostringstream oss; |
| 676 |
1/2✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
|
41 | oss << "udpsrc"; |
| 677 | |||
| 678 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 41 times.
|
41 | if (multicast) { |
| 679 | ✗ | oss << " multicast-group=" << inst.destIP; | |
| 680 | } | ||
| 681 | |||
| 682 |
2/4✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✗ Branch 3 not taken.
|
41 | oss << " port=" << inst.port; |
| 683 | |||
| 684 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 41 times.
|
41 | if (multicast) { |
| 685 | ✗ | oss << " auto-multicast=true"; | |
| 686 | ✗ | if (!inst.interface.empty()) { | |
| 687 | ✗ | oss << " multicast-iface=" << inst.interface; | |
| 688 | } | ||
| 689 | } | ||
| 690 | |||
| 691 | oss << " ! application/x-rtp,media=audio,encoding-name=OPUS" | ||
| 692 |
3/6✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 41 times.
✗ Branch 5 not taken.
|
41 | << ",payload=" << OpusRTP::RTP_PAYLOAD_TYPE << " " |
| 693 |
3/6✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 41 times.
✗ Branch 5 not taken.
|
41 | << "! rtpjitterbuffer latency=" << inst.latency << " " |
| 694 | << "! rtpopusdepay " | ||
| 695 | << "! opusdec plc=true " | ||
| 696 | << "! audioconvert " | ||
| 697 |
6/12✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 41 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 41 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 41 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 41 times.
✗ Branch 11 not taken.
|
41 | << "! audio/x-raw,rate=" << OpusRTP::AUDIO_RATE << ",format=F32LE " |
| 698 |
2/4✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✗ Branch 3 not taken.
|
41 | << "! pipewiresink name=pwsink"; |
| 699 | |||
| 700 |
1/2✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
|
41 | std::string pipelineStr = oss.str(); |
| 701 |
1/2✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
|
41 | LogInfo(VB_MEDIAOUT, "Opus RTP recv pipeline [%d] %s: %s\n", |
| 702 | inst.id, inst.name.c_str(), pipelineStr.c_str()); | ||
| 703 | |||
| 704 | 41 | GError* error = nullptr; | |
| 705 |
1/2✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
|
41 | GstElement* pipeline = gst_parse_launch(pipelineStr.c_str(), &error); |
| 706 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 41 times.
|
41 | if (error) { |
| 707 | ✗ | LogErr(VB_MEDIAOUT, "Opus RTP recv pipeline error [%d]: %s\n", | |
| 708 | inst.id, error->message); | ||
| 709 | ✗ | g_error_free(error); | |
| 710 | // gst_parse_launch() can return a non-null (partial) pipeline even | ||
| 711 | // when it also reports an error -- release it so we don't leak. | ||
| 712 | ✗ | if (pipeline) { | |
| 713 | ✗ | gst_object_unref(pipeline); | |
| 714 | } | ||
| 715 | ✗ | return false; | |
| 716 | } | ||
| 717 | |||
| 718 | // Set stream-properties post-launch — gst_parse_launch cannot | ||
| 719 | // deserialize GstStructure values containing '/' (e.g. Audio/Source) | ||
| 720 | // which crashes gst_value_deserialize. | ||
| 721 |
2/4✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✗ Branch 3 not taken.
|
41 | std::string recvNodeDesc = SafeNodeName(inst.name) + "_recv"; |
| 722 |
3/6✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 41 times.
✗ Branch 5 not taken.
|
41 | GstElement* pwsink = gst_bin_get_by_name(GST_BIN(pipeline), "pwsink"); |
| 723 |
1/2✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
|
41 | if (pwsink) { |
| 724 |
1/2✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
|
41 | GstStructure* props = gst_structure_new("props", |
| 725 | "media.class", G_TYPE_STRING, "Audio/Source", | ||
| 726 | "node.name", G_TYPE_STRING, nodeName.c_str(), | ||
| 727 | "node.description", G_TYPE_STRING, recvNodeDesc.c_str(), | ||
| 728 | NULL); | ||
| 729 |
1/2✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
|
41 | g_object_set(pwsink, "stream-properties", props, NULL); |
| 730 |
1/2✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
|
41 | gst_structure_free(props); |
| 731 |
1/2✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
|
41 | gst_object_unref(pwsink); |
| 732 | } | ||
| 733 | |||
| 734 |
3/6✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 41 times.
✗ Branch 5 not taken.
|
41 | GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline)); |
| 735 | |||
| 736 |
1/2✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
|
41 | GstStateChangeReturn ret = gst_element_set_state(pipeline, GST_STATE_PLAYING); |
| 737 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 33 times.
|
41 | if (ret == GST_STATE_CHANGE_FAILURE) { |
| 738 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | LogErr(VB_MEDIAOUT, "Opus RTP recv pipeline [%d] failed to start\n", inst.id); |
| 739 |
2/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
|
24 | WarningHolder::AddWarning(44, OPUS_WARNING_RECV_FAILED); |
| 740 | // Drop back to NULL before unreffing -- elements may already hold | ||
| 741 | // READY/PAUSED resources (sockets, threads, PipeWire connections) | ||
| 742 | // that gst_object_unref() alone will not release. | ||
| 743 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | gst_element_set_state(pipeline, GST_STATE_NULL); |
| 744 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | gst_object_unref(bus); |
| 745 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | gst_object_unref(pipeline); |
| 746 | 8 | return false; | |
| 747 | } | ||
| 748 | |||
| 749 | { | ||
| 750 |
1/2✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
|
33 | std::unique_lock<std::mutex> lock(m_pipelineMutex); |
| 751 | // See CreateSendPipeline() for why we stop any existing pipeline | ||
| 752 | // for this id instead of erasing the map entry directly. | ||
| 753 |
1/2✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
|
33 | auto node = m_recvPipelines.extract(inst.id); |
| 754 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
|
33 | if (!node.empty()) { |
| 755 | ✗ | lock.unlock(); | |
| 756 | ✗ | StopPipeline(node.mapped()); | |
| 757 | ✗ | lock.lock(); | |
| 758 | } | ||
| 759 |
1/2✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
|
33 | auto [it, ok] = m_recvPipelines.try_emplace(inst.id); |
| 760 | 33 | it->second.instanceId = inst.id; | |
| 761 | 33 | it->second.isSend = false; | |
| 762 | 33 | it->second.pipeline = pipeline; | |
| 763 | 33 | it->second.bus = bus; | |
| 764 | 33 | it->second.running = true; | |
| 765 | 33 | } | |
| 766 | |||
| 767 |
1/2✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
|
33 | LogInfo(VB_MEDIAOUT, "Opus RTP recv pipeline [%d] %s started <- %s:%d (%dch, %dms latency)\n", |
| 768 | inst.id, inst.name.c_str(), inst.destIP.c_str(), inst.port, | ||
| 769 | inst.channels, inst.latency); | ||
| 770 | 33 | return true; | |
| 771 | 41 | } | |
| 772 | |||
| 773 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 774 | // Pipeline management | ||
| 775 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 776 | 36 | void OpusRTPManager::StopPipeline(OpusRTPPipeline& p) { | |
| 777 |
1/2✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
|
36 | if (p.pipeline) { |
| 778 | 36 | gst_element_set_state(p.pipeline, GST_STATE_NULL); | |
| 779 | |||
| 780 | // Remove the buffer-drop probe installed by FlushSendPipelines() | ||
| 781 | // (if any) and release our ref on the pad it was installed on. | ||
| 782 | // Without this, the probe stays registered on the pad with | ||
| 783 | // &p.dropCounter as userdata -- a dangling pointer once this | ||
| 784 | // OpusRTPPipeline is erased from its owning map -- and the pad | ||
| 785 | // ref taken in FlushSendPipelines() leaks. | ||
| 786 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 36 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
36 | if (p.probeId != 0 && p.probePad) { |
| 787 | ✗ | gst_pad_remove_probe(p.probePad, p.probeId); | |
| 788 | ✗ | p.probeId = 0; | |
| 789 | } | ||
| 790 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 36 times.
|
36 | if (p.probePad) { |
| 791 | ✗ | gst_object_unref(p.probePad); | |
| 792 | ✗ | p.probePad = nullptr; | |
| 793 | } | ||
| 794 | |||
| 795 |
1/2✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
|
36 | if (p.bus) { |
| 796 | 36 | gst_object_unref(p.bus); | |
| 797 | 36 | p.bus = nullptr; | |
| 798 | } | ||
| 799 | 36 | gst_object_unref(p.pipeline); | |
| 800 | 36 | p.pipeline = nullptr; | |
| 801 | 36 | p.running = false; | |
| 802 | } | ||
| 803 | 36 | } | |
| 804 | |||
| 805 | 86 | void OpusRTPManager::StopAllPipelines() { | |
| 806 | 86 | std::map<int, OpusRTPPipeline> sendCopy, recvCopy; | |
| 807 | { | ||
| 808 |
1/2✓ Branch 0 taken 86 times.
✗ Branch 1 not taken.
|
86 | std::lock_guard<std::mutex> lock(m_pipelineMutex); |
| 809 | 86 | sendCopy.swap(m_sendPipelines); | |
| 810 | 86 | recvCopy.swap(m_recvPipelines); | |
| 811 | // Nothing is configured to be running any more, so no sender is | ||
| 812 | // waiting for a source either. ApplyConfig() refills this after its | ||
| 813 | // create pass; every other caller is a teardown. | ||
| 814 | 86 | m_deferredSenders.clear(); | |
| 815 | 86 | } | |
| 816 | |||
| 817 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 86 times.
|
89 | for (auto& [id, p] : sendCopy) { |
| 818 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | LogDebug(VB_MEDIAOUT, "OpusRTPManager: Stopping send pipeline [%d]\n", id); |
| 819 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | StopPipeline(p); |
| 820 | } | ||
| 821 | |||
| 822 |
2/2✓ Branch 0 taken 33 times.
✓ Branch 1 taken 86 times.
|
119 | for (auto& [id, p] : recvCopy) { |
| 823 |
1/2✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
|
33 | LogDebug(VB_MEDIAOUT, "OpusRTPManager: Stopping recv pipeline [%d]\n", id); |
| 824 |
1/2✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
|
33 | StopPipeline(p); |
| 825 | } | ||
| 826 | 86 | } | |
| 827 | |||
| 828 | // Pad probe callback: drops buffers while dropCounter > 0 | ||
| 829 | ✗ | static GstPadProbeReturn OpusDropBufferProbe(GstPad* pad, GstPadProbeInfo* info, gpointer userData) { | |
| 830 | ✗ | std::atomic<int>* counter = static_cast<std::atomic<int>*>(userData); | |
| 831 | ✗ | if (!(info->type & GST_PAD_PROBE_TYPE_BUFFER)) | |
| 832 | ✗ | return GST_PAD_PROBE_OK; | |
| 833 | ✗ | if (*counter <= 0) | |
| 834 | ✗ | return GST_PAD_PROBE_OK; | |
| 835 | ✗ | (*counter)--; | |
| 836 | ✗ | return GST_PAD_PROBE_DROP; | |
| 837 | } | ||
| 838 | |||
| 839 | 3106 | void OpusRTPManager::SetMasterVolume(int volume) { | |
| 840 |
1/2✓ Branch 0 taken 3106 times.
✗ Branch 1 not taken.
|
3106 | std::lock_guard<std::mutex> lock(m_pipelineMutex); |
| 841 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 3106 times.
|
3106 | for (auto& kv : m_sendPipelines) { |
| 842 | ✗ | if (kv.second.pipeline) { | |
| 843 | ✗ | SetPipelineMasterVolume(kv.second.pipeline, volume); | |
| 844 | } | ||
| 845 | } | ||
| 846 | 3106 | } | |
| 847 | |||
| 848 | ✗ | void OpusRTPManager::FlushSendPipelines() { | |
| 849 | ✗ | std::lock_guard<std::mutex> lock(m_pipelineMutex); | |
| 850 | ✗ | for (auto& kv : m_sendPipelines) { | |
| 851 | ✗ | OpusRTPPipeline& p = kv.second; | |
| 852 | ✗ | if (!p.pipeline || !p.running) | |
| 853 | ✗ | continue; | |
| 854 | |||
| 855 | ✗ | constexpr int DROP_COUNT = 10; | |
| 856 | ✗ | LogInfo(VB_MEDIAOUT, "Opus RTP send pipeline [%d]: dropping next %d buffers\n", | |
| 857 | p.instanceId, DROP_COUNT); | ||
| 858 | |||
| 859 | ✗ | p.dropCounter = DROP_COUNT; | |
| 860 | |||
| 861 | ✗ | if (p.probeId != 0) | |
| 862 | ✗ | continue; | |
| 863 | |||
| 864 | ✗ | GstIterator* it = gst_bin_iterate_sources(GST_BIN(p.pipeline)); | |
| 865 | ✗ | GValue val = G_VALUE_INIT; | |
| 866 | ✗ | GstElement* srcElem = nullptr; | |
| 867 | ✗ | if (gst_iterator_next(it, &val) == GST_ITERATOR_OK) { | |
| 868 | ✗ | srcElem = GST_ELEMENT(g_value_get_object(&val)); | |
| 869 | ✗ | gst_object_ref(srcElem); | |
| 870 | ✗ | g_value_unset(&val); | |
| 871 | } | ||
| 872 | ✗ | gst_iterator_free(it); | |
| 873 | |||
| 874 | ✗ | if (srcElem) { | |
| 875 | ✗ | GstPad* srcpad = gst_element_get_static_pad(srcElem, "src"); | |
| 876 | ✗ | if (srcpad) { | |
| 877 | ✗ | p.probePad = srcpad; | |
| 878 | ✗ | p.probeId = gst_pad_add_probe( | |
| 879 | srcpad, | ||
| 880 | GST_PAD_PROBE_TYPE_BUFFER, | ||
| 881 | OpusDropBufferProbe, | ||
| 882 | ✗ | &p.dropCounter, | |
| 883 | nullptr); | ||
| 884 | } | ||
| 885 | ✗ | gst_object_unref(srcElem); | |
| 886 | } | ||
| 887 | } | ||
| 888 | ✗ | } | |
| 889 | |||
| 890 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 891 | // Watchdog | ||
| 892 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 893 | 65 | void OpusRTPManager::WatchdogLoop() { | |
| 894 | 65 | constexpr int WATCHDOG_INTERVAL_S = 30; | |
| 895 | |||
| 896 |
1/2✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
|
65 | while (m_watchdogRunning.load()) { |
| 897 | // Sleep in small increments so we can exit promptly | ||
| 898 |
6/6✓ Branch 0 taken 713 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 649 times.
✓ Branch 3 taken 64 times.
✓ Branch 4 taken 649 times.
✓ Branch 5 taken 65 times.
|
714 | for (int i = 0; i < WATCHDOG_INTERVAL_S * 10 && m_watchdogRunning.load(); i++) { |
| 899 |
1/2✓ Branch 0 taken 649 times.
✗ Branch 1 not taken.
|
649 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); |
| 900 | } | ||
| 901 | |||
| 902 |
2/2✓ Branch 0 taken 64 times.
✓ Branch 1 taken 1 times.
|
65 | if (!m_watchdogRunning.load()) break; |
| 903 | |||
| 904 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | if (PollPipelinesWatchdog()) { |
| 905 | // We cannot call ApplyConfig() directly from this thread: | ||
| 906 | // ApplyConfig() joins m_watchdogThread -- which IS this thread | ||
| 907 | // -- and std::thread::join() on the calling thread itself | ||
| 908 | // throws std::system_error, which propagates out of this | ||
| 909 | // thread function and terminates fppd (see AES67Manager's | ||
| 910 | // SAPAnnounceLoop() for the same issue). Instead, stop this | ||
| 911 | // loop and hand off to a short-lived thread that calls | ||
| 912 | // ApplyConfig() once we've exited. | ||
| 913 | 1 | LogWarn(VB_MEDIAOUT, "OpusRTPManager: Watchdog detected failed pipelines, scheduling rebuild\n"); | |
| 914 | 1 | m_watchdogRunning.store(false); | |
| 915 | // Track this as m_rebuildThread (rather than detaching) so | ||
| 916 | // Shutdown() can join it before tearing anything else down -- a | ||
| 917 | // detached thread could otherwise call ApplyConfig() and | ||
| 918 | // resurrect pipelines after Shutdown() has already run, and a | ||
| 919 | // detached thread racing Shutdown()'s own join of | ||
| 920 | // m_watchdogThread is undefined behavior. | ||
| 921 | 1 | ScheduleRebuild(); | |
| 922 | 1 | break; | |
| 923 | } | ||
| 924 | } | ||
| 925 | 65 | } | |
| 926 | |||
| 927 | 1 | bool OpusRTPManager::PollPipelinesWatchdog() { | |
| 928 | 1 | bool needsRebuild = false; | |
| 929 | |||
| 930 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | std::lock_guard<std::mutex> lock(m_pipelineMutex); |
| 931 | |||
| 932 | 2 | auto checkPipelines = [this, &needsRebuild](std::map<int, OpusRTPPipeline>& pipelines, const char* direction) { | |
| 933 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | for (auto& kv : pipelines) { |
| 934 | ✗ | OpusRTPPipeline& p = kv.second; | |
| 935 | ✗ | if (!p.pipeline || !p.running) continue; | |
| 936 | |||
| 937 | // Drain bus messages | ||
| 938 | ✗ | if (p.bus) { | |
| 939 | GstMessage* msg; | ||
| 940 | ✗ | while ((msg = gst_bus_pop(p.bus)) != nullptr) { | |
| 941 | ✗ | switch (GST_MESSAGE_TYPE(msg)) { | |
| 942 | ✗ | case GST_MESSAGE_ERROR: { | |
| 943 | ✗ | GError* err = nullptr; gchar* dbg = nullptr; | |
| 944 | ✗ | gst_message_parse_error(msg, &err, &dbg); | |
| 945 | ✗ | LogErr(VB_MEDIAOUT, | |
| 946 | "Opus RTP %s pipeline [%d] bus error: %s\n", | ||
| 947 | direction, p.instanceId, err->message); | ||
| 948 | ✗ | g_error_free(err); g_free(dbg); | |
| 949 | ✗ | p.errorMessage = "GStreamer error"; | |
| 950 | ✗ | break; | |
| 951 | } | ||
| 952 | ✗ | case GST_MESSAGE_WARNING: { | |
| 953 | ✗ | GError* err = nullptr; gchar* dbg = nullptr; | |
| 954 | ✗ | gst_message_parse_warning(msg, &err, &dbg); | |
| 955 | ✗ | LogWarn(VB_MEDIAOUT, | |
| 956 | "Opus RTP %s pipeline [%d] bus warning: %s\n", | ||
| 957 | direction, p.instanceId, err->message); | ||
| 958 | ✗ | g_error_free(err); g_free(dbg); | |
| 959 | ✗ | break; | |
| 960 | } | ||
| 961 | ✗ | default: break; | |
| 962 | } | ||
| 963 | ✗ | gst_message_unref(msg); | |
| 964 | } | ||
| 965 | } | ||
| 966 | |||
| 967 | // Check current pipeline state | ||
| 968 | ✗ | GstState curState = GST_STATE_NULL, pendingState = GST_STATE_VOID_PENDING; | |
| 969 | ✗ | gst_element_get_state(p.pipeline, &curState, &pendingState, 0); | |
| 970 | |||
| 971 | ✗ | if (curState != GST_STATE_PLAYING && pendingState != GST_STATE_PLAYING) { | |
| 972 | ✗ | LogWarn(VB_MEDIAOUT, | |
| 973 | "Opus RTP %s pipeline [%d] is in %s state -- flagging for rebuild\n", | ||
| 974 | direction, p.instanceId, | ||
| 975 | gst_element_state_get_name(curState)); | ||
| 976 | ✗ | p.running = false; | |
| 977 | ✗ | p.errorMessage = "Watchdog: not in PLAYING state"; | |
| 978 | ✗ | needsRebuild = true; | |
| 979 | ✗ | } else if (p.isSend) { | |
| 980 | // Zombie detection for send pipelines: check udpsink byte count | ||
| 981 | ✗ | GstElement* usink = gst_bin_get_by_name(GST_BIN(p.pipeline), "usink"); | |
| 982 | ✗ | if (usink) { | |
| 983 | ✗ | guint64 bytesSent = 0; | |
| 984 | ✗ | g_object_get(usink, "bytes-served", &bytesSent, nullptr); | |
| 985 | ✗ | gst_object_unref(usink); | |
| 986 | |||
| 987 | ✗ | if (bytesSent == p.lastByteCount) { | |
| 988 | ✗ | p.stallCount++; | |
| 989 | ✗ | if (p.stallCount >= 2) { | |
| 990 | ✗ | LogWarn(VB_MEDIAOUT, | |
| 991 | "Opus RTP send pipeline [%d] stalled (no bytes for %d checks)\n", | ||
| 992 | p.instanceId, p.stallCount); | ||
| 993 | ✗ | p.running = false; | |
| 994 | ✗ | p.errorMessage = "Watchdog: send pipeline stalled"; | |
| 995 | ✗ | needsRebuild = true; | |
| 996 | } | ||
| 997 | } else { | ||
| 998 | ✗ | p.stallCount = 0; | |
| 999 | } | ||
| 1000 | ✗ | p.lastByteCount = bytesSent; | |
| 1001 | } | ||
| 1002 | } | ||
| 1003 | } | ||
| 1004 | 2 | }; | |
| 1005 | |||
| 1006 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | checkPipelines(m_sendPipelines, "send"); |
| 1007 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | checkPipelines(m_recvPipelines, "recv"); |
| 1008 | |||
| 1009 | // A stream that failed to start is never stored, so nothing above sees | ||
| 1010 | // it. The first retry is this pass; one that keeps failing waits 30s, | ||
| 1011 | // 60s, ... up to 8 minutes, and the count starts over after 10 quiet | ||
| 1012 | // minutes. | ||
| 1013 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | if (m_failedStarts > 0) { |
| 1014 | 1 | const auto now = std::chrono::steady_clock::now(); | |
| 1015 |
2/10✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1 times.
|
1 | if (m_failedStartRebuilds > 0 && now - m_lastFailedStartRebuild > std::chrono::minutes(10)) { |
| 1016 | ✗ | m_failedStartRebuilds = 0; | |
| 1017 | } | ||
| 1018 | 1 | const auto wait = std::chrono::seconds(30) * (1 << std::min(std::max(m_failedStartRebuilds - 1, 0), 4)); | |
| 1019 |
3/12✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 times.
✗ Branch 11 not taken.
|
1 | if (needsRebuild || m_failedStartRebuilds == 0 || now - m_lastFailedStartRebuild >= wait) { |
| 1020 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | if (!needsRebuild) { |
| 1021 | 1 | m_failedStartRebuilds++; | |
| 1022 | 1 | m_lastFailedStartRebuild = now; | |
| 1023 | } | ||
| 1024 | 1 | needsRebuild = true; | |
| 1025 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | LogWarn(VB_MEDIAOUT, "Opus RTP watchdog: %d stream(s) failed to start, trying again\n", |
| 1026 | m_failedStarts); | ||
| 1027 | } | ||
| 1028 | } | ||
| 1029 | |||
| 1030 | 1 | return needsRebuild; | |
| 1031 | 1 | } | |
| 1032 | |||
| 1033 | 584 | OpusRTPManager::Status OpusRTPManager::GetStatus() { | |
| 1034 | 584 | Status status; | |
| 1035 | |||
| 1036 | // Snapshot the config BEFORE taking the pipeline lock and use the copy | ||
| 1037 | // from here on -- reading m_config directly would race LoadConfig() | ||
| 1038 | // reallocating the instance vector on another thread. See m_configMutex. | ||
| 1039 |
1/2✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
|
584 | OpusRTPConfig config = GetConfigSnapshot(); |
| 1040 | |||
| 1041 | 584 | std::unique_lock<std::mutex> lock(m_pipelineMutex, std::try_to_lock); | |
| 1042 |
1/2✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
|
584 | if (lock.owns_lock()) { |
| 1043 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 584 times.
|
586 | for (const auto& [id, p] : m_sendPipelines) { |
| 1044 | 2 | Status::PipelineStatus ps; | |
| 1045 | 2 | ps.instanceId = id; | |
| 1046 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | ps.mode = "send"; |
| 1047 | 2 | ps.running = p.running; | |
| 1048 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | ps.error = p.errorMessage; |
| 1049 | |||
| 1050 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | for (const auto& inst : config.instances) { |
| 1051 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | if (inst.id == id) { |
| 1052 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | ps.name = inst.name; |
| 1053 | 2 | break; | |
| 1054 | } | ||
| 1055 | } | ||
| 1056 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | status.pipelines.push_back(ps); |
| 1057 | 2 | } | |
| 1058 | |||
| 1059 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 584 times.
|
585 | for (const auto& [id, p] : m_recvPipelines) { |
| 1060 | 1 | Status::PipelineStatus ps; | |
| 1061 | 1 | ps.instanceId = id; | |
| 1062 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | ps.mode = "receive"; |
| 1063 | 1 | ps.running = p.running; | |
| 1064 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | ps.error = p.errorMessage; |
| 1065 | |||
| 1066 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | for (const auto& inst : config.instances) { |
| 1067 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
|
2 | if (inst.id == id) { |
| 1068 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | ps.name = inst.name; |
| 1069 | 1 | break; | |
| 1070 | } | ||
| 1071 | } | ||
| 1072 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | status.pipelines.push_back(ps); |
| 1073 | 1 | } | |
| 1074 | |||
| 1075 | // Senders deliberately held idle. Reported alongside the real | ||
| 1076 | // pipelines rather than omitted: a stream the user enabled and cannot | ||
| 1077 | // see anywhere reads as FPP having lost the config. | ||
| 1078 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 584 times.
|
585 | for (const auto& [id, reason] : m_deferredSenders) { |
| 1079 | 1 | Status::PipelineStatus ps; | |
| 1080 | 1 | ps.instanceId = id; | |
| 1081 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | ps.mode = "send"; |
| 1082 | 1 | ps.running = false; | |
| 1083 | 1 | ps.waitingForSource = true; | |
| 1084 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | ps.note = reason; |
| 1085 | |||
| 1086 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | for (const auto& inst : config.instances) { |
| 1087 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | if (inst.id == id) { |
| 1088 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | ps.name = inst.name; |
| 1089 | 1 | break; | |
| 1090 | } | ||
| 1091 | } | ||
| 1092 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | status.pipelines.push_back(ps); |
| 1093 | 1 | } | |
| 1094 | } else { | ||
| 1095 | ✗ | Status::PipelineStatus ps; | |
| 1096 | ✗ | ps.instanceId = -1; | |
| 1097 | ✗ | ps.name = "(status unavailable -- pipeline operation in progress)"; | |
| 1098 | ✗ | ps.mode = "unknown"; | |
| 1099 | ✗ | ps.running = false; | |
| 1100 | ✗ | status.pipelines.push_back(ps); | |
| 1101 | ✗ | } | |
| 1102 | |||
| 1103 | 584 | return status; | |
| 1104 | 584 | } | |
| 1105 | |||
| 1106 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 1107 | // HTTP API | ||
| 1108 | // ────────────────────────────────────────────────────────────────────────────── | ||
| 1109 | 584 | HttpResponsePtr OpusRTPManager::render_GET(const HttpRequestPtr& req) { | |
| 1110 |
2/4✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 584 times.
✗ Branch 3 not taken.
|
584 | std::string url(req->path()); |
| 1111 | |||
| 1112 |
2/2✓ Branch 0 taken 579 times.
✓ Branch 1 taken 5 times.
|
584 | if (url.find("/opusrtp/") == 0) { |
| 1113 |
1/2✓ Branch 0 taken 579 times.
✗ Branch 1 not taken.
|
579 | url = url.substr(9); |
| 1114 |
2/4✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
|
5 | } else if (url == "/opusrtp") { |
| 1115 |
1/2✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
|
5 | url = "status"; |
| 1116 | } | ||
| 1117 | |||
| 1118 |
2/4✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 584 times.
✗ Branch 3 not taken.
|
584 | if (url == "status") { |
| 1119 |
1/2✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
|
584 | Status st = GetStatus(); |
| 1120 |
1/2✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
|
584 | Json::Value result; |
| 1121 | |||
| 1122 |
1/2✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
|
584 | Json::Value pipelines(Json::arrayValue); |
| 1123 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 584 times.
|
588 | for (const auto& p : st.pipelines) { |
| 1124 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | Json::Value pj; |
| 1125 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | pj["instanceId"] = p.instanceId; |
| 1126 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | pj["name"] = p.name; |
| 1127 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | pj["mode"] = p.mode; |
| 1128 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | pj["running"] = p.running; |
| 1129 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | if (!p.error.empty()) { |
| 1130 | ✗ | pj["error"] = p.error; | |
| 1131 | } | ||
| 1132 | // Distinct from "error" on purpose: the UI renders a failure in | ||
| 1133 | // red and this as guidance, so collapsing the two would put a | ||
| 1134 | // brand new instance back in the alarming state this replaced. | ||
| 1135 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3 times.
|
4 | if (p.waitingForSource) { |
| 1136 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
1 | pj["waitingForSource"] = true; |
| 1137 | } | ||
| 1138 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3 times.
|
4 | if (!p.note.empty()) { |
| 1139 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
1 | pj["note"] = p.note; |
| 1140 | } | ||
| 1141 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | pipelines.append(pj); |
| 1142 | 4 | } | |
| 1143 |
2/4✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 584 times.
✗ Branch 3 not taken.
|
584 | result["pipelines"] = pipelines; |
| 1144 |
2/4✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 584 times.
✗ Branch 3 not taken.
|
584 | result["active"] = m_active.load(); |
| 1145 | |||
| 1146 |
1/2✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
|
584 | Json::StreamWriterBuilder wbuilder; |
| 1147 |
3/6✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 584 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 584 times.
✗ Branch 5 not taken.
|
1752 | wbuilder["indentation"] = ""; |
| 1148 |
1/2✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
|
584 | std::string resultStr = Json::writeString(wbuilder, result); |
| 1149 | |||
| 1150 |
2/4✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 584 times.
✗ Branch 3 not taken.
|
584 | return makeStringResponse(resultStr, 200, "application/json"); |
| 1151 | 584 | } | |
| 1152 | |||
| 1153 | ✗ | return makeStringResponse("{\"error\":\"unknown endpoint\"}", 404, "application/json"); | |
| 1154 | 584 | } | |
| 1155 | |||
| 1156 | #endif // HAS_OPUS_RTP_GSTREAMER | ||
| 1157 |