GCC Code Coverage Report


Directory: ./
File: mediaoutput/PipeWireOutputStream.h
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 1 1 100.0%
Functions: 1 1 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 #include "GStreamerOut.h"
15
16 #ifdef HAS_GSTREAMER
17
18 #include <gst/gst.h>
19
20 #include <atomic>
21 #include <condition_variable>
22 #include <cstdint>
23 #include <deque>
24 #include <mutex>
25 #include <string>
26 #include <utility>
27
28 // A PipeWire playback stream that outlives the media played through it.
29 //
30 // Giving every track its own pipewiresink costs ~700 ms on an AM335x before the
31 // first sample reaches the card, and almost none of it is the card: opening a
32 // full-speed USB card directly takes ~25 ms. The rest is the new stream node
33 // appearing, WirePlumber linking it (~300 ms of policy on one slow core), the
34 // group and filter-chain nodes waking and the ALSA node starting. A sequence
35 // started alongside the media runs ahead of the sound for all of that.
36 //
37 // One stream per slot, opened ahead of need and held open between tracks, pays
38 // that once. A track fed into a stream that is already open reaches the graph
39 // in tens of milliseconds. Media pipelines feed it through an appsink (see
40 // AttachFeed()), converted to the stream's one fixed format (CapsFields()).
41 // While open and idle PipeWire plays silence, which keeps the card running.
42 //
43 // The stream presents exactly the node a per-track pipewiresink used to --
44 // node.name fppd_stream_N, same description, same target and stream
45 // properties -- so routing, input mixing and the graph view are unaffected.
46 class PipeWireOutputStream {
47 public:
48 // How long a stream stays open after its last track stops feeding it, and
49 // how far ahead of a scheduled playlist it is opened. Matches how long
50 // the channel output thread keeps running after output goes idle.
51 static constexpr int LINGER_MS = 5000;
52
53 // The slot's stream, or nullptr for a slot outside 1..MAX_SLOTS or when
54 // PipeWire is not the media backend.
55 static PipeWireOutputStream* ForSlot(int slot);
56
57 // Open the slot's stream now if it isn't, and keep it open for at least
58 // holdMs. Never blocks.
59 static void Prewarm(int slot, int holdMs);
60
61 // Close every stream immediately (fppd shutdown).
62 static void ShutdownAll();
63
64 // Keep slot 1's stream open while set (the PipeWireKeepOutputOpen
65 // setting, applied live), so media triggered at any moment -- GPIO, a
66 // command, the API -- never waits for it to open. Costs the CPU of a
67 // stream that is always running. Clearing it lets the stream close after
68 // the usual linger.
69 static void SetKeepOpen(bool keepOpen);
70
71 // The fixed format every stream carries, as caps fields to append to
72 // "audio/x-raw" (",format=F32LE,rate=...,channels=2,layout=interleaved").
73 // A feed must deliver exactly this: pipewiresink fixes its PipeWire format
74 // when the stream connects and never renegotiates it, so a feed in any
75 // other format would be reinterpreted, not converted.
76 static std::string CapsFields();
77
78 // A track is about to play through this stream. Opens it (or reopens it
79 // on `target`, if it is open on another sink) and keeps it open until
80 // Release(). Never blocks.
81 void Acquire(const std::string& target);
82 // The track stopped. Its feed is detached and the stream closes after
83 // LINGER_MS unless something acquires it again first.
84 void Release(GstElement* feed);
85
86 // Block until audio fed now will reach the card, or timeoutMs passes.
87 bool WaitReady(int timeoutMs);
88 // Block until what earlier feeds (and the silence pushed behind them)
89 // queued has played out, or maxMs passes. A track that starts right
90 // behind another must not go to PLAYING before then: its position runs
91 // from PLAYING, so audio still queued ahead of it would leave the lights
92 // ahead of the sound for the whole track.
93 //
94 // Then flush the stream, unless nothing was handed to it since the last
95 // flush: what that wait can't see -- audio held up by a stall in the
96 // graph -- would otherwise sit ahead of every track to come, and every
97 // track starts from the same empty queue (see Flush()).
98 void WaitDrained(int maxMs);
99
100 // Route `appsink`'s samples into this stream, replacing any previous
101 // feed. Samples from a feed that has since been replaced are dropped, so
102 // a track still tearing down can't interleave with the next one. The
103 // appsink must sync (on Clock()): it paces the audio, and this
104 // stream forwards each buffer as soon as it arrives.
105 void AttachFeed(GstElement* appsink);
106
107 // How far ahead of the clock a feed delivers (see AttachFeed()).
108 static int64_t FeedLeadNs();
109 // The clock this stream's own pipeline runs on (a new reference). It runs
110 // at the rate the card plays samples, not the system's: see
111 // DisciplineClock().
112 GstClock* Clock();
113 // The clock every pipeline feeding this stream must run on (a new
114 // reference): the card clock, run faster or slower by SetFeedSpeed().
115 // Until a feed sets a speed it is the card clock exactly.
116 GstClock* FeedClock();
117 // Play `feed`'s media `speed` times as fast as the card plays samples (1.0
118 // is the card's own pace). A MultiSync remote steers its copy of the
119 // show audio onto the master's timeline with this. The feed clock runs
120 // `speed` times as fast, so the feed hands over media at that pace, and
121 // each buffer is resampled to the card's rate on the way in: the stream
122 // still gets exactly what the card plays, so nothing queues up or runs
123 // dry. A GStreamer rate seek can't do it -- this stream plays at a fixed
124 // rate, so a faster feed only builds a queue the position can't see, and
125 // mp3 and FLAC refuse instant rate changes anyway (GstBaseParse). No-op
126 // for a feed that isn't the current one; reset for each new feed.
127 void SetFeedSpeed(GstElement* feed, double speed);
128 // Drop what `feed` has handed over and not yet played, when it seeks:
129 // left in place, the sound from before the seek plays out first and
130 // everything after it is that much late, by an amount its position can't
131 // show. No-op for a feed that isn't the current one.
132 void FlushFeed(GstElement* feed);
133 // That rate, relative to the system clock, smoothed over the last couple
134 // of minutes (1.000432 for a card 432 ppm fast). 1.0 until measured.
135 150747 double ClockRate() const { return m_clockRate.load(std::memory_order_relaxed); }
136
137 private:
138 explicit PipeWireOutputStream(int slot);
139
140 enum class State { Closed,
141 Opening,
142 Ready,
143 Failed };
144
145 // Worker-thread side.
146 static void WorkerLoop();
147 static void WakeWorker();
148 void Service(uint64_t now); // takes and may drop/retake m_lock
149 bool OpenLocked(std::unique_lock<std::mutex>& lock);
150 void CloseLocked(const char* why);
151 void ForgetFailureLocked();
152 void PushSilenceLocked(int ms);
153 bool WaitQueuedUntil(int64_t deadlineUs);
154 void Flush(bool prime);
155 bool CardRunning();
156 static int TailFlushMs();
157 bool StreamLinked();
158
159 static GstFlowReturn OnFeedSample(GstElement* appsink, gpointer userData);
160 // Whether the card lost time (an xrun) while a track was feeding the stream.
161 bool DisciplineClock();
162 void ResetClockSyncLocked();
163 void EnsureClocksLocked();
164 // Re-derive the feed clock from the card clock and m_feedSpeed, continuous
165 // at `internal` (called whenever either changes).
166 void RecalibrateFeedClockLocked(GstClockTime internal);
167 // `in` resampled by m_feedSpeed (a new buffer), carrying m_rs* across calls.
168 GstBuffer* ResampleLocked(GstBuffer* in);
169 void KeepResampleHistoryLocked(GstBuffer* in);
170
171 const int m_slot;
172 std::mutex m_lock;
173 std::condition_variable m_stateChanged;
174
175 State m_state = State::Closed;
176 std::string m_target; // sink the stream is (or is being) linked to
177 std::string m_wantTarget; // sink the next Acquire/Prewarm asked for
178 bool m_wantOpen = false;
179 int m_users = 0;
180 uint64_t m_holdUntilMs = 0;
181 uint64_t m_openStartedMs = 0;
182 uint64_t m_failedMs = 0; // when the stream last failed (keep-open retry)
183 bool m_linked = false;
184 bool m_cardWasRunning = false; // someone else had the card running when we opened
185 uint64_t m_linkedMs = 0;
186 uint64_t m_lastLinkCheckMs = 0;
187
188 GstElement* m_pipeline = nullptr;
189 GstElement* m_src = nullptr;
190 GstCaps* m_caps = nullptr; // the stream's fixed caps, set at open
191 // When (CLOCK_MONOTONIC, us) everything handed to the stream so far will
192 // have played, assuming the graph takes it at real time once queued.
193 int64_t m_queuedUntilUs = 0;
194 void NoteQueuedLocked(size_t bytes);
195 // Nothing has been handed to the stream since it was last flushed.
196 bool m_flushed = false;
197 int m_rate = 0; // ...and their rate
198 bool m_warnedCaps = false; // logged a feed in the wrong format
199 GstElement* m_feed = nullptr; // appsink currently allowed to feed (no ref held)
200 std::string m_cardStatusPath; // /proc/asound/.../status of the sink's card, "" if none
201
202 // The card clock (DisciplineClock()). Created on first use, never freed.
203 GstClock* m_clock = nullptr;
204 // The feed clock (FeedClock()), and the speed it runs at against the card
205 // clock. Both are under m_lock.
206 GstClock* m_feedClock = nullptr;
207 double m_feedSpeed = 1.0;
208 // The feed clock has run at a speed other than 1 since the feed attached,
209 // so it no longer reads what the card clock does.
210 bool m_feedDiverged = false;
211 // Resampling the feed (SetFeedSpeed()): on from the first speed other than
212 // 1 to the end of the feed. m_rsPos is where the next output frame falls,
213 // in input frames from the start of the next buffer; m_rsHist the last
214 // fifteen input frames, the resampler's filter span less one (kept even
215 // while not resampling, so starting is seamless), oldest first,
216 // m_rsHistFrames of them valid.
217 bool m_rsActive = false;
218 double m_rsPos = 0.0;
219 float m_rsHist[15 * 2] = {};
220 int m_rsHistFrames = 0;
221 std::atomic<double> m_clockRate{ 1.0 };
222 std::string m_syncPath; // the card the fields below describe
223 int m_syncCardRate = 0; // its nominal rate
224 bool m_syncAnchored = false;
225 int64_t m_syncHw0 = 0; // hw_ptr at the anchor...
226 GstClockTime m_syncInt0 = 0; // ...the clock's internal time then...
227 GstClockTime m_syncExt0 = 0; // ...and its own time then
228 int64_t m_syncLastHw = 0; // hw_ptr as last read
229 int64_t m_syncWrap = 0; // added to hw_ptr for each time it wrapped
230 std::string m_syncTrigger; // the card's trigger_time as last read
231 uint64_t m_lastFlushMs = 0; // NowMs() of the last Flush() (under m_lock)
232 GstClockTime m_syncLastInt = 0;
233 // (seconds of internal time, seconds of card time) since the anchor.
234 std::deque<std::pair<double, double>> m_syncWindow;
235 };
236
237 #endif
238