GCC Code Coverage Report


Directory: ./
File: commands/MediaCommands.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 455 481 94.6%
Functions: 56 58 96.6%
Branches: 590 1096 53.8%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 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
17 #include "../Warnings.h" // WarningHolder -- needed directly for NOPCH builds
18 #include "../common.h" // SaveJsonToString -- needed directly for NOPCH builds
19
20 #include <curl/curl.h>
21
22 #include <atomic>
23 #include <cstdint>
24 #include <functional>
25 #include <mutex>
26 #include <thread>
27 #include <vector>
28
29 #include "../Timers.h"
30 #include "../fppversion.h"
31 #include "../log.h"
32 #include "../settings.h"
33
34 #include "MediaCommands.h"
35 #include "../mediaoutput/GStreamerOut.h"
36 #include "../mediaoutput/mediaoutput.h" // ResolveVideoOutput()
37 #include "../mediaoutput/StreamSlotManager.h"
38 #include "../CurlSocket.h"
39
40 1888 IncreaseVolumeCommand::IncreaseVolumeCommand() :
41
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("Volume Increase", "Increases the volume by the given amount (0 - 100)") {
42
6/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
11328 args.push_back(CommandArg("volume", "int", "Volume").setRange(0, 100).setDefaultValue("0"));
43 1888 }
44 4 std::unique_ptr<Command::Result> IncreaseVolumeCommand::run(const std::vector<std::string>& args) {
45
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3 times.
4 if (args.size() != 1) {
46
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 return std::make_unique<Command::ErrorResult>("Not found");
47 }
48 3 int v = getVolume();
49 3 v += std::atoi(args[0].c_str());
50 3 setVolume(v);
51 3 return std::make_unique<Command::Result>("Volume Set");
52 }
53
54 1888 DecreaseVolumeCommand::DecreaseVolumeCommand() :
55
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("Volume Decrease", "Decreases the volume by the given amount (0 - 100)") {
56
6/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
11328 args.push_back(CommandArg("volume", "int", "Volume").setRange(0, 100).setDefaultValue("0"));
57 1888 }
58 4 std::unique_ptr<Command::Result> DecreaseVolumeCommand::run(const std::vector<std::string>& args) {
59
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3 times.
4 if (args.size() != 1) {
60
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 return std::make_unique<Command::ErrorResult>("Not found");
61 }
62
63 3 int v = getVolume();
64 3 v -= std::atoi(args[0].c_str());
65 3 setVolume(v);
66 3 return std::make_unique<Command::Result>("Volume Set");
67 }
68
69 // A URL command's response is kept whole for the caller, so it is capped:
70 // unbounded, one URL at a large or endless body (file:///dev/zero, before
71 // the protocols were limited below, or any HTTP server that streams) grew it
72 // until fppd was OOM-killed. Returning short aborts the transfer.
73 static constexpr size_t URL_MAX_RESPONSE = 4 * 1024 * 1024;
74 93 static size_t URL_write_data(void* buffer, size_t size, size_t nmemb, void* userp) {
75 93 std::string* msg = (std::string*)userp;
76 93 size_t n = size * nmemb;
77
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 93 times.
93 if (msg->size() + n > URL_MAX_RESPONSE) {
78 ✗ return 0;
79 }
80 93 msg->append(static_cast<const char*>(buffer), n);
81 93 return n;
82 }
83
84 // A URL command's transfer outlives its Result. Fire-and-forget callers
85 // (presets, an If's Parallel branch, GPIO, MultiSync) drop the Result at
86 // once, and ~CURLResult used to busy-wait up to a second for the transfer and
87 // then abort it: every webhook to a slow or dead host stalled the calling
88 // thread - usually the main loop - for a second, and any response slower than
89 // that was silently cut off. The destructor now hands the still-running
90 // transfer here, whole (moving the easy handle to another multi would restart
91 // it, and could send a POST twice), and the main loop finishes it.
92 static constexpr long URL_TRANSFER_TIMEOUT_MS = 30000;
93 static constexpr long URL_CONNECT_TIMEOUT_MS = 10000;
94 namespace {
95 struct OrphanTransfer {
96 CURLM* multi;
97 CURL* easy;
98 long long deadline;
99 std::string url;
100 };
101 std::mutex s_orphanLock;
102 std::vector<OrphanTransfer> s_orphans;
103 bool s_reaperArmed = false;
104
105 27 size_t DiscardBody(void*, size_t size, size_t nmemb, void*) {
106 27 return size * nmemb;
107 }
108
109 697 void PumpOrphanTransfers() {
110 697 std::vector<OrphanTransfer> work;
111 {
112
1/2
✓ Branch 0 taken 697 times.
✗ Branch 1 not taken.
697 std::lock_guard<std::mutex> l(s_orphanLock);
113 697 work.swap(s_orphans);
114 697 }
115 697 std::vector<OrphanTransfer> keep;
116
1/2
✓ Branch 0 taken 697 times.
✗ Branch 1 not taken.
697 long long now = GetMonotonicTimeMS();
117
2/2
✓ Branch 0 taken 900 times.
✓ Branch 1 taken 697 times.
1597 for (auto& o : work) {
118 900 int running = 0;
119
1/2
✓ Branch 0 taken 900 times.
✗ Branch 1 not taken.
900 CURLMcode mc = curl_multi_perform(o.multi, &running);
120
4/6
✓ Branch 0 taken 900 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 896 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 896 times.
✗ Branch 5 not taken.
900 if (mc == CURLM_OK && running > 0 && now < o.deadline) {
121
1/2
✓ Branch 0 taken 896 times.
✗ Branch 1 not taken.
896 keep.push_back(o);
122 896 continue;
123 }
124
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (running > 0) {
125 ✗ LogWarn(VB_COMMAND, "URL \"%s\" did not finish; abandoning it\n", o.url.c_str());
126 }
127
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 curl_multi_remove_handle(o.multi, o.easy);
128
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 curl_multi_cleanup(o.multi);
129
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 curl_easy_cleanup(o.easy);
130 }
131
1/2
✓ Branch 0 taken 697 times.
✗ Branch 1 not taken.
697 std::lock_guard<std::mutex> l(s_orphanLock);
132
1/2
✓ Branch 0 taken 697 times.
✗ Branch 1 not taken.
697 s_orphans.insert(s_orphans.end(), keep.begin(), keep.end());
133
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 694 times.
697 if (s_orphans.empty()) {
134 3 s_reaperArmed = false;
135 } else {
136
3/6
✓ Branch 0 taken 694 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 694 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 694 times.
✗ Branch 5 not taken.
2082 Timers::INSTANCE.addTimer("URLCommandTransfers", GetTimeMS() + 10, PumpOrphanTransfers);
137 }
138 697 }
139
140 8 void AdoptOrphanTransfer(CURLM* multi, CURL* easy, const std::string& url) {
141 // The body has nowhere to go once the Result is gone.
142
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, DiscardBody);
143
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 curl_easy_setopt(easy, CURLOPT_WRITEDATA, nullptr);
144
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::lock_guard<std::mutex> l(s_orphanLock);
145
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 s_orphans.push_back({ multi, easy, GetMonotonicTimeMS() + URL_TRANSFER_TIMEOUT_MS + 5000, url });
146
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 3 times.
8 if (!s_reaperArmed) {
147 5 s_reaperArmed = true;
148
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
15 Timers::INSTANCE.addTimer("URLCommandTransfers", GetTimeMS() + 10, PumpOrphanTransfers);
149 }
150
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
16 }
151 } // namespace
152
153 class CURLResult : public Command::Result {
154 public:
155 39 CURLResult(const std::vector<std::string>& args) :
156 39 Command::Result() {
157
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 std::string url = args[0];
158
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 m_url = url;
159
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 LogDebug(VB_COMMAND, "URL: \"%s\"\n", url.c_str());
160
161
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 std::string method = "GET";
162
2/2
✓ Branch 0 taken 38 times.
✓ Branch 1 taken 1 times.
39 if (args.size() > 1) {
163
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 method = args[1];
164
2/4
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 38 times.
38 if (method == "") {
165 ✗ method = "GET";
166 }
167 }
168 39 std::string data;
169
2/2
✓ Branch 0 taken 38 times.
✓ Branch 1 taken 1 times.
39 if (args.size() > 2) {
170
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 data = args[2];
171 }
172
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 m_curl = curl_easy_init();
173
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 SetCurlCloseOnExec(m_curl);
174
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 curl_easy_setopt(m_curl, CURLOPT_NOSIGNAL, 1);
175 // Without these, a plain-HTTP URL that 301/302s to HTTPS (extremely
176 // common) captures the redirect response instead of the real page -
177 // typically an empty body - and reports success with no error, since
178 // isError()/isDone() below only look at handle setup and CURLMSG_DONE,
179 // not the HTTP status or transfer result. A missing User-Agent also
180 // gets flatly blocked (empty body, no redirect) by some sites/WAFs
181 // that tolerate the same request with one set (e.g. Akamai-fronted
182 // sites) - same "FPP/<version>" string used by CurlManager/MultiSync.
183
6/12
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 12 times.
✓ Branch 2 taken 27 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 27 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 27 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 27 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
93 static const std::string userAgent = std::string("FPP/") + getFPPVersionTriplet();
184
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 curl_easy_setopt(m_curl, CURLOPT_USERAGENT, userAgent.c_str());
185
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 curl_easy_setopt(m_curl, CURLOPT_FOLLOWLOCATION, 1L);
186
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 curl_easy_setopt(m_curl, CURLOPT_MAXREDIRS, 10L);
187 // HTTP(S) only, for the URL and every redirect. libcurl speaks file://,
188 // gopher://, dict:// and more by default, and this command is reachable
189 // from the API and MultiSync: "file:///etc/shadow" read any file as
190 // root, and gopher:// could put arbitrary bytes on a local port.
191 #if LIBCURL_VERSION_NUM >= 0x075500
192
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 curl_easy_setopt(m_curl, CURLOPT_PROTOCOLS_STR, "http,https");
193
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 curl_easy_setopt(m_curl, CURLOPT_REDIR_PROTOCOLS_STR, "http,https");
194 #else
195 curl_easy_setopt(m_curl, CURLOPT_PROTOCOLS, (long)(CURLPROTO_HTTP | CURLPROTO_HTTPS));
196 curl_easy_setopt(m_curl, CURLOPT_REDIR_PROTOCOLS, (long)(CURLPROTO_HTTP | CURLPROTO_HTTPS));
197 #endif
198 // A transfer may now outlive its Result (see AdoptOrphanTransfer()),
199 // so it needs its own end.
200
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 curl_easy_setopt(m_curl, CURLOPT_CONNECTTIMEOUT_MS, URL_CONNECT_TIMEOUT_MS);
201
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 curl_easy_setopt(m_curl, CURLOPT_TIMEOUT_MS, URL_TRANSFER_TIMEOUT_MS);
202
203 CURLcode status;
204
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 status = curl_easy_setopt(m_curl, CURLOPT_URL, url.c_str());
205
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 38 times.
39 if (status != CURLE_OK) {
206
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 cleanupCurl();
207 1 return;
208 }
209
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 status = curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, URL_write_data);
210
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 38 times.
38 if (status != CURLE_OK) {
211 ✗ cleanupCurl();
212 ✗ return;
213 }
214
215
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 status = curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, &m_result);
216
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 38 times.
38 if (status != CURLE_OK) {
217 ✗ cleanupCurl();
218 ✗ return;
219 }
220
221
3/4
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 32 times.
38 if (method == "POST") {
222
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 curl_easy_setopt(m_curl, CURLOPT_POSTFIELDSIZE, data.size());
223
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 status = curl_easy_setopt(m_curl, CURLOPT_COPYPOSTFIELDS, data.c_str());
224
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (status != CURLE_OK) {
225 ✗ cleanupCurl();
226 ✗ return;
227 }
228 }
229
230
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 m_curlm = curl_multi_init();
231
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 CURLMcode mstatus = curl_multi_add_handle(m_curlm, m_curl);
232
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 38 times.
38 if (mstatus != CURLM_OK) {
233 ✗ cleanupCurl();
234 ✗ return;
235 }
236 38 m_isDone = false;
237
6/6
✓ Branch 0 taken 38 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 38 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 38 times.
✓ Branch 5 taken 1 times.
41 }
238 156 virtual ~CURLResult() {
239
6/8
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 31 times.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✓ Branch 7 taken 31 times.
78 if (!isDone() && m_curl && m_curlm) {
240 16 AdoptOrphanTransfer(m_curlm, m_curl, m_url);
241 16 m_curl = nullptr;
242 16 m_curlm = nullptr;
243 16 return;
244 }
245 62 cleanupCurl();
246
4/4
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 31 times.
✓ Branch 3 taken 8 times.
172 }
247 // m_curl/m_curlm only go null on setup failure (cleanupCurl()); a transfer
248 // that completes but failed (DNS/connect/timeout/etc, msg->data.result !=
249 // CURLE_OK below) left both non-null, so isError() alone previously never
250 // caught it - callers like RecurringTasks::finish() treat !isError() as
251 // "ok" and write get()'s (empty, on a failed transfer) result straight
252 // into the target Variable with no visible error.
253
4/6
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 25 times.
28 virtual bool isError() override { return m_curl == nullptr || m_curlm == nullptr || m_transferFailed; }
254 536 virtual bool isDone() override {
255
3/4
✓ Branch 0 taken 535 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 535 times.
536 if (m_curl == nullptr || m_curlm == nullptr) {
256 1 return true;
257 }
258
2/2
✓ Branch 0 taken 58 times.
✓ Branch 1 taken 477 times.
535 if (m_isDone) {
259 58 return true;
260 }
261 int handleCount;
262
1/2
✓ Branch 0 taken 477 times.
✗ Branch 1 not taken.
477 CURLMcode mstatus = curl_multi_perform(m_curlm, &handleCount);
263
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 477 times.
477 if (mstatus != CURLM_OK) {
264 ✗ cleanupCurl();
265 ✗ return true;
266 }
267
268
2/2
✓ Branch 0 taken 30 times.
✓ Branch 1 taken 447 times.
477 if (handleCount == 0) {
269 30 int messagesLeft = 0;
270
1/2
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
30 CURLMsg* msg = curl_multi_info_read(m_curlm, &messagesLeft);
271
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 if (msg && msg->msg == CURLMSG_DONE) {
272
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 27 times.
30 if (msg->data.result != CURLE_OK) {
273 3 m_transferFailed = true;
274
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 m_result = curl_easy_strerror(msg->data.result);
275
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogErr(VB_COMMAND, "URL \"%s\" failed: %s\n", m_url.c_str(), m_result.c_str());
276 } else {
277 27 char* ct = nullptr;
278
2/4
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27 times.
✗ Branch 3 not taken.
27 if (CURLE_OK == curl_easy_getinfo(m_curl, CURLINFO_CONTENT_TYPE, &ct)) {
279
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 24 times.
27 if (ct != nullptr) {
280
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 m_contentType = ct;
281 }
282 }
283 }
284 30 m_isDone = true;
285 }
286 }
287 477 return m_isDone;
288 }
289
290 32 void cleanupCurl() {
291
2/2
✓ Branch 0 taken 30 times.
✓ Branch 1 taken 2 times.
32 if (m_curlm) {
292
1/2
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
30 if (m_curl) {
293 30 curl_multi_remove_handle(m_curlm, m_curl);
294 }
295 30 curl_multi_cleanup(m_curlm);
296 30 m_curlm = nullptr;
297 }
298
2/2
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 1 times.
32 if (m_curl) {
299 31 curl_easy_cleanup(m_curl);
300 31 m_curl = nullptr;
301 }
302 32 }
303
304 private:
305 // Initialized here, not in the constructor: a setup failure (a URL curl
306 // rejects, curl_easy_init() returning NULL) calls cleanupCurl() before
307 // m_curlm is assigned, and it then handed a garbage pointer to
308 // curl_multi_remove_handle()/curl_multi_cleanup().
309 CURL* m_curl = nullptr;
310 CURLM* m_curlm = nullptr;
311 bool m_isDone = true;
312 bool m_transferFailed = false;
313 std::string m_url;
314 };
315
316 1888 URLCommand::URLCommand() :
317
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
3776 Command("URL") {
318
5/10
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
11328 args.push_back(CommandArg("name", "string", "URL"));
319
7/14
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1888 times.
✗ Branch 13 not taken.
15104 args.push_back(CommandArg("method", "string", "Method").setContentList({ "GET", "POST" }).setDefaultValue("GET"));
320
5/10
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
11328 args.push_back(CommandArg("data", "string", "Post Data"));
321 1888 }
322 33 std::unique_ptr<Command::Result> URLCommand::run(const std::vector<std::string>& args) {
323
1/2
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
33 return std::make_unique<CURLResult>(args);
324 }
325
326 // Apply a volume to one node of the PipeWire graph rather than the global
327 // master. "target" is a flat key from GET /api/pipewire/audio/targets
328 // ("sink:<node>", "slot:<n>", "input:<groupId>:<memberIndex>",
329 // "route:<inputGroupId>:<outputGroupId>"). With relative set, the value is a
330 // delta against the target's own current level rather than an absolute.
331 //
332 // Handed to the PHP API rather than resolved here: mapping a target to a
333 // PipeWire node means reading the group/input-group JSON and reproducing the
334 // node-naming rules, and reading a target's current level back for a relative
335 // adjust -- all of which already exist in www/api/controllers/pipewire.php and
336 // would otherwise have to be kept in sync in a second language. Same loopback
337 // approach ApplyRoutingPresetCommand below already uses.
338 5 static std::unique_ptr<Command::Result> setTargetedVolume(const std::string& target, int value, bool relative) {
339
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 Json::Value body;
340
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 body["target"] = target;
341
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
5 if (relative) {
342
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 body["adjust"] = value;
343 } else {
344
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 body["volume"] = value;
345 }
346
347 std::vector<std::string> curlArgs = {
348 "http://localhost/api/system/volume",
349 "POST",
350 5 SaveJsonToString(body)
351
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
25 };
352
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
10 return std::make_unique<CURLResult>(curlArgs);
353
4/12
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
15 }
354
355 1888 SetVolumeCommand::SetVolumeCommand() :
356
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("Volume Set", "Sets the volume to the specific value. (0 - 100)") {
357
8/16
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1888 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 1888 times.
✗ Branch 15 not taken.
15104 args.push_back(CommandArg("volume", "int", "Volume").setRange(0, 100).setDefaultValue("70").setGetAdjustableValueURL("api/fppd/volume?simple=true"));
358 // Optional and trailing: every existing caller passes only the volume and
359 // must keep hitting the global-master path below untouched.
360
4/8
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
13216 args.push_back(CommandArg("target", "string", "Target", true)
361
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
3776 .setContentListUrl("api/pipewire/audio/targets", true));
362 1888 }
363 1175 std::unique_ptr<Command::Result> SetVolumeCommand::run(const std::vector<std::string>& args) {
364
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1175 times.
1175 if (args.empty()) {
365 ✗ return std::make_unique<Command::ErrorResult>("Not found");
366 }
367
368 1175 int v = std::atoi(args[0].c_str());
369
6/6
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1172 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 1173 times.
1175 if (args.size() > 1 && !args[1].empty()) {
370 2 return setTargetedVolume(args[1], v, false);
371 }
372 1173 setVolume(v);
373 1173 return std::make_unique<Command::Result>("Volume Set");
374 }
375
376 1888 AdjustVolumeCommand::AdjustVolumeCommand() :
377
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("Volume Adjust", "Adjust volume either up or down by the given amount. (-100 - 100)") {
378
6/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
11328 args.push_back(CommandArg("volume", "int", "Volume").setRange(-100, 100).setDefaultValue("0"));
379
4/8
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
13216 args.push_back(CommandArg("target", "string", "Target", true)
380
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
3776 .setContentListUrl("api/pipewire/audio/targets", true));
381 1888 }
382 9 std::unique_ptr<Command::Result> AdjustVolumeCommand::run(const std::vector<std::string>& args) {
383
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (args.empty()) {
384 ✗ return std::make_unique<Command::ErrorResult>("Not found");
385 }
386
387 9 int delta = std::atoi(args[0].c_str());
388
6/6
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 6 times.
9 if (args.size() > 1 && !args[1].empty()) {
389 3 return setTargetedVolume(args[1], delta, true);
390 }
391
392 6 int v = getVolume();
393 6 v += delta;
394 6 setVolume(v);
395 6 return std::make_unique<Command::Result>("Volume Set");
396 }
397
398 #ifdef HAS_GSTREAMER
399
400 // Common data for tracking running command media
401 // Recursive: Stop() runs under this lock. MEDIA_STOPPED now fires later from
402 // the main loop with the lock released (RemoveRunningMedia), but it once fired
403 // from inside Stop(), where a preset running Play Media re-took this lock on
404 // the same thread and a plain mutex deadlocked fppd.
405 std::recursive_mutex runningMediaLock;
406 std::map<std::string, MediaOutputBase*> runningCommandMedia;
407
408 // Called from Stopped(), which runs on a GStreamer streaming thread when the
409 // media ends by itself, so everything happens on the main loop instead. The
410 // MEDIA_STOPPED preset in particular can stop playlists, start media or stop
411 // this very output; run on the streaming thread, those wait on the thread
412 // that is running them.
413 1792 static void RemoveRunningMedia(const std::string& filename, MediaOutputBase* self) {
414 1792 intptr_t i = (intptr_t)self;
415 1792 MediaOutputBase* dt = self;
416
1/2
✓ Branch 0 taken 1792 times.
✗ Branch 1 not taken.
1792 std::string fn = filename;
417
418
6/12
✓ Branch 0 taken 1792 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1792 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1792 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1792 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1792 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1792 times.
✗ Branch 11 not taken.
1792 Timers::INSTANCE.addTimer("RemoveRunningMedia-" + std::to_string(i), GetTimeMS() + 1, [dt, fn]() {
419 // Unregister first: the preset below can start the same file again,
420 // and it should get the name back rather than a "_" suffix. This
421 // object stays alive until the delete at the end, so a preset that
422 // reaches it through its stream slot still finds a valid output.
423
1/2
✓ Branch 0 taken 1788 times.
✗ Branch 1 not taken.
1788 runningMediaLock.lock();
424
1/2
✓ Branch 0 taken 1788 times.
✗ Branch 1 not taken.
1788 auto it = runningCommandMedia.find(fn);
425
3/6
✓ Branch 0 taken 1788 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1788 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1788 times.
✗ Branch 5 not taken.
1788 if (it != runningCommandMedia.end() && it->second == dt) {
426
1/2
✓ Branch 0 taken 1788 times.
✗ Branch 1 not taken.
1788 runningCommandMedia.erase(it);
427
1/2
✓ Branch 0 taken 1788 times.
✗ Branch 1 not taken.
1788 LogDebug(VB_COMMAND, "Removed cached PlayData for file: \"%s\"\n", fn.c_str());
428 }
429 1788 runningMediaLock.unlock();
430
431
4/6
✓ Branch 0 taken 1788 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1788 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 1786 times.
3576 if (CommandManager::INSTANCE.HasPreset("MEDIA_STOPPED")) {
432 2 std::map<std::string, std::string> keywords;
433
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.
6 keywords["MEDIA_NAME"] = fn;
434
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 CommandManager::INSTANCE.TriggerPreset("MEDIA_STOPPED", keywords);
435 2 }
436
1/2
✓ Branch 0 taken 1788 times.
✗ Branch 1 not taken.
1788 delete dt;
437 1788 });
438 1792 }
439
440 1837 static std::string RegisterRunningMedia(const std::string& file, MediaOutputBase* media) {
441 1837 std::string filename = file;
442
1/2
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
1837 runningMediaLock.lock();
443
3/4
✓ Branch 0 taken 2441 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 604 times.
✓ Branch 3 taken 1837 times.
2441 while (runningCommandMedia.find(filename) != runningCommandMedia.end()) {
444
1/2
✓ Branch 0 taken 604 times.
✗ Branch 1 not taken.
604 filename += "_";
445 }
446
1/2
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
1837 runningCommandMedia[filename] = media;
447 1837 runningMediaLock.unlock();
448 1837 return filename;
449 ✗ }
450
451 #ifdef HAS_GSTREAMER
452 class GStreamerPlayData : public GStreamerOutput {
453 public:
454 GStreamerPlayData(const std::string& file, int l, int vol, int slot = 1,
455 const std::string& videoOut = "");
456 virtual ~GStreamerPlayData();
457 virtual void Stopped() override;
458 virtual void RequestStop() override;
459 12947 virtual bool NeedsSlotPump() const override { return true; }
460 // Declared first so it is destroyed last, remaining valid throughout ~GStreamerPlayData()
461 // even if the GStreamer bus fires a second Stopped() callback during our destruction.
462 std::atomic<bool> m_stoppedOnce{ false };
463 std::string filename;
464 int volumeAdjust = 0;
465 int streamSlot = 1;
466 MediaOutputStatus status;
467 // Names this output in a deferred stop. A pointer can't: by the time the
468 // stop runs, the output may have been freed and its address reused.
469 const uint64_t playId = NextPlayId();
470
471 private:
472 1837 static uint64_t NextPlayId() {
473 static std::atomic<uint64_t> next{ 1 };
474 1837 return next++;
475 }
476 };
477
478 1837 GStreamerPlayData::GStreamerPlayData(const std::string& file, int l, int vol, int slot,
479 1837 const std::string& videoOut) :
480 GStreamerOutput(file,
481
1/2
✓ Branch 0 taken 1104 times.
✗ Branch 1 not taken.
1104 (slot > 1 && slot <= StreamSlotManager::MAX_SLOTS)
482 1104 ? StreamSlotManager::Instance().GetStatus(slot)
483 : &status,
484 videoOut, slot),
485
1/2
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
1837 filename(file),
486 1837 volumeAdjust(vol),
487
2/2
✓ Branch 0 taken 1104 times.
✓ Branch 1 taken 733 times.
4778 streamSlot(slot) {
488 1837 SetLoopCount(l > 0 ? l - 1 : 0);
489
1/2
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
1837 SetVolumeAdjustment(vol);
490 // Media played by command runs beside the show; it must not pace a
491 // running sequence (a companion keeps in step through the slot manager's
492 // sync instead).
493 1837 SetDrivesShowClock(false);
494
1/2
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
1837 filename = RegisterRunningMedia(file, this);
495 1837 }
496
497 7160 GStreamerPlayData::~GStreamerPlayData() {
498 // Detach while this class is still whole. ~GStreamerOutput's Close() runs
499 // only after filename, status and the slot-1 status target are destroyed
500 // and the vtable no longer reaches RequestStop()/Stopped() here, yet until
501 // that Close() the slot manager (under its lock, from an HTTP thread) and
502 // a late bus message could still call them. Close() is idempotent, so the
503 // base destructor's own call is then a no-op.
504 //
505 // Whoever is deleting this owns it, so the Stopped() that Close() can
506 // reach must not hand it to RemoveRunningMedia's deferred delete as well
507 // (a second free, and a MEDIA_STOPPED for media that may never have
508 // played): a failed Start() can leave a pipeline for Close() to stop.
509 3580 m_stoppedOnce.store(true);
510 3580 Close();
511 7160 }
512
513 // Stops this media from the main loop, through runningCommandMedia like every
514 // other stop of command media. Called by the slot manager while it holds its
515 // own lock, so the stop itself -- up to half a second for video -- must not
516 // run here.
517 53 void GStreamerPlayData::RequestStop() {
518 53 uint64_t id = playId;
519
4/10
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 53 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 53 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
53 Timers::INSTANCE.addTimer("PlayMediaStop" + std::to_string(id), GetTimeMS(), [id]() {
520
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 std::lock_guard<std::recursive_mutex> lock(runningMediaLock);
521
2/2
✓ Branch 0 taken 49 times.
✓ Branch 1 taken 48 times.
97 for (const auto& item : runningCommandMedia) {
522
1/2
✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
49 GStreamerPlayData* gpd = dynamic_cast<GStreamerPlayData*>(item.second);
523
3/4
✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 45 times.
49 if (gpd && gpd->playId == id) {
524
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3 times.
4 if (!gpd->m_stoppedOnce.load()) {
525
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 gpd->Stop();
526 }
527 4 return;
528 }
529 }
530
2/2
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 4 times.
52 });
531 53 }
532
533 1915 void GStreamerPlayData::Stopped() {
534
2/2
✓ Branch 0 taken 123 times.
✓ Branch 1 taken 1792 times.
1915 if (m_stoppedOnce.exchange(true)) {
535 123 return;
536 }
537 1792 GStreamerOutput::Stopped();
538 1792 RemoveRunningMedia(filename, this);
539 }
540
541 // Stop the command-started media playing on `slot`. Returns the name it is
542 // registered under, or "" if there was none.
543 //
544 // This is the only safe way to stop command media by slot. The slot manager
545 // hands out a raw pointer with nothing keeping it alive, and a stopped
546 // GStreamerPlayData is deleted on the main thread 1ms after Stopped() - so two
547 // requests that both fetched the slot's output could see one stop it, the main
548 // thread free it, and the other still be inside its Stop(). Here the stop runs
549 // under runningMediaLock, which RemoveRunningMedia()'s deferred delete must take
550 // before freeing the object, so it stays alive for the whole call, and
551 // concurrent requests take turns.
552 //
553 // Entries already stopped stay in the map until that delete runs; skip them, or
554 // a dead entry is "stopped" and the live stream on the slot keeps playing.
555 5110 static std::string StopCommandMediaInSlot(int slot) {
556
1/2
✓ Branch 0 taken 5110 times.
✗ Branch 1 not taken.
5110 std::lock_guard<std::recursive_mutex> lock(runningMediaLock);
557
2/2
✓ Branch 0 taken 2628 times.
✓ Branch 1 taken 3960 times.
6588 for (const auto& item : runningCommandMedia) {
558
1/2
✓ Branch 0 taken 2628 times.
✗ Branch 1 not taken.
2628 GStreamerPlayData* gpd = dynamic_cast<GStreamerPlayData*>(item.second);
559
7/8
✓ Branch 0 taken 2628 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1209 times.
✓ Branch 3 taken 1419 times.
✓ Branch 4 taken 1150 times.
✓ Branch 5 taken 59 times.
✓ Branch 6 taken 1150 times.
✓ Branch 7 taken 1478 times.
2628 if (gpd && gpd->streamSlot == slot && !gpd->m_stoppedOnce.load()) {
560
1/4
✓ Branch 0 taken 1150 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1150 std::string name = item.first;
561
1/2
✓ Branch 0 taken 1150 times.
✗ Branch 1 not taken.
1150 gpd->Stop();
562
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1150 times.
1150 return name;
563 ✗ }
564 }
565
1/2
✓ Branch 0 taken 3960 times.
✗ Branch 1 not taken.
7920 return "";
566 5110 }
567 #endif
568
569 // Runtime backend selection for "Play Media" command
570 1837 static bool UseGStreamerForPlayMedia() {
571 #ifdef HAS_GSTREAMER
572 // GStreamer is the sole media player for all backends
573 1837 return true;
574 #endif
575 return false;
576 }
577
578 1888 PlayMediaCommand::PlayMediaCommand() :
579
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("Play Media", "Plays the media in the background") {
580
7/14
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1888 times.
✗ Branch 13 not taken.
15104 args.push_back(CommandArg("media", "string", "Media").setContentListUrl("api/media"));
581
7/14
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1888 times.
✗ Branch 13 not taken.
15104 args.push_back(CommandArg("loop", "int", "Loop Count").setDefaultValue(std::string("1")).setRange(1, 100));
582
7/14
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1888 times.
✗ Branch 13 not taken.
15104 args.push_back(CommandArg("volume", "int", "Volume Adjust").setDefaultValue(std::string("0")).setRange(-100, 100));
583
7/14
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1888 times.
✗ Branch 13 not taken.
15104 args.push_back(CommandArg("slot", "int", "Stream Slot").setDefaultValue(std::string("1")).setRange(1, StreamSlotManager::MAX_SLOTS));
584 // Video output for this stream. Empty (the default) keeps the historical
585 // audio-only behavior; setting it to a connector (e.g. "HDMI-A-2") lets a
586 // non-primary slot drive its own display, which is what makes independent
587 // video-per-output possible.
588
7/14
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1888 times.
✗ Branch 13 not taken.
15104 args.push_back(CommandArg("videoOut", "string", "Video Output", true).setContentListUrl("api/options/PlaylistVideoOutput"));
589 // Opt-in: follow the playlist's primary media clock instead of free-running.
590 // Wanted when this stream belongs to the show (a second language feed on
591 // another audio output); not wanted for unrelated media on a second display.
592
6/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
11328 args.push_back(CommandArg("sync", "bool", "Sync To Show", true).setDefaultValue("false"));
593 1888 }
594 1837 std::unique_ptr<Command::Result> PlayMediaCommand::run(const std::vector<std::string>& args) {
595
1/2
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
1837 int loop = args.size() > 1 ? std::atoi(args[1].c_str()) : 1;
596 1837 int volAdjust = 0;
597
1/2
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
1837 if (args.size() > 2) {
598 1837 volAdjust = std::atoi(args[2].c_str());
599 }
600 1837 int slot = 1;
601
1/2
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
1837 if (args.size() > 3) {
602 1837 slot = std::atoi(args[3].c_str());
603
2/4
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1837 times.
1837 if (slot < 1 || slot > StreamSlotManager::MAX_SLOTS) slot = 1;
604 }
605
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1837 times.
1837 if (loop < 1) {
606 ✗ loop = 1;
607 }
608 1837 std::string videoOut;
609
2/2
✓ Branch 0 taken 66 times.
✓ Branch 1 taken 1771 times.
1837 if (args.size() > 4) {
610 // An omitted video output stays "" (audio only); "--Default--" means
611 // the same display a playlist's default would use. It used to be
612 // passed through as-is and taken for a pixel overlay model name.
613
6/10
✓ Branch 0 taken 66 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 65 times.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 65 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 66 times.
✗ Branch 9 not taken.
66 videoOut = args[4] == "--Default--" ? ResolveVideoOutput(args[4]) : args[4];
614 }
615 1837 bool syncToShow = false;
616
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 1830 times.
1837 if (args.size() > 5) {
617
5/8
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 4 times.
7 syncToShow = (args[5] == "true" || args[5] == "1");
618 }
619
620 // A slot holds one stream at a time. Without this, a repeated "Play
621 // Media" on the same slot (e.g. pressing play twice) would build a
622 // second GStreamer pipeline that reuses the same PipeWire node name
623 // (StreamSlotManager::GetNodeName() is keyed only by slot number), while
624 // the first pipeline is left running -- StreamSlotManager only ever
625 // tracks the newest output, so the original becomes untracked and
626 // unstoppable via any command, and the two streams mix audibly. Stop
627 // whatever already holds the slot first so only one stream ever owns it.
628 #ifdef HAS_GSTREAMER
629 // Stop-then-start is one step per slot. Two Play Media on the same slot at
630 // once (two HTTP or MQTT requests) both found it empty, both started, and
631 // the slot went on tracking only the later stream while the two mixed.
632 // Recursive: a preset reached from Start() may play onto the same slot on
633 // this thread. Released before the MEDIA_STARTED preset below.
634 static std::recursive_mutex slotStartLocks[StreamSlotManager::MAX_SLOTS + 1];
635
1/2
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
1837 std::unique_lock<std::recursive_mutex> slotStartLock(slotStartLocks[slot]);
636
1/2
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
1837 std::string stoppedMedia = StopCommandMediaInSlot(slot);
637
2/2
✓ Branch 0 taken 746 times.
✓ Branch 1 taken 1091 times.
1837 if (!stoppedMedia.empty()) {
638
1/2
✓ Branch 0 taken 746 times.
✗ Branch 1 not taken.
746 LogInfo(VB_COMMAND, "Play Media: slot %d was playing %s, stopped it before starting %s\n", slot, stoppedMedia.c_str(), args[0].c_str());
639
4/6
✓ Branch 0 taken 1091 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1091 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 23 times.
✓ Branch 5 taken 1068 times.
1091 } else if (StreamSlotManager::Instance().RequestStop(slot)) {
640 // Not command media - a playlist-owned stream. Its owner stops and
641 // frees it on its own thread; stopping it from here raced the owner's
642 // Process() and its delete. It stops on the owner's next pass.
643
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 LogInfo(VB_COMMAND, "Play Media: slot %d already active, asked its owner to stop it before starting %s\n", slot, args[0].c_str());
644 }
645 #endif
646
647 1837 MediaOutputBase* out = nullptr;
648 #ifdef HAS_GSTREAMER
649
1/2
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
1837 if (UseGStreamerForPlayMedia()) {
650
3/8
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1837 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1837 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
1837 out = new GStreamerPlayData(args[0], loop, volAdjust, slot, videoOut);
651
1/2
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
1837 LogInfo(VB_COMMAND, "Play Media using GStreamer backend for: %s (slot %d)\n", args[0].c_str(), slot);
652 }
653 #endif
654
655
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1837 times.
1837 if (!out) {
656 ✗ return std::make_unique<Command::ErrorResult>("No media backend available");
657 }
658
659
3/4
✓ Branch 0 taken 1837 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1835 times.
1837 if (!out->Start()) {
660 // GStreamerPlayData's ctor self-registers into runningCommandMedia
661 // (RegisterRunningMedia()). Normally the entry is removed when
662 // GStreamerOutput::Stopped() fires, but every failure path inside
663 // GStreamerOutput::Start() returns before the GStreamer bus
664 // watch/state-change thread is ever installed, so Stopped() (and
665 // thus RemoveRunningMedia()) will never be called. Without this,
666 // the object -- and its map entry -- leaks forever. Deregister
667 // directly and delete. Start() can fail with a pipeline already
668 // built (gst_parse_launch() returns one alongside a recoverable
669 // error), which the destructor's Close() stops; the destructor
670 // keeps that stop from scheduling a deferred delete of its own.
671
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_COMMAND, "Play Media: could not start media %s\n", args[0].c_str());
672
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 WarningHolder::AddWarningTimeout(60, 30, "Could not start media " + args[0]);
673
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 runningMediaLock.lock();
674
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 for (auto it = runningCommandMedia.begin(); it != runningCommandMedia.end(); ++it) {
675
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (it->second == out) {
676
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 runningCommandMedia.erase(it);
677 2 break;
678 }
679 }
680 2 runningMediaLock.unlock();
681
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 delete out;
682
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 return std::make_unique<Command::ErrorResult>("Could not start media " + args[0]);
683 }
684
685 // Set after a successful Start(): the slot registers its active output
686 // during Start(), so flagging earlier would target a slot that isn't
687 // populated yet.
688
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1832 times.
1835 if (syncToShow) {
689
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 StreamSlotManager::Instance().SetSyncToMaster(slot, true);
690 }
691 #ifdef HAS_GSTREAMER
692
1/2
✓ Branch 0 taken 1835 times.
✗ Branch 1 not taken.
1835 slotStartLock.unlock();
693 #endif
694
695 1835 std::map<std::string, std::string> keywords;
696
3/6
✓ Branch 0 taken 1835 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1835 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1835 times.
✗ Branch 5 not taken.
3670 keywords["MEDIA_NAME"] = args[0];
697
2/4
✓ Branch 0 taken 1835 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1835 times.
✗ Branch 3 not taken.
5505 keywords["STREAM_SLOT"] = std::to_string(slot);
698
4/6
✓ Branch 0 taken 1835 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1835 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 1833 times.
3670 if (CommandManager::INSTANCE.HasPreset("MEDIA_STARTED")) {
699
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 CommandManager::INSTANCE.TriggerPreset("MEDIA_STARTED", keywords);
700 }
701
702
1/2
✓ Branch 0 taken 1835 times.
✗ Branch 1 not taken.
1835 return std::make_unique<Command::Result>("Playing");
703 1837 }
704
705 1888 StopMediaCommand::StopMediaCommand() :
706
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("Stop Media", "Stops the media in the background started via a command") {
707
7/14
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1888 times.
✗ Branch 13 not taken.
15104 args.push_back(CommandArg("media", "string", "Media").setContentListUrl("api/media"));
708 1888 }
709 424 std::unique_ptr<Command::Result> StopMediaCommand::run(const std::vector<std::string>& args) {
710
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 424 times.
424 if (args.size() == 0) {
711 ✗ return std::make_unique<Command::Result>("Invalid Number of Arguments");
712 }
713
1/2
✓ Branch 0 taken 424 times.
✗ Branch 1 not taken.
424 std::string rc = "Media Not Running";
714
1/2
✓ Branch 0 taken 424 times.
✗ Branch 1 not taken.
424 runningMediaLock.lock();
715 424 MediaOutputBase* media_ptr = nullptr;
716 try {
717
2/2
✓ Branch 0 taken 134 times.
✓ Branch 1 taken 290 times.
424 media_ptr = runningCommandMedia.at(args[0]);
718
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 290 times.
290 } catch (const std::out_of_range& oor) {
719
1/2
✓ Branch 0 taken 290 times.
✗ Branch 1 not taken.
290 LogInfo(VB_COMMAND, "Media: \"%s\" is not running so can't stop via command\n", args[0].c_str());
720 290 media_ptr = nullptr;
721 290 }
722
2/2
✓ Branch 0 taken 134 times.
✓ Branch 1 taken 290 times.
424 if (media_ptr) {
723
1/2
✓ Branch 0 taken 134 times.
✗ Branch 1 not taken.
134 media_ptr->Stop();
724
1/2
✓ Branch 0 taken 134 times.
✗ Branch 1 not taken.
134 rc = "Stopped";
725 }
726 424 runningMediaLock.unlock();
727
728
1/2
✓ Branch 0 taken 424 times.
✗ Branch 1 not taken.
424 return std::make_unique<Command::Result>(rc);
729 424 }
730
731 1888 StopAllMediaCommand::StopAllMediaCommand() :
732
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("Stop All Media", "Stops all running media that was created via a Command") {
733 1888 }
734 293 std::unique_ptr<Command::Result> StopAllMediaCommand::run(const std::vector<std::string>& args) {
735 293 runningMediaLock.lock();
736
2/2
✓ Branch 0 taken 418 times.
✓ Branch 1 taken 293 times.
711 for (const auto& item : runningCommandMedia) {
737
1/2
✓ Branch 0 taken 418 times.
✗ Branch 1 not taken.
418 LogDebug(VB_COMMAND, "Stopping: \"%s\"\n", item.first.c_str());
738
1/2
✓ Branch 0 taken 418 times.
✗ Branch 1 not taken.
418 if (item.second) {
739
1/2
✓ Branch 0 taken 418 times.
✗ Branch 1 not taken.
418 item.second->Stop();
740 }
741 }
742 293 runningMediaLock.unlock();
743 293 return std::make_unique<Command::Result>("Stopped");
744 }
745
746 1888 StopMediaSlotCommand::StopMediaSlotCommand() :
747
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("Stop Media Slot", "Stop playback on a specific stream slot (1-5)") {
748
6/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
11328 args.push_back(CommandArg("slot", "int", "Stream Slot").setRange(1, StreamSlotManager::MAX_SLOTS).setDefaultValue("1"));
749 1888 }
750 3273 std::unique_ptr<Command::Result> StopMediaSlotCommand::run(const std::vector<std::string>& args) {
751
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3273 times.
3273 if (args.empty()) {
752 ✗ return std::make_unique<Command::ErrorResult>("No slot specified");
753 }
754 3273 int slot = std::atoi(args[0].c_str());
755
2/4
✓ Branch 0 taken 3273 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3273 times.
3273 if (slot < 1 || slot > StreamSlotManager::MAX_SLOTS) {
756 ✗ return std::make_unique<Command::ErrorResult>("Invalid slot number (1-" + std::to_string(StreamSlotManager::MAX_SLOTS) + ")");
757 }
758
759 // Try command-started media first (matches by slot)
760 3273 std::string stoppedFile;
761 #ifdef HAS_GSTREAMER
762
1/2
✓ Branch 0 taken 3273 times.
✗ Branch 1 not taken.
3273 stoppedFile = StopCommandMediaInSlot(slot);
763 #endif
764
765
2/2
✓ Branch 0 taken 404 times.
✓ Branch 1 taken 2869 times.
3273 if (!stoppedFile.empty()) {
766
1/2
✓ Branch 0 taken 404 times.
✗ Branch 1 not taken.
404 LogInfo(VB_COMMAND, "Stop Media Slot %d: stopped command media '%s'\n", slot, stoppedFile.c_str());
767
2/4
✓ Branch 0 taken 404 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 404 times.
✗ Branch 3 not taken.
404 return std::make_unique<Command::Result>("Stopped slot " + std::to_string(slot));
768 }
769
770 // Fall back to StreamSlotManager (catches playlist-started media), which
771 // asks the owner to stop it on its own thread -- see Play Media above.
772
4/6
✓ Branch 0 taken 2869 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2869 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 26 times.
✓ Branch 5 taken 2843 times.
2869 if (StreamSlotManager::Instance().RequestStop(slot)) {
773
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 LogInfo(VB_COMMAND, "Stop Media Slot %d: asked the slot's owner to stop it\n", slot);
774 // Not "Stopped": the owner stops it on its next pass, and only if it
775 // is still processing that output.
776
2/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
26 return std::make_unique<Command::Result>("Stop requested for slot " + std::to_string(slot));
777 }
778
779
3/6
✓ Branch 0 taken 2843 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2843 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2843 times.
✗ Branch 5 not taken.
2843 return std::make_unique<Command::Result>("Slot " + std::to_string(slot) + " not playing");
780 3273 }
781
782 1888 SetSlotVolumeCommand::SetSlotVolumeCommand() :
783
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("Set Slot Volume", "Sets the volume on a specific stream slot (1-5)") {
784
6/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
11328 args.push_back(CommandArg("slot", "int", "Stream Slot").setRange(1, StreamSlotManager::MAX_SLOTS).setDefaultValue("1"));
785
6/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1888 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1888 times.
✗ Branch 11 not taken.
11328 args.push_back(CommandArg("volume", "int", "Volume").setRange(0, 100).setDefaultValue("70"));
786 1888 }
787 70910 std::unique_ptr<Command::Result> SetSlotVolumeCommand::run(const std::vector<std::string>& args) {
788
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 70909 times.
70910 if (args.size() < 2) {
789
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 return std::make_unique<Command::ErrorResult>("Requires slot and volume arguments");
790 }
791 70909 int slot = std::atoi(args[0].c_str());
792 70909 int volume = std::atoi(args[1].c_str());
793
4/4
✓ Branch 0 taken 70907 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 70906 times.
70909 if (slot < 1 || slot > StreamSlotManager::MAX_SLOTS) {
794
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 return std::make_unique<Command::ErrorResult>("Invalid slot number (1-" + std::to_string(StreamSlotManager::MAX_SLOTS) + ")");
795 }
796
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 70905 times.
70906 if (volume < 0) volume = 0;
797
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 70905 times.
70906 if (volume > 100) volume = 100;
798
799 70906 bool deferred = false;
800
3/6
✓ Branch 0 taken 70906 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 70906 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 70906 times.
✗ Branch 5 not taken.
70906 if (StreamSlotManager::Instance().SetSlotVolume(slot, volume, &deferred)) {
801
2/2
✓ Branch 0 taken 7893 times.
✓ Branch 1 taken 63013 times.
70906 if (deferred) {
802
1/2
✓ Branch 0 taken 7893 times.
✗ Branch 1 not taken.
15786 return std::make_unique<Command::Result>("Slot " + std::to_string(slot) +
803
2/4
✓ Branch 0 taken 7893 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7893 times.
✗ Branch 3 not taken.
7893 " is idle; the volume applies when it next plays");
804 }
805
2/4
✓ Branch 0 taken 63013 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 63013 times.
✗ Branch 3 not taken.
63013 return std::make_unique<Command::Result>("Volume set on slot " + std::to_string(slot));
806 }
807 ✗ return std::make_unique<Command::ErrorResult>("Slot volume is not available");
808 }
809
810 1888 MediaSlotStatusCommand::MediaSlotStatusCommand() :
811
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("Media Slot Status", "Returns the current playback status of all stream slots") {
812 1888 }
813 5909 std::unique_ptr<Command::Result> MediaSlotStatusCommand::run(const std::vector<std::string>& args) {
814
2/4
✓ Branch 0 taken 5909 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5909 times.
✗ Branch 3 not taken.
5909 Json::Value status = StreamSlotManager::Instance().GetAllSlotsStatus();
815
1/2
✓ Branch 0 taken 5909 times.
✗ Branch 1 not taken.
5909 Json::StreamWriterBuilder builder;
816
3/6
✓ Branch 0 taken 5909 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5909 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5909 times.
✗ Branch 5 not taken.
17727 builder["indentation"] = "";
817
1/2
✓ Branch 0 taken 5909 times.
✗ Branch 1 not taken.
5909 std::string result = Json::writeString(builder, status);
818
1/2
✓ Branch 0 taken 5909 times.
✗ Branch 1 not taken.
11818 return std::make_unique<Command::Result>(result);
819 5909 }
820 #endif
821
822 #ifdef HAS_AES67_GSTREAMER
823 #include "mediaoutput/AES67Manager.h"
824
825 1888 AES67ApplyCommand::AES67ApplyCommand() :
826
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("AES67 Apply", "Apply AES67 audio-over-IP configuration") {
827 1888 }
828 89 std::unique_ptr<Command::Result> AES67ApplyCommand::run(const std::vector<std::string>& args) {
829
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 if (AES67Manager::INSTANCE.ApplyConfig()) {
830 89 return std::make_unique<Command::Result>("AES67 config applied");
831 }
832 ✗ return std::make_unique<Command::ErrorResult>("AES67 apply failed");
833 }
834
835 1888 AES67CleanupCommand::AES67CleanupCommand() :
836
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("AES67 Cleanup", "Stop all AES67 pipelines and clean up") {
837 1888 }
838 1 std::unique_ptr<Command::Result> AES67CleanupCommand::run(const std::vector<std::string>& args) {
839 1 AES67Manager::INSTANCE.Cleanup();
840 1 return std::make_unique<Command::Result>("AES67 cleaned up");
841 }
842
843 1888 AES67TestCommand::AES67TestCommand() :
844
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("AES67 Test", "Run AES67 subsystem self-test") {
845 1888 }
846 1 std::unique_ptr<Command::Result> AES67TestCommand::run(const std::vector<std::string>& args) {
847
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 auto tests = AES67Manager::INSTANCE.RunSelfTest();
848 1 int passed = 0, failed = 0;
849 1 std::string report;
850
2/2
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 1 times.
24 for (const auto& t : tests) {
851
3/4
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
23 report += (t.passed ? "PASS" : "FAIL");
852
5/10
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 23 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 23 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 23 times.
✗ Branch 9 not taken.
23 report += ": " + t.testName + " - " + t.message + "\n";
853
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 9 times.
23 if (t.passed) passed++; else failed++;
854 }
855
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 report += "\nTotal: " + std::to_string(tests.size()) +
856
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
4 " | Passed: " + std::to_string(passed) +
857
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 " | Failed: " + std::to_string(failed);
858
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (failed == 0) {
859 ✗ return std::make_unique<Command::Result>(report);
860 }
861
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 return std::make_unique<Command::ErrorResult>(report);
862 1 }
863
864 1888 ApplyRoutingPresetCommand::ApplyRoutingPresetCommand() :
865
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("Pipewire Apply Routing Preset", "Live-apply a saved PipeWire routing preset without stopping playback") {
866
4/8
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
13216 args.push_back(CommandArg("Preset", "string", "Routing Preset Name")
867
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
3776 .setContentListUrl("api/pipewire/audio/routing/presets/names"));
868 1888 }
869 1 std::unique_ptr<Command::Result> ApplyRoutingPresetCommand::run(const std::vector<std::string>& args) {
870
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 (args.empty() || args[0].empty()) {
871 ✗ return std::make_unique<Command::ErrorResult>("No preset name specified");
872 }
873 // Build the API call — POST to the live-apply endpoint with the preset name
874
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::string url = "http://localhost/api/pipewire/audio/routing/presets/live-apply";
875 // Serialized, not concatenated: a preset name with a quote or backslash
876 // in it made invalid JSON (or a different request).
877
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 Json::Value body;
878
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 body["name"] = args[0];
879
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::string postData = SaveJsonToString(body);
880
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
5 std::vector<std::string> curlArgs = { url, "POST", postData };
881
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 return std::make_unique<CURLResult>(curlArgs);
882
3/10
✓ 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.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
3 }
883 #endif
884
885 #ifdef HAS_OPUS_RTP_GSTREAMER
886 #include "mediaoutput/OpusRTPManager.h"
887
888 1888 OpusRTPApplyCommand::OpusRTPApplyCommand() :
889
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("Opus RTP Apply", "Apply Opus RTP audio streaming configuration") {
890 1888 }
891 58 std::unique_ptr<Command::Result> OpusRTPApplyCommand::run(const std::vector<std::string>& args) {
892
2/2
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 1 times.
58 if (OpusRTPManager::INSTANCE.ApplyConfig()) {
893 57 return std::make_unique<Command::Result>("Opus RTP config applied");
894 }
895
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 return std::make_unique<Command::ErrorResult>("Opus RTP apply failed");
896 }
897
898 1888 OpusRTPCleanupCommand::OpusRTPCleanupCommand() :
899
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
7552 Command("Opus RTP Cleanup", "Stop all Opus RTP pipelines and clean up") {
900 1888 }
901 1 std::unique_ptr<Command::Result> OpusRTPCleanupCommand::run(const std::vector<std::string>& args) {
902 1 OpusRTPManager::INSTANCE.Cleanup();
903 1 return std::make_unique<Command::Result>("Opus RTP cleaned up");
904 }
905 #endif
906