GCC Code Coverage Report


Directory: ./
File: mediaoutput/AudioLevelMonitor.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 140 165 84.8%
Functions: 7 7 100.0%
Branches: 151 300 50.3%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2026 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "AudioLevelMonitor.h"
16
17 #ifdef HAS_AUDIO_LEVEL_MONITOR
18
19 #include <algorithm>
20 #include <chrono>
21 #include <cmath>
22
23 #include "../fpp-json.h"
24 #include "common_mini.h"
25 #include "log.h"
26
27 AudioLevelMonitor AudioLevelMonitor::INSTANCE;
28
29 // How often the level element reports, and therefore the ceiling on how fast a
30 // meter can move. 100ms is well below what reads as laggy and keeps the message
31 // traffic trivial.
32 static constexpr int LEVEL_INTERVAL_NS = 100000000;
33
34 // Floor of the meter scale. Programme material sits well above this; anything
35 // quieter is indistinguishable from silence on a bar a few pixels tall.
36 static constexpr double METER_FLOOR_DB = -60.0;
37
38 458 AudioLevelMonitor::~AudioLevelMonitor() {
39 458 Shutdown();
40 458 }
41
42 // Builds and starts a meter's pipeline. Never called with m_lock held: it
43 // talks to the PipeWire daemon (pipewiresrc connects as the state rises), and
44 // the status websocket reads levels under m_lock.
45 13 bool AudioLevelMonitor::BuildMeter(const std::string& node, bool isSink, Meter& m) {
46 // Node names are validated by the callers that accept them from outside
47 // (the HTTP endpoint), but this builds a pipeline description string, so
48 // anything unexpected is a reason to refuse rather than to quote around.
49
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 13 times.
26 if (node.empty() ||
50 13 node.find_first_not_of("abcdefghijklmnopqrstuvwxyz"
51 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
52 "0123456789_.-") != std::string::npos) {
53 ✗ LogWarn(VB_MEDIAOUT, "AudioLevelMonitor: refusing node with unexpected name: %s\n", node.c_str());
54 ✗ return false;
55 }
56
57 // Capturing what a sink is playing needs stream.capture.sink; a playback
58 // stream is captured by targeting it directly and must NOT have it set.
59 //
60 // Getting this wrong is not visibly an error: pipewiresrc cannot resolve
61 // the target, silently falls back to the default sink, and reports a
62 // plausible level for entirely the wrong node. That is what made every
63 // meter read the same signal and move together. (There is no ".monitor"
64 // node to target either -- that name is a PulseAudio compatibility
65 // construct and matches nothing in the PipeWire graph.)
66 // GStreamer may not have been initialised yet: metering can be switched on
67 // before anything has played, and gst_parse_launch() segfaults rather than
68 // failing when it is called first. Same guard AES67Manager and
69 // OpusRTPManager use.
70
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (!gst_is_initialized()) {
71 ✗ gst_init(nullptr, nullptr);
72 }
73
74 // The stream-properties value is a GstStructure and must be quoted, or the
75 // comma inside it terminates the property and the rest is parsed as another
76 // element.
77
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 std::string desc = "pipewiresrc target-object=" + node;
78
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 4 times.
13 if (isSink) {
79
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 desc += " stream-properties=\"props,stream.capture.sink=true\"";
80 }
81 13 desc += " ! audioconvert ! audio/x-raw,channels=1,rate=8000 ! " +
82
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
65 std::string("level interval=") + std::to_string(LEVEL_INTERVAL_NS) +
83
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
13 " post-messages=true ! fakesink sync=false";
84
85 13 GError* err = nullptr;
86
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 GstElement* pipeline = gst_parse_launch(desc.c_str(), &err);
87
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (!pipeline || err) {
88 ✗ LogWarn(VB_MEDIAOUT, "AudioLevelMonitor: could not build meter for %s: %s\n",
89 node.c_str(), err ? err->message : "unknown error");
90 ✗ if (err) {
91 ✗ g_error_free(err);
92 }
93 ✗ if (pipeline) {
94 ✗ gst_object_unref(pipeline);
95 }
96 ✗ return false;
97 }
98
99 13 m.isSink = isSink;
100 13 m.pipeline = pipeline;
101
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 m.bus = gst_element_get_bus(pipeline);
102
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 m.updatedMS = GetMonotonicTimeMS();
103
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 gst_element_set_state(pipeline, GST_STATE_PLAYING);
104
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 LogDebug(VB_MEDIAOUT, "AudioLevelMonitor: metering %s\n", node.c_str());
105 13 return true;
106 13 }
107
108 // Stops and frees a meter taken out of m_meters. Never called with m_lock
109 // held, for the same reason as BuildMeter().
110 13 void AudioLevelMonitor::DestroyMeter(const std::string& node, Meter& m) {
111
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 if (m.bus) {
112 13 gst_object_unref(m.bus);
113 13 m.bus = nullptr;
114 }
115
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 if (m.pipeline) {
116 13 gst_element_set_state(m.pipeline, GST_STATE_NULL);
117 13 gst_object_unref(m.pipeline);
118 13 m.pipeline = nullptr;
119 }
120 13 LogDebug(VB_MEDIAOUT, "AudioLevelMonitor: stopped metering %s\n", node.c_str());
121 13 }
122
123 12 void AudioLevelMonitor::Subscribe(const std::vector<Target>& nodes, int ttlMs) {
124 // Bounded: this is reachable from the web UI, and each entry is a real
125 // pipeline against a real audio device.
126 12 std::map<std::string, bool> wanted;
127
2/2
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 12 times.
33 for (const auto& t : nodes) {
128
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (t.name.empty()) {
129 ✗ continue;
130 }
131
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 21 times.
21 if (wanted.size() >= 12 && wanted.find(t.name) == wanted.end()) {
132 ✗ break;
133 }
134
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 wanted[t.name] = t.isSink;
135 }
136
137 // Decide under m_lock; build and tear down pipelines outside it (see
138 // BuildMeter()).
139 12 std::map<std::string, Meter> dead;
140 12 std::vector<std::pair<std::string, bool>> toStart;
141 {
142
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 std::lock_guard<std::mutex> lk(m_lock);
143
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 12 times.
21 for (auto it = m_meters.begin(); it != m_meters.end();) {
144
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 auto w = wanted.find(it->first);
145 // A node whose kind changed has to be rebuilt: the capture mode is
146 // fixed when the pipeline is created.
147
5/6
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 8 times.
9 if (w == wanted.end() || w->second != it->second.isSink) {
148
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 dead[it->first] = it->second;
149
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 it = m_meters.erase(it);
150 } else {
151 8 ++it;
152 }
153 }
154 // Already-running nodes are left as they are: a keepalive must not
155 // restart a pipeline and blank the meter every few seconds.
156
2/2
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 12 times.
33 for (const auto& kv : wanted) {
157
3/4
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 8 times.
21 if (m_meters.find(kv.first) == m_meters.end()) {
158
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
13 toStart.push_back(kv);
159 }
160 }
161 12 }
162
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 12 times.
13 for (auto& kv : dead) {
163
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 DestroyMeter(kv.first, kv.second);
164 }
165 12 dead.clear();
166
167 12 std::map<std::string, Meter> built;
168
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 12 times.
25 for (const auto& n : toStart) {
169 13 Meter m;
170
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
13 if (BuildMeter(n.first, n.second, m)) {
171
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 built[n.first] = m;
172 }
173 }
174
175 bool needThread;
176 {
177
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 std::lock_guard<std::mutex> lk(m_lock);
178
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 12 times.
25 for (auto& kv : built) {
179
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (m_meters.find(kv.first) != m_meters.end()) {
180 // Another subscribe started the same node meanwhile.
181 ✗ dead[kv.first] = kv.second;
182 } else {
183
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 m_meters[kv.first] = kv.second;
184 }
185 }
186
3/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 11 times.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
12 m_expiresMS = m_meters.empty() ? 0 : GetMonotonicTimeMS() + std::max(1000, ttlMs);
187 12 m_active.store(!m_meters.empty());
188 12 needThread = !m_meters.empty();
189 12 }
190
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
12 for (auto& kv : dead) {
191 ✗ DestroyMeter(kv.first, kv.second);
192 }
193
194 // Thread lifecycle is handled outside m_lock, and the previous thread is
195 // joined before a new one is assigned: the poll thread retires itself once
196 // the last meter goes away, and assigning over a still-joinable thread
197 // calls std::terminate() -- which aborted fppd the first time metering was
198 // switched on, off, and on again.
199
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 1 times.
12 if (needThread) {
200
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 std::lock_guard<std::mutex> tlk(m_threadLock);
201
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 4 times.
11 if (!m_running.load()) {
202
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 6 times.
7 if (m_thread.joinable()) {
203
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 m_thread.join();
204 }
205 7 m_running.store(true);
206
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 m_thread = std::thread(&AudioLevelMonitor::PollThread, this);
207 }
208 11 }
209 12 }
210
211 // Drains each pipeline's bus for level messages. A thread rather than bus
212 // callbacks so this owns no GLib main loop -- fppd has no shared one to attach
213 // to, and adding a second is a bigger commitment than a 20ms poll.
214 7 void AudioLevelMonitor::PollThread() {
215
2/2
✓ Branch 0 taken 977 times.
✓ Branch 1 taken 7 times.
984 while (m_running.load()) {
216 977 std::map<std::string, Meter> expired;
217 {
218
1/2
✓ Branch 0 taken 977 times.
✗ Branch 1 not taken.
977 std::lock_guard<std::mutex> lk(m_lock);
219
220 // Nobody has re-subscribed: tear everything down. This is what
221 // keeps a closed browser tab from metering forever. The
222 // pipelines are stopped below, once m_lock is released.
223
7/8
✓ Branch 0 taken 976 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 976 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 973 times.
✓ Branch 6 taken 3 times.
✓ Branch 7 taken 974 times.
977 if (!m_meters.empty() && GetMonotonicTimeMS() > m_expiresMS) {
224 3 expired.swap(m_meters);
225 3 m_active.store(false);
226 }
227
228
2/2
✓ Branch 0 taken 1752 times.
✓ Branch 1 taken 977 times.
2729 for (auto& kv : m_meters) {
229 1752 GstMessage* msg = nullptr;
230
1/2
✓ Branch 0 taken 1948 times.
✗ Branch 1 not taken.
1948 while ((msg = gst_bus_pop_filtered(kv.second.bus,
231
2/2
✓ Branch 0 taken 196 times.
✓ Branch 1 taken 1752 times.
1948 (GstMessageType)(GST_MESSAGE_ELEMENT | GST_MESSAGE_ERROR))) != nullptr) {
232
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 196 times.
196 if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ERROR) {
233 ✗ GError* err = nullptr;
234 ✗ gst_message_parse_error(msg, &err, nullptr);
235 ✗ LogWarn(VB_MEDIAOUT, "AudioLevelMonitor: %s: %s\n", kv.first.c_str(),
236 err ? err->message : "pipeline error");
237 ✗ if (err) {
238 ✗ g_error_free(err);
239 }
240 ✗ gst_message_unref(msg);
241 ✗ continue;
242 ✗ }
243
244
1/2
✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
196 const GstStructure* st = gst_message_get_structure(msg);
245
4/8
✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 196 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 196 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 196 times.
✗ Branch 7 not taken.
196 if (st && gst_structure_has_name(st, "level")) {
246 // The level element historically reported per-channel
247 // values as GValueArray (GLib) and newer GStreamer as
248 // GstValueArray. Modern GLib deprecates GValueArray in
249 // favour of GArray (G_TYPE_ARRAY), so check GArray first,
250 // then GstValueArray, then fall back to GValueArray via
251 // runtime lookup (avoids G_TYPE_VALUE_ARRAY deprecated
252 // macro which triggers "Deprecated pre-processor symbol"
253 // via _GLIB_GNUC_DO_PRAGMA(GCC warning) not suppressible
254 // by -Wdeprecated-declarations).
255
1/2
✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
196 const GValue* arr = gst_structure_get_value(st, "rms");
256 196 const GValue* v = nullptr;
257
7/14
✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 196 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 196 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 196 times.
✓ Branch 8 taken 196 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 196 times.
✗ Branch 12 not taken.
✓ Branch 13 taken 196 times.
196 if (arr && G_VALUE_HOLDS(arr, G_TYPE_ARRAY)) {
258 ✗ GArray* va = (GArray*)g_value_get_boxed(arr);
259 ✗ if (va && va->len > 0) {
260 ✗ v = &g_array_index(va, GValue, 0);
261 }
262
4/12
✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 196 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 196 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 196 times.
196 } else if (arr && GST_VALUE_HOLDS_ARRAY(arr) && gst_value_array_get_size(arr) > 0) {
263 ✗ v = gst_value_array_get_value(arr, 0);
264
1/2
✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
196 } else if (arr) {
265 static GType gvaType = 0;
266
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 190 times.
196 if (gvaType == 0) {
267
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 gvaType = g_type_from_name("GValueArray");
268 }
269
5/12
✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 196 times.
✓ Branch 4 taken 196 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 196 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 196 times.
✗ Branch 11 not taken.
196 if (gvaType != 0 && G_VALUE_HOLDS(arr, gvaType)) {
270
1/2
✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
196 GValueArray* va = (GValueArray*)g_value_get_boxed(arr);
271
2/4
✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 196 times.
✗ Branch 3 not taken.
196 if (va && va->n_values > 0) {
272 196 v = &va->values[0];
273 }
274 }
275 }
276
5/12
✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 196 times.
✓ Branch 4 taken 196 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 196 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 196 times.
✗ Branch 11 not taken.
196 if (v && G_VALUE_HOLDS_DOUBLE(v)) {
277
1/2
✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
196 kv.second.rmsDb = g_value_get_double(v);
278
1/2
✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
196 kv.second.updatedMS = GetMonotonicTimeMS();
279 }
280 }
281
1/2
✓ Branch 0 taken 196 times.
✗ Branch 1 not taken.
196 gst_message_unref(msg);
282 }
283 }
284
285
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 973 times.
977 if (m_meters.empty()) {
286 4 m_running.store(false);
287 }
288 977 }
289
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 977 times.
982 for (auto& kv : expired) {
290
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 DestroyMeter(kv.first, kv.second);
291 }
292
1/2
✓ Branch 0 taken 977 times.
✗ Branch 1 not taken.
977 std::this_thread::sleep_for(std::chrono::milliseconds(20));
293 977 }
294 7 }
295
296 166 Json::Value AudioLevelMonitor::GetLevels() {
297
1/2
✓ Branch 0 taken 166 times.
✗ Branch 1 not taken.
166 Json::Value out(Json::objectValue);
298
1/2
✓ Branch 0 taken 166 times.
✗ Branch 1 not taken.
166 std::lock_guard<std::mutex> lk(m_lock);
299
1/2
✓ Branch 0 taken 166 times.
✗ Branch 1 not taken.
166 long long now = GetMonotonicTimeMS();
300
2/2
✓ Branch 0 taken 290 times.
✓ Branch 1 taken 166 times.
456 for (const auto& kv : m_meters) {
301 // A pipeline that has stopped reporting (device gone, node removed)
302 // reads as silent rather than freezing at its last value.
303
2/2
✓ Branch 0 taken 197 times.
✓ Branch 1 taken 93 times.
290 double db = (now - kv.second.updatedMS) > 1000 ? METER_FLOOR_DB : kv.second.rmsDb;
304 290 double pct = ((db - METER_FLOOR_DB) / -METER_FLOOR_DB) * 100.0;
305
3/6
✓ Branch 0 taken 290 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 290 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 290 times.
✗ Branch 5 not taken.
290 out[kv.first] = (int)std::lround(std::clamp(pct, 0.0, 100.0));
306 }
307 166 return out;
308 166 }
309
310 2325 void AudioLevelMonitor::Shutdown() {
311 2325 m_running.store(false);
312 {
313
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 std::lock_guard<std::mutex> tlk(m_threadLock);
314
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2319 times.
2325 if (m_thread.joinable()) {
315
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 m_thread.join();
316 }
317 2325 }
318 2325 std::map<std::string, Meter> all;
319 {
320
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 std::lock_guard<std::mutex> lk(m_lock);
321 2325 all.swap(m_meters);
322 2325 m_active.store(false);
323 2325 }
324
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 2325 times.
2332 for (auto& kv : all) {
325
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 DestroyMeter(kv.first, kv.second);
326 }
327 2325 }
328
329 #endif
330