GCC Code Coverage Report


Directory: ./
File: mediaoutput/AudioLevelMonitor.h
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 2 2 100.0%
Functions: 2 2 100.0%
Branches: 0 0 -%

Line Branch Exec Source
1 #pragma once
2 /*
3 * This file is part of the Falcon Player (FPP) and is Copyright (C)
4 * 2013-2026 by the Falcon Player Developers.
5 *
6 * The Falcon Player (FPP) is free software, and is covered under
7 * multiple Open Source licenses. Please see the included 'LICENSES'
8 * file for descriptions of what files are covered by each license.
9 *
10 * This source file is covered under the LGPL v2.1 as described in the
11 * included LICENSE.LGPL file.
12 */
13
14 // AudioLevelMonitor -- live signal levels for PipeWire nodes.
15 //
16 // Feeds the mixer's meters. One GStreamer pipeline per metered node,
17 //
18 // pipewiresrc target-object=<node>.monitor ! audioconvert
19 // ! audio/x-raw,channels=1,rate=8000 ! level ! fakesink
20 //
21 // which is how a meter is supposed to be done: the pipeline stays open and the
22 // level element reports RMS on a timer. Measured on a Pi 4 that is ~3% of one
23 // core per node. The naive alternative -- sampling a node by starting a short
24 // capture each refresh -- pays the stream setup cost over and over, and cannot
25 // produce a usable reading anyway, since a burst short enough to be cheap is
26 // dominated by the ~250ms the capture takes to start.
27 //
28 // Metering is entirely on demand. Callers subscribe with a TTL and re-subscribe
29 // to keep it alive, so a browser that closes its tab (or crashes) stops costing
30 // anything within a few seconds without needing to tell us.
31
32 #if __has_include(<gst/gst.h>)
33 #define HAS_AUDIO_LEVEL_MONITOR
34
35 #include <gst/gst.h>
36
37 #include <atomic>
38 #include <map>
39 #include <mutex>
40 #include <set>
41 #include <string>
42 #include <thread>
43 #include <vector>
44
45 #include "fpp-json.h"
46
47 class AudioLevelMonitor {
48 public:
49 static AudioLevelMonitor INSTANCE;
50
51 // A node to meter. isSink distinguishes a sink (a group, a member's filter
52 // chain, an input bus) from a playback stream (fppd_stream_N): capturing a
53 // sink needs stream.capture.sink, and setting it on a stream instead makes
54 // pipewiresrc fall back to the default sink -- see BuildMeter().
55 struct Target {
56 std::string name;
57 bool isSink = true;
58 };
59
60 // Meter exactly these nodes for the next ttlMs. Pipelines for nodes no
61 // longer listed are torn down; nodes already running are left alone so a
62 // keepalive does not interrupt them.
63 void Subscribe(const std::vector<Target>& nodes, int ttlMs);
64
65 // { "<node>": <0-100>, ... } -- empty when nothing is subscribed, which is
66 // what makes this free on an idle system.
67 Json::Value GetLevels();
68
69 // True while any pipeline is running, so the WebSocket only pays for a
70 // faster broadcast while someone is actually watching meters.
71 959 bool Active() const { return m_active.load(); }
72
73 void Shutdown();
74
75 private:
76 2346 AudioLevelMonitor() = default;
77 ~AudioLevelMonitor();
78
79 struct Meter {
80 GstElement* pipeline = nullptr;
81 GstBus* bus = nullptr;
82 // Written by the bus poll, read by GetLevels().
83 double rmsDb = -100.0;
84 long long updatedMS = 0;
85 bool isSink = true;
86 };
87
88 bool BuildMeter(const std::string& node, bool isSink, Meter& m);
89 void DestroyMeter(const std::string& node, Meter& m);
90 void PollThread();
91
92 mutable std::mutex m_lock;
93 // Guards thread lifecycle only. Deliberately not m_lock: joining the poll
94 // thread while holding m_lock would deadlock, because the thread takes
95 // m_lock on every pass.
96 std::mutex m_threadLock;
97 std::map<std::string, Meter> m_meters;
98 long long m_expiresMS = 0;
99 std::atomic<bool> m_active{ false };
100 std::atomic<bool> m_running{ false };
101 std::thread m_thread;
102 };
103
104 #endif
105