| 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 "PipeWireOutputStream.h" | ||
| 16 | |||
| 17 | #ifdef HAS_GSTREAMER | ||
| 18 | |||
| 19 | #include <gst/app/gstappsink.h> | ||
| 20 | #include <gst/app/gstappsrc.h> | ||
| 21 | |||
| 22 | #if defined(__ARM_NEON) || defined(__ARM_NEON__) | ||
| 23 | #include <arm_neon.h> | ||
| 24 | #endif | ||
| 25 | |||
| 26 | #include <algorithm> | ||
| 27 | #include <array> | ||
| 28 | #include <atomic> | ||
| 29 | #include <chrono> | ||
| 30 | #include <cmath> | ||
| 31 | #include <ctime> | ||
| 32 | #include <cstdio> | ||
| 33 | #include <cstring> | ||
| 34 | #include <thread> | ||
| 35 | #include <vector> | ||
| 36 | |||
| 37 | #include "common.h" | ||
| 38 | #include "common_mini.h" | ||
| 39 | #include "log.h" | ||
| 40 | #include "settings.h" | ||
| 41 | #include "StreamSlotManager.h" | ||
| 42 | |||
| 43 | namespace { | ||
| 44 | // Every stream carries F32 stereo at the graph's own rate, so PipeWire never | ||
| 45 | // resamples it and a feed only converts what differs. | ||
| 46 | constexpr int kChannels = 2; | ||
| 47 | constexpr int kBytesPerFrame = 4 * kChannels; | ||
| 48 | |||
| 49 | // Time from a buffer handed to the stream to it being written to the card, | ||
| 50 | // on top of the card's own queue: pipewiresink queues it for the graph's next | ||
| 51 | // cycle, so one graph quantum (FPP's default is 1024 frames). | ||
| 52 | constexpr int64_t kGraphQuantum = 1024; | ||
| 53 | |||
| 54 | // Longest a stream may take to come up before it is abandoned. A cold open is | ||
| 55 | // ~0.6 s on an AM335x; this only trips when PipeWire itself is unwell. | ||
| 56 | constexpr int kOpenTimeoutMs = 8000; | ||
| 57 | // How often an opening stream's links are checked (each check runs pw-link). | ||
| 58 | constexpr int kLinkCheckIntervalMs = 50; | ||
| 59 | // With no card to watch (a group of network sinks), how long the stream must | ||
| 60 | // have been linked before it is trusted to be on the real driver. | ||
| 61 | constexpr int kNoCardSettleMs = 150; | ||
| 62 | // With the stream kept open, how long after a failure to try again. A | ||
| 63 | // PipeWire restart under a running fppd leaves GStreamer's PipeWire connection | ||
| 64 | // dead until fppd restarts, so retrying hard would only fill the log. | ||
| 65 | constexpr int kKeepOpenRetryMs = 30000; | ||
| 66 | // Silence fed after a flush between tracks, which the reactivated stream | ||
| 67 | // drops in place of the next track's head (see Flush()). Measured on a Pi 5 | ||
| 68 | // I2S HAT: with none, 30-60 ms of the head went; with 60 ms, none did. | ||
| 69 | constexpr int kFlushPrimeMs = 60; | ||
| 70 | |||
| 71 | // The card clock (see DisciplineClock()). How often the card's position is | ||
| 72 | // read, how much of its history the rate is fitted over, how fast a phase | ||
| 73 | // error is closed, and how far the clock may be pushed off the system's. | ||
| 74 | constexpr int kClockSampleMs = 250; | ||
| 75 | constexpr double kClockWindowS = 120.0; | ||
| 76 | constexpr double kClockMinFitS = 4.0; | ||
| 77 | constexpr double kClockPhaseGain = 1.0 / 20.0; // per second of error, per second | ||
| 78 | constexpr double kClockMaxPhaseSlew = 500e-6; | ||
| 79 | constexpr double kClockMaxOffset = 2000e-6; | ||
| 80 | // A jump this big in the card's position is a restart or an xrun, not drift: | ||
| 81 | // start again from it rather than chase it. | ||
| 82 | constexpr double kClockMaxPhaseError = 0.050; | ||
| 83 | // How long after one of our own flushes the card stopping or restarting is | ||
| 84 | // put down to the flush. A flush takes the stream inactive and back, and | ||
| 85 | // where it is the card's only follower (a Pi 4's headphone jack) PipeWire | ||
| 86 | // stops the device and starts it again; the samples a few hundred ms later | ||
| 87 | // see that. | ||
| 88 | constexpr uint64_t kFlushRestartMs = 1000; | ||
| 89 | |||
| 90 | std::mutex s_slotsLock; | ||
| 91 | std::array<PipeWireOutputStream*, StreamSlotManager::MAX_SLOTS + 1> s_slots{}; | ||
| 92 | |||
| 93 | std::mutex s_workerLock; | ||
| 94 | std::condition_variable s_workerWake; | ||
| 95 | std::thread s_worker; | ||
| 96 | bool s_workerRunning = false; | ||
| 97 | // Set by ShutdownAll() and never cleared: a ForSlot() racing it (a prewarm, a | ||
| 98 | // settings listener) must not start a worker again, let alone assign a new | ||
| 99 | // std::thread over the one ShutdownAll() is joining. | ||
| 100 | bool s_workerShutDown = false; | ||
| 101 | bool s_workerKick = false; | ||
| 102 | |||
| 103 | std::atomic<bool> s_keepOpen{ false }; | ||
| 104 | |||
| 105 | 37424 | uint64_t NowMs() { | |
| 106 | 37424 | return (uint64_t)GetMonotonicTimeMS(); // holds and timeouts: must not step with the wall clock | |
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | 163 | PipeWireOutputStream::PipeWireOutputStream(int slot) : | |
| 111 |
1/2✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
|
163 | m_slot(slot) { |
| 112 | 163 | } | |
| 113 | |||
| 114 | 5931 | PipeWireOutputStream* PipeWireOutputStream::ForSlot(int slot) { | |
| 115 |
7/10✓ Branch 0 taken 5931 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5931 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5931 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 5929 times.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 5929 times.
|
5931 | if (slot < 1 || slot > StreamSlotManager::MAX_SLOTS || !isPipeWireBackend()) { |
| 116 | 2 | return nullptr; | |
| 117 | } | ||
| 118 |
1/2✓ Branch 0 taken 5929 times.
✗ Branch 1 not taken.
|
5929 | std::unique_lock<std::mutex> lock(s_slotsLock); |
| 119 |
2/2✓ Branch 0 taken 163 times.
✓ Branch 1 taken 5766 times.
|
5929 | if (!s_slots[slot]) { |
| 120 |
3/8✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 163 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 163 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
163 | s_slots[slot] = new PipeWireOutputStream(slot); |
| 121 | } | ||
| 122 | { | ||
| 123 |
1/2✓ Branch 0 taken 5929 times.
✗ Branch 1 not taken.
|
5929 | std::unique_lock<std::mutex> wlock(s_workerLock); |
| 124 |
4/4✓ Branch 0 taken 300 times.
✓ Branch 1 taken 5629 times.
✓ Branch 2 taken 150 times.
✓ Branch 3 taken 150 times.
|
5929 | if (!s_workerRunning && !s_workerShutDown) { |
| 125 | 150 | s_workerRunning = true; | |
| 126 |
1/2✓ Branch 0 taken 150 times.
✗ Branch 1 not taken.
|
150 | s_worker = std::thread(WorkerLoop); |
| 127 | } | ||
| 128 | 5929 | } | |
| 129 | 5929 | return s_slots[slot]; | |
| 130 | 5929 | } | |
| 131 | |||
| 132 | 2172 | void PipeWireOutputStream::Prewarm(int slot, int holdMs) { | |
| 133 |
1/2✓ Branch 0 taken 2172 times.
✗ Branch 1 not taken.
|
2172 | PipeWireOutputStream* s = ForSlot(slot); |
| 134 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2170 times.
|
2172 | if (!s) { |
| 135 | 2 | return; | |
| 136 | } | ||
| 137 |
1/2✓ Branch 0 taken 2170 times.
✗ Branch 1 not taken.
|
2170 | std::string target = GStreamerOutput::PipeWireSinkNameForSlot(slot); |
| 138 | { | ||
| 139 |
1/2✓ Branch 0 taken 2170 times.
✗ Branch 1 not taken.
|
2170 | std::unique_lock<std::mutex> lock(s->m_lock); |
| 140 |
1/2✓ Branch 0 taken 2170 times.
✗ Branch 1 not taken.
|
2170 | s->m_wantTarget = target; |
| 141 |
1/2✓ Branch 0 taken 2170 times.
✗ Branch 1 not taken.
|
2170 | s->m_holdUntilMs = std::max(s->m_holdUntilMs, NowMs() + (uint64_t)std::max(holdMs, 0)); |
| 142 | 2170 | s->m_wantOpen = true; | |
| 143 |
1/2✓ Branch 0 taken 2170 times.
✗ Branch 1 not taken.
|
2170 | s->ForgetFailureLocked(); |
| 144 | 2170 | } | |
| 145 |
1/2✓ Branch 0 taken 2170 times.
✗ Branch 1 not taken.
|
2170 | WakeWorker(); |
| 146 | 2170 | } | |
| 147 | |||
| 148 | 1887 | void PipeWireOutputStream::SetKeepOpen(bool keepOpen) { | |
| 149 |
2/2✓ Branch 0 taken 1870 times.
✓ Branch 1 taken 17 times.
|
1887 | if (s_keepOpen.exchange(keepOpen) == keepOpen) { |
| 150 | 1870 | return; | |
| 151 | } | ||
| 152 |
1/2✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
|
17 | if (keepOpen) { |
| 153 | 17 | LogInfo(VB_MEDIAOUT, "PipeWire output stream 1 will be kept open\n"); | |
| 154 | 17 | Prewarm(1, 0); | |
| 155 | } else { | ||
| 156 | // Falls back to the usual linger; the worker closes it once that | ||
| 157 | // and any playing track are done. | ||
| 158 | ✗ | LogInfo(VB_MEDIAOUT, "PipeWire output stream 1 no longer kept open\n"); | |
| 159 | ✗ | if (PipeWireOutputStream* s = ForSlot(1)) { | |
| 160 | ✗ | std::unique_lock<std::mutex> lock(s->m_lock); | |
| 161 | ✗ | s->m_holdUntilMs = std::max(s->m_holdUntilMs, NowMs() + LINGER_MS); | |
| 162 | ✗ | } | |
| 163 | ✗ | WakeWorker(); | |
| 164 | } | ||
| 165 | } | ||
| 166 | |||
| 167 | 2085 | std::string PipeWireOutputStream::CapsFields() { | |
| 168 |
2/4✓ Branch 0 taken 2085 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2085 times.
✗ Branch 3 not taken.
|
4170 | return ",format=F32LE,rate=" + std::to_string(GStreamerOutput::PipeWireGraphRate()) + |
| 169 |
3/6✓ Branch 0 taken 2085 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2085 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2085 times.
✗ Branch 5 not taken.
|
6191 | ",channels=" + std::to_string(kChannels) + ",layout=interleaved"; |
| 170 | } | ||
| 171 | |||
| 172 | 1867 | void PipeWireOutputStream::ShutdownAll() { | |
| 173 | 1867 | std::thread worker; | |
| 174 | { | ||
| 175 |
1/2✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
|
1867 | std::unique_lock<std::mutex> wlock(s_workerLock); |
| 176 | 1867 | s_workerShutDown = true; | |
| 177 |
2/2✓ Branch 0 taken 1717 times.
✓ Branch 1 taken 150 times.
|
1867 | if (!s_workerRunning) { |
| 178 | 1717 | return; | |
| 179 | } | ||
| 180 | 150 | s_workerRunning = false; | |
| 181 | 150 | worker = std::move(s_worker); | |
| 182 |
2/2✓ Branch 0 taken 150 times.
✓ Branch 1 taken 1717 times.
|
1867 | } |
| 183 | 150 | s_workerWake.notify_all(); | |
| 184 |
1/2✓ Branch 0 taken 150 times.
✗ Branch 1 not taken.
|
150 | if (worker.joinable()) { |
| 185 |
1/2✓ Branch 0 taken 150 times.
✗ Branch 1 not taken.
|
150 | worker.join(); |
| 186 | } | ||
| 187 |
1/2✓ Branch 0 taken 150 times.
✗ Branch 1 not taken.
|
150 | std::unique_lock<std::mutex> lock(s_slotsLock); |
| 188 |
2/2✓ Branch 0 taken 900 times.
✓ Branch 1 taken 150 times.
|
1050 | for (PipeWireOutputStream* s : s_slots) { |
| 189 |
2/2✓ Branch 0 taken 163 times.
✓ Branch 1 taken 737 times.
|
900 | if (s) { |
| 190 |
1/2✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
|
163 | std::unique_lock<std::mutex> slock(s->m_lock); |
| 191 | 163 | s->m_users = 0; | |
| 192 | 163 | s->m_holdUntilMs = 0; | |
| 193 |
1/2✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
|
163 | s->CloseLocked("fppd shutting down"); |
| 194 | 163 | } | |
| 195 | } | ||
| 196 |
2/2✓ Branch 0 taken 150 times.
✓ Branch 1 taken 1717 times.
|
1867 | } |
| 197 | |||
| 198 | 1881 | void PipeWireOutputStream::Acquire(const std::string& target) { | |
| 199 | { | ||
| 200 |
1/2✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
|
1881 | std::unique_lock<std::mutex> lock(m_lock); |
| 201 | 1881 | m_users++; | |
| 202 |
1/2✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
|
1881 | m_wantTarget = target; |
| 203 | 1881 | m_wantOpen = true; | |
| 204 |
1/2✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
|
1881 | ForgetFailureLocked(); |
| 205 | 1881 | } | |
| 206 | 1881 | WakeWorker(); | |
| 207 | 1881 | } | |
| 208 | |||
| 209 | // A failure belongs to the attempt that made it. Left in place, the next | ||
| 210 | // track's WaitReady() returned on it at once while the worker was already | ||
| 211 | // opening the stream again, and the head of that track went nowhere. | ||
| 212 | 4051 | void PipeWireOutputStream::ForgetFailureLocked() { | |
| 213 |
2/2✓ Branch 0 taken 33 times.
✓ Branch 1 taken 4018 times.
|
4051 | if (m_state == State::Failed) { |
| 214 | 33 | m_state = State::Closed; | |
| 215 | } | ||
| 216 | 4051 | } | |
| 217 | |||
| 218 | 1836 | void PipeWireOutputStream::Release(GstElement* feed) { | |
| 219 | { | ||
| 220 |
1/2✓ Branch 0 taken 1836 times.
✗ Branch 1 not taken.
|
1836 | std::unique_lock<std::mutex> lock(m_lock); |
| 221 |
2/4✓ Branch 0 taken 1836 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1836 times.
✗ Branch 3 not taken.
|
1836 | if (feed && m_feed == feed) { |
| 222 | 1836 | m_feed = nullptr; | |
| 223 | // Push the track's last fragment out behind some silence. PipeWire | ||
| 224 | // holds a partial cycle until there is more data, so with nothing | ||
| 225 | // behind it the tail of a stopped track sat in the stream and played | ||
| 226 | // at the head of the next one. (Flushing here instead would drop | ||
| 227 | // the tail; WaitDrained() flushes only once it has played.) | ||
| 228 |
2/4✓ Branch 0 taken 1836 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1836 times.
✗ Branch 3 not taken.
|
1836 | PushSilenceLocked(TailFlushMs()); |
| 229 | } | ||
| 230 |
1/2✓ Branch 0 taken 1836 times.
✗ Branch 1 not taken.
|
1836 | if (m_users > 0) { |
| 231 | 1836 | m_users--; | |
| 232 | } | ||
| 233 |
1/2✓ Branch 0 taken 1836 times.
✗ Branch 1 not taken.
|
1836 | m_holdUntilMs = std::max(m_holdUntilMs, NowMs() + LINGER_MS); |
| 234 | 1836 | } | |
| 235 | 1836 | WakeWorker(); | |
| 236 | 1836 | } | |
| 237 | |||
| 238 | 415073 | static int64_t MonoUs() { | |
| 239 | struct timespec ts; | ||
| 240 | 415073 | clock_gettime(CLOCK_MONOTONIC, &ts); | |
| 241 | 415073 | return (int64_t)ts.tv_sec * 1000000LL + ts.tv_nsec / 1000; | |
| 242 | } | ||
| 243 | |||
| 244 | 55628 | void PipeWireOutputStream::NoteQueuedLocked(size_t bytes) { | |
| 245 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 55628 times.
|
55628 | if (m_rate <= 0) { |
| 246 | ✗ | return; | |
| 247 | } | ||
| 248 | 55628 | int64_t now = MonoUs(); | |
| 249 | 55628 | int64_t durUs = (int64_t)(bytes / kBytesPerFrame) * 1000000LL / m_rate; | |
| 250 | 55628 | m_queuedUntilUs = std::max(m_queuedUntilUs, now) + durUs; | |
| 251 | } | ||
| 252 | |||
| 253 | // Whether the queue drained before the deadline. | ||
| 254 | 178256 | bool PipeWireOutputStream::WaitQueuedUntil(int64_t deadlineUs) { | |
| 255 | while (true) { | ||
| 256 | int64_t remaining; | ||
| 257 | { | ||
| 258 |
1/2✓ Branch 0 taken 178256 times.
✗ Branch 1 not taken.
|
178256 | std::unique_lock<std::mutex> lock(m_lock); |
| 259 | 178256 | remaining = m_queuedUntilUs - MonoUs(); | |
| 260 | 178256 | } | |
| 261 |
2/2✓ Branch 0 taken 815 times.
✓ Branch 1 taken 177441 times.
|
178256 | if (remaining <= 0) { |
| 262 | 3748 | return true; | |
| 263 | } | ||
| 264 |
2/2✓ Branch 0 taken 2933 times.
✓ Branch 1 taken 174508 times.
|
177441 | if (MonoUs() >= deadlineUs) { |
| 265 | 2933 | return false; | |
| 266 | } | ||
| 267 |
1/2✓ Branch 0 taken 174508 times.
✗ Branch 1 not taken.
|
174508 | std::this_thread::sleep_for(std::chrono::microseconds(std::min<int64_t>(remaining, 5000))); |
| 268 | 174508 | } | |
| 269 | } | ||
| 270 | |||
| 271 | 1874 | void PipeWireOutputStream::WaitDrained(int maxMs) { | |
| 272 | 1874 | int64_t deadline = MonoUs() + (int64_t)maxMs * 1000; | |
| 273 | 1874 | WaitQueuedUntil(deadline); | |
| 274 | 1874 | Flush(true); | |
| 275 | // The silence Flush() pushed must play before the track goes to PLAYING, | ||
| 276 | // or the track starts that much behind its position. | ||
| 277 | 1874 | WaitQueuedUntil(MonoUs() + (int64_t)(kFlushPrimeMs + 20) * 1000); | |
| 278 | 1874 | } | |
| 279 | |||
| 280 | // m_queuedUntilUs only knows what was handed to the stream and assumes the | ||
| 281 | // graph takes it at real time. When the graph stalls instead -- an overloaded | ||
| 282 | // board, an xrun -- the card falls behind, the feed doesn't (it runs on the | ||
| 283 | // clock), and the difference stays queued inside pipewiresink, where nothing | ||
| 284 | // here can see it. With the stream held open and fed track after track it | ||
| 285 | // never drains: the sound stays that far behind the lights until fppd | ||
| 286 | // restarts. One 300 ms stall on a Pi 5 left every later song 280 ms behind | ||
| 287 | // (issue #3083). | ||
| 288 | // | ||
| 289 | // So between tracks, once the estimate says the last one has played, flush: | ||
| 290 | // pipewiresink drops whatever its stream still holds (pw_stream_flush()), | ||
| 291 | // which is nothing unless there was a stall, and every track starts from an | ||
| 292 | // empty queue. The first track on a newly opened stream is flushed as well. | ||
| 293 | // Left as it opened, the stream's queue settled a whole graph cycle deeper on | ||
| 294 | // some opens than others, and the sound landed anywhere from 1 to 28 ms off | ||
| 295 | // the lights from one show start to the next on a Pi 5; flushed, 12-20 ms. | ||
| 296 | // The stream goes inactive and back, and drops the first few tens of ms | ||
| 297 | // handed to it after that, so between tracks it is primed with silence. | ||
| 298 | // | ||
| 299 | // When the stall is an xrun on the card itself, DisciplineClock() sees the | ||
| 300 | // card stop or restart, and the stream is flushed right then, mid-track, | ||
| 301 | // unprimed: the sound skips what the stall held up -- the xrun was already | ||
| 302 | // audible -- and is back with the lights at once rather than at the next | ||
| 303 | // track. A stall that never reaches the card (a late group node) is left to | ||
| 304 | // the flush between tracks. | ||
| 305 | 1896 | void PipeWireOutputStream::Flush(bool prime) { | |
| 306 | 1896 | GstElement* src = nullptr; | |
| 307 | { | ||
| 308 |
1/2✓ Branch 0 taken 1896 times.
✗ Branch 1 not taken.
|
1896 | std::unique_lock<std::mutex> lock(m_lock); |
| 309 |
6/6✓ Branch 0 taken 1886 times.
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 1885 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 1544 times.
✓ Branch 5 taken 341 times.
|
1896 | if (!m_src || m_state != State::Ready || m_flushed) { |
| 310 | 1555 | return; | |
| 311 | } | ||
| 312 | 341 | m_flushed = true; | |
| 313 |
1/2✓ Branch 0 taken 341 times.
✗ Branch 1 not taken.
|
341 | m_lastFlushMs = NowMs(); |
| 314 |
3/6✓ Branch 0 taken 341 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 341 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 341 times.
✗ Branch 5 not taken.
|
341 | src = GST_ELEMENT(gst_object_ref(m_src)); |
| 315 |
2/2✓ Branch 0 taken 341 times.
✓ Branch 1 taken 1555 times.
|
1896 | } |
| 316 | // Outside the lock: the flush takes the sink's own locks. | ||
| 317 | 341 | gst_element_send_event(src, gst_event_new_flush_start()); | |
| 318 | 341 | gst_element_send_event(src, gst_event_new_flush_stop(FALSE)); | |
| 319 | { | ||
| 320 |
1/2✓ Branch 0 taken 341 times.
✗ Branch 1 not taken.
|
341 | std::unique_lock<std::mutex> lock(m_lock); |
| 321 |
1/2✓ Branch 0 taken 341 times.
✗ Branch 1 not taken.
|
341 | if (m_src == src) { |
| 322 | 341 | m_queuedUntilUs = 0; | |
| 323 |
2/2✓ Branch 0 taken 319 times.
✓ Branch 1 taken 22 times.
|
341 | if (prime) { |
| 324 |
1/2✓ Branch 0 taken 319 times.
✗ Branch 1 not taken.
|
319 | PushSilenceLocked(kFlushPrimeMs); |
| 325 | } | ||
| 326 | } | ||
| 327 | 341 | } | |
| 328 | 341 | gst_object_unref(src); | |
| 329 |
2/2✓ Branch 0 taken 319 times.
✓ Branch 1 taken 22 times.
|
341 | LogDebug(VB_MEDIAOUT, "PipeWire output stream %d flushed%s\n", m_slot, prime ? " between tracks" : ""); |
| 330 | } | ||
| 331 | |||
| 332 | 1881 | bool PipeWireOutputStream::WaitReady(int timeoutMs) { | |
| 333 | // Ready only counts on the target asked for: a stream still Ready on the | ||
| 334 | // previous sink is about to be closed and reopened by the worker. | ||
| 335 |
1/2✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
|
1881 | std::unique_lock<std::mutex> lock(m_lock); |
| 336 |
4/4✓ Branch 0 taken 3729 times.
✓ Branch 1 taken 164 times.
✓ Branch 2 taken 3728 times.
✓ Branch 3 taken 1 times.
|
3893 | auto readyHere = [this] { return m_state == State::Ready && m_target == m_wantTarget; }; |
| 337 |
1/2✓ Branch 0 taken 1874 times.
✗ Branch 1 not taken.
|
1881 | m_stateChanged.wait_for(lock, std::chrono::milliseconds(timeoutMs), [this, &readyHere] { |
| 338 |
4/4✓ Branch 0 taken 155 times.
✓ Branch 1 taken 1864 times.
✓ Branch 2 taken 146 times.
✓ Branch 3 taken 9 times.
|
2165 | return readyHere() || m_state == State::Failed || |
| 339 |
3/4✓ Branch 0 taken 48 times.
✓ Branch 1 taken 98 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 48 times.
|
2165 | (m_state == State::Closed && !m_wantOpen); |
| 340 | }); | ||
| 341 | 3748 | return readyHere(); | |
| 342 | 1874 | } | |
| 343 | |||
| 344 | 1836 | int PipeWireOutputStream::TailFlushMs() { | |
| 345 | // Three graph cycles: more than any partial cycle plus the feed's lead. | ||
| 346 | 1836 | return (int)(3 * kGraphQuantum * 1000 / GStreamerOutput::PipeWireGraphRate()) + 1; | |
| 347 | } | ||
| 348 | |||
| 349 | 1881 | int64_t PipeWireOutputStream::FeedLeadNs() { | |
| 350 | 1881 | return kGraphQuantum * 1000000000LL / GStreamerOutput::PipeWireGraphRate(); | |
| 351 | } | ||
| 352 | |||
| 353 | 1881 | void PipeWireOutputStream::AttachFeed(GstElement* appsink) { | |
| 354 | { | ||
| 355 |
1/2✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
|
1881 | std::unique_lock<std::mutex> lock(m_lock); |
| 356 | 1881 | m_feed = appsink; | |
| 357 | // Each feed starts at the card's own pace. The feed clock is put back | ||
| 358 | // on the card clock now, before this feed's pipeline has a base time | ||
| 359 | // on it, so a speed the last track was steered at doesn't carry over. | ||
| 360 | 1881 | m_feedSpeed = 1.0; | |
| 361 | 1881 | m_feedDiverged = false; | |
| 362 | 1881 | m_rsActive = false; | |
| 363 | 1881 | m_rsPos = 0.0; | |
| 364 | 1881 | m_rsHistFrames = 0; | |
| 365 |
2/4✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1881 times.
✗ Branch 3 not taken.
|
1881 | if (m_clock && m_feedClock) { |
| 366 |
2/4✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1881 times.
✗ Branch 3 not taken.
|
1881 | RecalibrateFeedClockLocked(gst_clock_get_internal_time(m_clock)); |
| 367 | } | ||
| 368 | 1881 | } | |
| 369 | // Run the feed one graph cycle ahead of the clock. Handed over exactly | ||
| 370 | // on time, a buffer can miss the cycle that needed it by a hair; PipeWire | ||
| 371 | // then plays a quantum of silence there and the rest of the track is a | ||
| 372 | // quantum late. A cycle's worth queued absorbs that. | ||
| 373 | 1881 | g_object_set(appsink, "emit-signals", TRUE, "ts-offset", -FeedLeadNs(), NULL); | |
| 374 | 1881 | g_signal_connect(appsink, "new-sample", G_CALLBACK(OnFeedSample), this); | |
| 375 | 1881 | } | |
| 376 | |||
| 377 | // Runs on the feeding pipeline's streaming thread. `this` is a per-slot | ||
| 378 | // singleton that is never freed, so a feed outliving its GStreamerOutput (async | ||
| 379 | // teardown) can still land here safely; it is simply no longer m_feed. | ||
| 380 | 54023 | GstFlowReturn PipeWireOutputStream::OnFeedSample(GstElement* appsink, gpointer userData) { | |
| 381 | 54023 | PipeWireOutputStream* self = static_cast<PipeWireOutputStream*>(userData); | |
| 382 | 54023 | GstSample* sample = gst_app_sink_pull_sample(GST_APP_SINK(appsink)); | |
| 383 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 54023 times.
|
54023 | if (!sample) { |
| 384 | ✗ | return GST_FLOW_OK; | |
| 385 | } | ||
| 386 | 54023 | GstBuffer* in = gst_sample_get_buffer(sample); | |
| 387 | 54023 | const GstSegment* segment = gst_sample_get_segment(sample); | |
| 388 | 54023 | GstCaps* caps = gst_sample_get_caps(sample); | |
| 389 | |||
| 390 | 54023 | GstElement* src = nullptr; | |
| 391 | 54023 | GstBuffer* out = nullptr; | |
| 392 | { | ||
| 393 |
1/2✓ Branch 0 taken 54023 times.
✗ Branch 1 not taken.
|
54023 | std::unique_lock<std::mutex> lock(self->m_lock); |
| 394 |
6/8✓ Branch 0 taken 54023 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 54023 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 53323 times.
✓ Branch 5 taken 700 times.
✓ Branch 6 taken 53322 times.
✓ Branch 7 taken 1 times.
|
54023 | bool ours = in && caps && self->m_src && self->m_feed == appsink; |
| 395 |
6/10✓ Branch 0 taken 53322 times.
✓ Branch 1 taken 701 times.
✓ Branch 2 taken 53322 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 53322 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 53322 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 54023 times.
|
54023 | if (ours && self->m_caps && !gst_caps_can_intersect(self->m_caps, caps)) { |
| 396 | // Can't happen unless the graph rate moved under a running fppd; | ||
| 397 | // playing it would be noise, so drop it and say so once. | ||
| 398 | ✗ | if (!self->m_warnedCaps) { | |
| 399 | ✗ | self->m_warnedCaps = true; | |
| 400 | ✗ | gchar* got = gst_caps_to_string(caps); | |
| 401 | ✗ | gchar* want = gst_caps_to_string(self->m_caps); | |
| 402 | ✗ | LogErr(VB_MEDIAOUT, "PipeWire output stream %d: feed is %s, stream is %s -- dropping it\n", | |
| 403 | self->m_slot, got, want); | ||
| 404 | ✗ | g_free(got); | |
| 405 | ✗ | g_free(want); | |
| 406 | } | ||
| 407 | ✗ | ours = false; | |
| 408 | } | ||
| 409 |
2/2✓ Branch 0 taken 53322 times.
✓ Branch 1 taken 701 times.
|
54023 | if (ours) { |
| 410 | // Steered to another speed (SetFeedSpeed()), the media is | ||
| 411 | // resampled to the card's rate here; otherwise it goes in as it | ||
| 412 | // came, and only its last frames are kept for a later start. | ||
| 413 |
2/2✓ Branch 0 taken 12434 times.
✓ Branch 1 taken 40888 times.
|
53322 | if (self->m_rsActive) { |
| 414 |
1/2✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
|
12434 | out = self->ResampleLocked(in); |
| 415 | } else { | ||
| 416 |
1/2✓ Branch 0 taken 40888 times.
✗ Branch 1 not taken.
|
40888 | self->KeepResampleHistoryLocked(in); |
| 417 |
1/2✓ Branch 0 taken 40888 times.
✗ Branch 1 not taken.
|
40888 | out = gst_buffer_copy(in); |
| 418 | } | ||
| 419 | 53322 | ours = out != nullptr; | |
| 420 | } | ||
| 421 |
2/2✓ Branch 0 taken 53322 times.
✓ Branch 1 taken 701 times.
|
54023 | if (ours) { |
| 422 | // The feed clock is the card clock (until it is steered, then off | ||
| 423 | // it by however far it has run ahead or behind), so a buffer's | ||
| 424 | // running time in the feeding pipeline maps onto ours through the | ||
| 425 | // two base times and that offset. The sink doesn't sync on it | ||
| 426 | // (the feed already paced the buffer); it is carried for | ||
| 427 | // PipeWire's own bookkeeping. | ||
| 428 |
1/2✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
|
53322 | GstClockTime rt = gst_segment_to_running_time(segment, GST_FORMAT_TIME, GST_BUFFER_PTS(in)); |
| 429 |
1/2✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
|
53322 | if (GST_CLOCK_TIME_IS_VALID(rt)) { |
| 430 |
1/2✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
|
53322 | GstClockTime abs = gst_element_get_base_time(appsink) + rt; |
| 431 |
2/2✓ Branch 0 taken 12434 times.
✓ Branch 1 taken 40888 times.
|
53322 | if (self->m_feedDiverged) { |
| 432 |
2/4✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12434 times.
✗ Branch 3 not taken.
|
12434 | GstClockTimeDiff ahead = GST_CLOCK_DIFF(gst_clock_get_time(self->m_clock), |
| 433 | gst_clock_get_time(self->m_feedClock)); | ||
| 434 |
3/4✓ Branch 0 taken 877 times.
✓ Branch 1 taken 11557 times.
✓ Branch 2 taken 877 times.
✗ Branch 3 not taken.
|
12434 | abs = ahead > 0 && (GstClockTime)ahead > abs ? 0 : abs - ahead; |
| 435 | } | ||
| 436 |
1/2✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
|
53322 | GstClockTime ourBase = gst_element_get_base_time(self->m_pipeline); |
| 437 |
1/2✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
|
53322 | GST_BUFFER_PTS(out) = abs > ourBase ? abs - ourBase : 0; |
| 438 | } else { | ||
| 439 | ✗ | GST_BUFFER_PTS(out) = GST_CLOCK_TIME_NONE; | |
| 440 | } | ||
| 441 |
2/2✓ Branch 0 taken 12434 times.
✓ Branch 1 taken 40888 times.
|
53322 | if (self->m_rsActive) { |
| 442 | 12434 | GST_BUFFER_DURATION(out) = self->m_rate > 0 | |
| 443 |
3/6✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12434 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12434 times.
✗ Branch 5 not taken.
|
12434 | ? gst_util_uint64_scale(gst_buffer_get_size(out) / kBytesPerFrame, GST_SECOND, self->m_rate) |
| 444 | : GST_CLOCK_TIME_NONE; | ||
| 445 | } | ||
| 446 | 53322 | GST_BUFFER_DTS(out) = GST_CLOCK_TIME_NONE; | |
| 447 |
2/4✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53322 times.
✗ Branch 3 not taken.
|
53322 | self->NoteQueuedLocked(gst_buffer_get_size(out)); |
| 448 | 53322 | self->m_flushed = false; | |
| 449 |
3/6✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53322 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 53322 times.
✗ Branch 5 not taken.
|
53322 | src = GST_ELEMENT(gst_object_ref(self->m_src)); |
| 450 | } | ||
| 451 | 54023 | } | |
| 452 |
2/2✓ Branch 0 taken 53322 times.
✓ Branch 1 taken 701 times.
|
54023 | if (src) { |
| 453 | // Outside the lock: the push can take the sink's own locks. | ||
| 454 | 53322 | gst_app_src_push_buffer(GST_APP_SRC(src), out); | |
| 455 | 53322 | gst_object_unref(src); | |
| 456 | } | ||
| 457 | 54023 | gst_sample_unref(sample); | |
| 458 | 54023 | return GST_FLOW_OK; | |
| 459 | } | ||
| 460 | |||
| 461 | 5887 | void PipeWireOutputStream::WakeWorker() { | |
| 462 | { | ||
| 463 |
1/2✓ Branch 0 taken 5887 times.
✗ Branch 1 not taken.
|
5887 | std::unique_lock<std::mutex> wlock(s_workerLock); |
| 464 | 5887 | s_workerKick = true; | |
| 465 | 5887 | } | |
| 466 | 5887 | s_workerWake.notify_all(); | |
| 467 | 5887 | } | |
| 468 | |||
| 469 | 150 | void PipeWireOutputStream::WorkerLoop() { | |
| 470 |
2/4✓ Branch 0 taken 150 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 150 times.
✗ Branch 3 not taken.
|
300 | SetThreadName("FPP-PWStream"); |
| 471 | while (true) { | ||
| 472 | // Slots are created on demand but never freed, so a snapshot of the | ||
| 473 | // table is safe to service without holding s_slotsLock -- which | ||
| 474 | // ForSlot() callers on the main loop would otherwise wait on while a | ||
| 475 | // stream connects to the daemon. | ||
| 476 | std::array<PipeWireOutputStream*, StreamSlotManager::MAX_SLOTS + 1> slots; | ||
| 477 | { | ||
| 478 |
1/2✓ Branch 0 taken 19423 times.
✗ Branch 1 not taken.
|
19423 | std::unique_lock<std::mutex> lock(s_slotsLock); |
| 479 | 19423 | slots = s_slots; | |
| 480 | 19423 | } | |
| 481 | 19423 | bool anyOpening = false; | |
| 482 |
1/2✓ Branch 0 taken 19423 times.
✗ Branch 1 not taken.
|
19423 | uint64_t now = NowMs(); |
| 483 |
2/2✓ Branch 0 taken 116538 times.
✓ Branch 1 taken 19423 times.
|
135961 | for (PipeWireOutputStream* s : slots) { |
| 484 |
2/2✓ Branch 0 taken 28502 times.
✓ Branch 1 taken 88036 times.
|
116538 | if (s) { |
| 485 |
3/4✓ Branch 0 taken 28502 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 28496 times.
|
28502 | if (s->DisciplineClock()) { |
| 486 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | LogInfo(VB_MEDIAOUT, "PipeWire output stream %d: the card lost time (xrun) while playing; " |
| 487 | "flushing so the sound stays with the lights\n", s->m_slot); | ||
| 488 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | s->Flush(false); |
| 489 | } | ||
| 490 |
1/2✓ Branch 0 taken 28502 times.
✗ Branch 1 not taken.
|
28502 | s->Service(now); |
| 491 |
1/2✓ Branch 0 taken 28502 times.
✗ Branch 1 not taken.
|
28502 | std::unique_lock<std::mutex> slock(s->m_lock); |
| 492 | 28502 | anyOpening |= (s->m_state == State::Opening); | |
| 493 | 28502 | } | |
| 494 | } | ||
| 495 |
1/2✓ Branch 0 taken 19423 times.
✗ Branch 1 not taken.
|
19423 | std::unique_lock<std::mutex> wlock(s_workerLock); |
| 496 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 19422 times.
|
19423 | if (!s_workerRunning) { |
| 497 | 1 | break; | |
| 498 | } | ||
| 499 | // Opening streams are polled every 10 ms; otherwise the only thing to | ||
| 500 | // watch for is a linger running out. | ||
| 501 |
3/4✓ Branch 0 taken 2844 times.
✓ Branch 1 taken 16578 times.
✓ Branch 2 taken 19422 times.
✗ Branch 3 not taken.
|
19422 | s_workerWake.wait_for(wlock, std::chrono::milliseconds(anyOpening ? 10 : 250), |
| 502 |
4/4✓ Branch 0 taken 33116 times.
✓ Branch 1 taken 5538 times.
✓ Branch 2 taken 148 times.
✓ Branch 3 taken 32968 times.
|
38654 | [] { return s_workerKick || !s_workerRunning; }); |
| 503 | 19422 | s_workerKick = false; | |
| 504 |
2/2✓ Branch 0 taken 149 times.
✓ Branch 1 taken 19273 times.
|
19422 | if (!s_workerRunning) { |
| 505 | 149 | break; | |
| 506 | } | ||
| 507 |
2/2✓ Branch 0 taken 19273 times.
✓ Branch 1 taken 150 times.
|
38696 | } |
| 508 | 150 | } | |
| 509 | |||
| 510 | 28502 | void PipeWireOutputStream::Service(uint64_t now) { | |
| 511 |
1/2✓ Branch 0 taken 28502 times.
✗ Branch 1 not taken.
|
28502 | std::unique_lock<std::mutex> lock(m_lock); |
| 512 | |||
| 513 |
3/4✓ Branch 0 taken 2041 times.
✓ Branch 1 taken 26461 times.
✓ Branch 2 taken 2041 times.
✗ Branch 3 not taken.
|
28502 | bool keepOpen = s_keepOpen && m_slot == 1; |
| 514 |
3/8✓ Branch 0 taken 2041 times.
✓ Branch 1 taken 26461 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2041 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
28502 | if (keepOpen && !m_wantOpen && (m_state == State::Closed || m_state == State::Failed) && |
| 515 | ✗ | now - m_failedMs >= (uint64_t)kKeepOpenRetryMs) { | |
| 516 | ✗ | if (m_wantTarget.empty()) { | |
| 517 | ✗ | m_wantTarget = GStreamerOutput::PipeWireSinkNameForSlot(m_slot); | |
| 518 | } | ||
| 519 | ✗ | m_wantOpen = true; | |
| 520 | } | ||
| 521 |
6/6✓ Branch 0 taken 26461 times.
✓ Branch 1 taken 2041 times.
✓ Branch 2 taken 11208 times.
✓ Branch 3 taken 15253 times.
✓ Branch 4 taken 10392 times.
✓ Branch 5 taken 816 times.
|
28502 | bool wanted = keepOpen || m_users > 0 || now < m_holdUntilMs; |
| 522 |
2/2✓ Branch 0 taken 816 times.
✓ Branch 1 taken 27686 times.
|
28502 | if (!wanted) { |
| 523 | 816 | m_wantOpen = false; | |
| 524 |
2/2✓ Branch 0 taken 17 times.
✓ Branch 1 taken 799 times.
|
816 | if (m_state != State::Closed) { |
| 525 |
1/2✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
|
17 | CloseLocked("idle"); |
| 526 | } | ||
| 527 | 816 | return; | |
| 528 | } | ||
| 529 | |||
| 530 |
2/2✓ Branch 0 taken 27143 times.
✓ Branch 1 taken 543 times.
|
27686 | if (m_pipeline) { |
| 531 | // A bus error means the stream is dead (PipeWire restarted, sink gone). | ||
| 532 | // Drop it; the next track or prewarm opens a fresh one. | ||
| 533 |
1/2✓ Branch 0 taken 27143 times.
✗ Branch 1 not taken.
|
27143 | GstBus* bus = gst_element_get_bus(m_pipeline); |
| 534 |
2/4✓ Branch 0 taken 27143 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 27143 times.
|
27143 | while (GstMessage* msg = gst_bus_pop_filtered(bus, GST_MESSAGE_ERROR)) { |
| 535 | ✗ | GError* err = nullptr; | |
| 536 | ✗ | gst_message_parse_error(msg, &err, nullptr); | |
| 537 | ✗ | LogWarn(VB_MEDIAOUT, "PipeWire output stream %d: %s\n", m_slot, err ? err->message : "error"); | |
| 538 | ✗ | if (err) { | |
| 539 | ✗ | g_error_free(err); | |
| 540 | } | ||
| 541 | ✗ | gst_message_unref(msg); | |
| 542 | ✗ | m_wantOpen = false; | |
| 543 | ✗ | CloseLocked("stream error"); | |
| 544 | ✗ | m_state = State::Failed; | |
| 545 | ✗ | m_failedMs = now; | |
| 546 | ✗ | m_stateChanged.notify_all(); | |
| 547 | ✗ | } | |
| 548 |
1/2✓ Branch 0 taken 27143 times.
✗ Branch 1 not taken.
|
27143 | gst_object_unref(bus); |
| 549 | } | ||
| 550 | |||
| 551 |
8/8✓ Branch 0 taken 27484 times.
✓ Branch 1 taken 202 times.
✓ Branch 2 taken 27143 times.
✓ Branch 3 taken 341 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 27142 times.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 27685 times.
|
27686 | if (m_state != State::Closed && m_state != State::Failed && m_wantTarget != m_target) { |
| 552 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | LogInfo(VB_MEDIAOUT, "PipeWire output stream %d: target changed '%s' -> '%s', reopening\n", |
| 553 | m_slot, m_target.c_str(), m_wantTarget.c_str()); | ||
| 554 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | CloseLocked("target changed"); |
| 555 | 1 | m_wantOpen = true; | |
| 556 | } | ||
| 557 | |||
| 558 |
6/6✓ Branch 0 taken 27483 times.
✓ Branch 1 taken 203 times.
✓ Branch 2 taken 341 times.
✓ Branch 3 taken 27142 times.
✓ Branch 4 taken 203 times.
✓ Branch 5 taken 341 times.
|
27686 | if ((m_state == State::Closed || m_state == State::Failed) && m_wantOpen) { |
| 559 |
3/4✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 50 times.
✓ Branch 3 taken 153 times.
|
203 | if (!OpenLocked(lock)) { |
| 560 | 50 | m_wantOpen = false; | |
| 561 | 50 | m_state = State::Failed; | |
| 562 |
1/2✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
|
50 | m_failedMs = NowMs(); |
| 563 | 50 | m_stateChanged.notify_all(); | |
| 564 | } | ||
| 565 | 203 | return; | |
| 566 | } | ||
| 567 | |||
| 568 |
2/2✓ Branch 0 taken 24494 times.
✓ Branch 1 taken 2989 times.
|
27483 | if (m_state != State::Opening) { |
| 569 | 24494 | return; | |
| 570 | } | ||
| 571 | |||
| 572 | // Opening. Two things have to be true, and neither implies the other. | ||
| 573 | // Our stream must be linked into the graph -- until it is, pipewiresink | ||
| 574 | // drops whatever it is given. And the card must be RUNNING: PipeWire runs | ||
| 575 | // a newly linked chain on a placeholder driver for ~100 ms while the ALSA | ||
| 576 | // node is still opening, and audio fed then is consumed into nothing. | ||
| 577 | // | ||
| 578 | // If the card was idle when we opened, it starting is the proof of both: | ||
| 579 | // nothing else woke it. If something else (AES67, an input mix) already | ||
| 580 | // had it running, that proves nothing about us, so ask pw-link -- as for a | ||
| 581 | // sink that reaches no card at all (a group of network senders only), | ||
| 582 | // where there is nothing to watch but the link. | ||
| 583 | // | ||
| 584 | // (The stream's GstPipeWireClock would be the natural thing to watch, but | ||
| 585 | // it does not advance at all for a follower of an ALSA-driven graph -- | ||
| 586 | // pw_stream_get_time_n() reports a zero rate and the clock returns its last | ||
| 587 | // value -- so nothing here runs on it.) | ||
| 588 | 2989 | bool ready = false; | |
| 589 | // | ||
| 590 | // No settling time after the card starts is needed: a cold-start sweep on | ||
| 591 | // an AM335x with a USB card, checking what actually crossed the bus to the | ||
| 592 | // card, kept the head of the track intact with none, and 25-400 ms bought | ||
| 593 | // nothing. | ||
| 594 |
6/6✓ Branch 0 taken 319 times.
✓ Branch 1 taken 2670 times.
✓ Branch 2 taken 134 times.
✓ Branch 3 taken 185 times.
✓ Branch 4 taken 134 times.
✓ Branch 5 taken 2855 times.
|
2989 | if (!m_cardStatusPath.empty() && !m_cardWasRunning) { |
| 595 |
1/2✓ Branch 0 taken 134 times.
✗ Branch 1 not taken.
|
134 | ready = CardRunning(); |
| 596 | } else { | ||
| 597 |
7/8✓ Branch 0 taken 773 times.
✓ Branch 1 taken 2082 times.
✓ Branch 2 taken 773 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 256 times.
✓ Branch 5 taken 517 times.
✓ Branch 6 taken 256 times.
✓ Branch 7 taken 2599 times.
|
2855 | if (!m_linked && NowMs() - m_lastLinkCheckMs >= (uint64_t)kLinkCheckIntervalMs) { |
| 598 | // pw-link talks to the daemon; don't hold the lock the feed and | ||
| 599 | // Acquire() need while it does. | ||
| 600 |
3/6✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 256 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 256 times.
✗ Branch 5 not taken.
|
256 | GstElement* pipeline = GST_ELEMENT(gst_object_ref(m_pipeline)); |
| 601 |
1/2✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
|
256 | lock.unlock(); |
| 602 |
1/2✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
|
256 | bool linked = StreamLinked(); |
| 603 |
1/2✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
|
256 | lock.lock(); |
| 604 | 256 | bool same = (m_pipeline == pipeline); | |
| 605 |
1/2✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
|
256 | gst_object_unref(pipeline); |
| 606 |
2/4✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 256 times.
|
256 | if (!same || m_state != State::Opening) { |
| 607 | ✗ | return; | |
| 608 | } | ||
| 609 |
1/2✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
|
256 | m_lastLinkCheckMs = NowMs(); |
| 610 |
2/2✓ Branch 0 taken 136 times.
✓ Branch 1 taken 120 times.
|
256 | if (linked) { |
| 611 | 136 | m_linked = true; | |
| 612 |
1/2✓ Branch 0 taken 136 times.
✗ Branch 1 not taken.
|
136 | m_linkedMs = NowMs(); |
| 613 | } | ||
| 614 | } | ||
| 615 |
7/8✓ Branch 0 taken 2218 times.
✓ Branch 1 taken 637 times.
✓ Branch 2 taken 2218 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 133 times.
✓ Branch 5 taken 2085 times.
✓ Branch 6 taken 8 times.
✓ Branch 7 taken 125 times.
|
2996 | ready = m_linked && NowMs() - m_linkedMs >= (uint64_t)kNoCardSettleMs && |
| 616 |
2/4✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
|
141 | (m_cardStatusPath.empty() || CardRunning()); |
| 617 | } | ||
| 618 |
1/2✓ Branch 0 taken 2989 times.
✗ Branch 1 not taken.
|
2989 | now = NowMs(); |
| 619 |
2/2✓ Branch 0 taken 147 times.
✓ Branch 1 taken 2842 times.
|
2989 | if (ready) { |
| 620 | 147 | m_state = State::Ready; | |
| 621 |
1/2✓ Branch 0 taken 147 times.
✗ Branch 1 not taken.
|
147 | LogInfo(VB_MEDIAOUT, "PipeWire output stream %d ready on '%s' after %d ms\n", |
| 622 | m_slot, m_target.c_str(), (int)(now - m_openStartedMs)); | ||
| 623 | 147 | m_stateChanged.notify_all(); | |
| 624 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2842 times.
|
2842 | } else if (now - m_openStartedMs > (uint64_t)kOpenTimeoutMs) { |
| 625 | ✗ | LogWarn(VB_MEDIAOUT, "PipeWire output stream %d: not running after %d ms (linked %s, card %s), giving up\n", | |
| 626 | m_slot, kOpenTimeoutMs, m_linked ? "yes" : "no", | ||
| 627 | m_cardStatusPath.empty() ? "n/a" : (CardRunning() ? "running" : "not running")); | ||
| 628 | ✗ | m_wantOpen = false; | |
| 629 | ✗ | CloseLocked("open timed out"); | |
| 630 | ✗ | m_state = State::Failed; | |
| 631 | ✗ | m_failedMs = now; | |
| 632 | ✗ | m_stateChanged.notify_all(); | |
| 633 | } | ||
| 634 |
2/2✓ Branch 0 taken 2989 times.
✓ Branch 1 taken 25513 times.
|
28502 | } |
| 635 | |||
| 636 | 203 | bool PipeWireOutputStream::OpenLocked(std::unique_lock<std::mutex>& lock) { | |
| 637 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | std::string target = m_wantTarget; |
| 638 | |||
| 639 | // Nothing below touches this object's state until the pipeline is | ||
| 640 | // running, and all of it can be slow -- gst_init() on first use (~20 s | ||
| 641 | // the first time a box ever runs GStreamer), and pipewiresink connecting | ||
| 642 | // to the daemon -- so none of it runs under the slot lock, which the feed | ||
| 643 | // callback and Acquire() on the playlist thread both need. | ||
| 644 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | lock.unlock(); |
| 645 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | GStreamerOutput::EnsureGStreamerInit(); |
| 646 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | int rate = GStreamerOutput::PipeWireGraphRate(); |
| 647 |
2/4✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 203 times.
✗ Branch 3 not taken.
|
203 | std::string capsStr = "audio/x-raw" + CapsFields(); |
| 648 | 203 | std::string launch = std::string("appsrc name=src is-live=true format=time do-timestamp=false block=false " | |
| 649 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
406 | "max-time=1000000000 leaky-type=downstream caps=\"") + |
| 650 |
2/4✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 203 times.
✗ Branch 3 not taken.
|
203 | capsStr + "\" ! pipewiresink name=pwsink sync=false"; |
| 651 | 203 | GError* error = nullptr; | |
| 652 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | GstElement* pipeline = gst_parse_launch(launch.c_str(), &error); |
| 653 |
2/4✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 203 times.
|
203 | if (error || !pipeline) { |
| 654 | ✗ | LogErr(VB_MEDIAOUT, "PipeWire output stream %d: could not build: %s\n", m_slot, | |
| 655 | error ? error->message : "unknown error"); | ||
| 656 | ✗ | if (error) { | |
| 657 | ✗ | g_error_free(error); | |
| 658 | } | ||
| 659 | ✗ | if (pipeline) { | |
| 660 | ✗ | gst_object_unref(pipeline); | |
| 661 | } | ||
| 662 | ✗ | lock.lock(); | |
| 663 | ✗ | return false; | |
| 664 | } | ||
| 665 | // The identity every per-track pipewiresink used to present, so anything | ||
| 666 | // that matches or routes fppd_stream_N sees no difference. | ||
| 667 |
4/8✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 203 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 203 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 203 times.
✗ Branch 7 not taken.
|
203 | if (GstElement* pwsink = gst_bin_get_by_name(GST_BIN(pipeline), "pwsink")) { |
| 668 | // The target is a property, not part of the description: it comes | ||
| 669 | // from the PipeWireSinkName setting, and pasted in, a crafted value | ||
| 670 | // added elements to the pipeline. | ||
| 671 |
2/2✓ Branch 0 taken 152 times.
✓ Branch 1 taken 51 times.
|
203 | if (!target.empty()) { |
| 672 |
1/2✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
|
152 | g_object_set(pwsink, "target-object", target.c_str(), nullptr); |
| 673 | } | ||
| 674 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | std::string nodeName = StreamSlotManager::GetNodeName(m_slot); |
| 675 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | std::string nodeDesc = StreamSlotManager::GetNodeDescription(m_slot); |
| 676 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | GstStructure* props = gst_structure_new("props", |
| 677 | "node.name", G_TYPE_STRING, nodeName.c_str(), | ||
| 678 | "node.description", G_TYPE_STRING, nodeDesc.c_str(), | ||
| 679 | NULL); | ||
| 680 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | g_object_set(pwsink, "stream-properties", props, NULL); |
| 681 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | gst_structure_free(props); |
| 682 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | gst_object_unref(pwsink); |
| 683 | 203 | } | |
| 684 |
3/6✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 203 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 203 times.
✗ Branch 5 not taken.
|
203 | GstElement* src = gst_bin_get_by_name(GST_BIN(pipeline), "src"); |
| 685 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | std::string cardStatus = GStreamerOutput::AlsaSinkStatusPath(target); |
| 686 |
4/4✓ Branch 0 taken 22 times.
✓ Branch 1 taken 181 times.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 14 times.
|
225 | bool cardWasRunning = !cardStatus.empty() && |
| 687 |
3/6✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✓ Branch 3 taken 181 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
225 | GetFileContents(cardStatus).find("state: RUNNING") != std::string::npos; |
| 688 | |||
| 689 | // The card clock, like every pipeline feeding this one, so that the base | ||
| 690 | // times OnFeedSample() maps between are on one clock (see the note in | ||
| 691 | // Service() on why not pipewiresink's own). | ||
| 692 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | GstClock* clock = Clock(); |
| 693 |
3/6✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 203 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 203 times.
✗ Branch 5 not taken.
|
203 | gst_pipeline_use_clock(GST_PIPELINE(pipeline), clock); |
| 694 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | gst_object_unref(clock); |
| 695 | |||
| 696 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | uint64_t started = NowMs(); |
| 697 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | GstStateChangeReturn ret = gst_element_set_state(pipeline, GST_STATE_PLAYING); |
| 698 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | lock.lock(); |
| 699 | |||
| 700 |
2/2✓ Branch 0 taken 50 times.
✓ Branch 1 taken 153 times.
|
203 | if (ret == GST_STATE_CHANGE_FAILURE) { |
| 701 |
1/2✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
|
50 | LogErr(VB_MEDIAOUT, "PipeWire output stream %d: could not start on '%s'\n", m_slot, target.c_str()); |
| 702 |
1/2✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
|
50 | gst_object_unref(src); |
| 703 | 50 | std::thread([pipeline]() { | |
| 704 | 50 | gst_element_set_state(pipeline, GST_STATE_NULL); | |
| 705 | 50 | gst_object_unref(pipeline); | |
| 706 |
2/4✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 50 times.
✗ Branch 3 not taken.
|
50 | }).detach(); |
| 707 | 50 | return false; | |
| 708 | } | ||
| 709 | |||
| 710 | 153 | m_pipeline = pipeline; | |
| 711 | 153 | m_src = src; | |
| 712 |
1/2✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
|
153 | GstCaps* fixedCaps = gst_caps_from_string(capsStr.c_str()); |
| 713 |
1/2✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
|
153 | gst_caps_replace(&m_caps, fixedCaps); |
| 714 |
1/2✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
|
153 | gst_caps_unref(fixedCaps); |
| 715 | 153 | m_rate = rate; | |
| 716 | 153 | m_warnedCaps = false; | |
| 717 | 153 | m_queuedUntilUs = 0; | |
| 718 | // The silence that connects the stream counts: the first track is | ||
| 719 | // flushed too, so it starts from the same state as every later one. | ||
| 720 | 153 | m_flushed = false; | |
| 721 |
1/2✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
|
153 | m_target = target; |
| 722 |
1/2✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
|
153 | m_cardStatusPath = cardStatus; |
| 723 | 153 | m_cardWasRunning = cardWasRunning; | |
| 724 | 153 | m_openStartedMs = started; | |
| 725 | 153 | m_linked = false; | |
| 726 | 153 | m_linkedMs = 0; | |
| 727 | 153 | m_lastLinkCheckMs = 0; | |
| 728 | 153 | m_state = State::Opening; | |
| 729 |
5/6✓ Branch 0 taken 131 times.
✓ Branch 1 taken 22 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 152 times.
✓ Branch 4 taken 153 times.
✗ Branch 5 not taken.
|
153 | LogInfo(VB_MEDIAOUT, "PipeWire output stream %d opening on '%s' (card status %s)\n", m_slot, |
| 730 | target.empty() ? "(default)" : target.c_str(), cardStatus.empty() ? "n/a" : cardStatus.c_str()); | ||
| 731 | // pipewiresink only connects its stream once it has caps, which arrive | ||
| 732 | // with the first buffer. | ||
| 733 |
1/2✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
|
153 | PushSilenceLocked(20); |
| 734 | 153 | return true; | |
| 735 | 203 | } | |
| 736 | |||
| 737 | 2308 | void PipeWireOutputStream::PushSilenceLocked(int ms) { | |
| 738 |
3/4✓ Branch 0 taken 2306 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2306 times.
|
2308 | if (!m_src || m_rate <= 0) { |
| 739 | 2 | return; | |
| 740 | } | ||
| 741 | 2306 | size_t frames = (size_t)m_rate * ms / 1000; | |
| 742 | 2306 | GstBuffer* buf = gst_buffer_new_and_alloc(frames * kBytesPerFrame); | |
| 743 | 2306 | gst_buffer_memset(buf, 0, 0, frames * kBytesPerFrame); | |
| 744 | 2306 | GstClockTime now = gst_clock_get_time(m_clock); | |
| 745 | 2306 | GstClockTime base = gst_element_get_base_time(m_pipeline); | |
| 746 |
1/2✓ Branch 0 taken 2306 times.
✗ Branch 1 not taken.
|
2306 | GST_BUFFER_PTS(buf) = now > base ? now - base : 0; |
| 747 | 2306 | GST_BUFFER_DURATION(buf) = gst_util_uint64_scale(frames, GST_SECOND, m_rate); | |
| 748 | 2306 | NoteQueuedLocked(frames * kBytesPerFrame); | |
| 749 | 2306 | gst_app_src_push_buffer(GST_APP_SRC(m_src), buf); | |
| 750 | } | ||
| 751 | |||
| 752 | // Whether this slot's stream has an output port linked to anything, per | ||
| 753 | // pw-link. Only run while a stream is opening, every kLinkCheckIntervalMs. | ||
| 754 | // Time-limited, like the other PipeWire tools fppd runs: a pw-link that never | ||
| 755 | // answers (a wedged daemon) would otherwise hold this worker for good, and | ||
| 756 | // shutdown joins it. | ||
| 757 | 256 | bool PipeWireOutputStream::StreamLinked() { | |
| 758 |
2/4✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 256 times.
✗ Branch 3 not taken.
|
256 | std::string prefix = StreamSlotManager::GetNodeName(m_slot) + ":"; |
| 759 |
1/2✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
|
256 | FILE* f = popen("timeout -k 1 2 pw-link -o -l 2>/dev/null", "r"); |
| 760 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 256 times.
|
256 | if (!f) { |
| 761 | ✗ | return false; | |
| 762 | } | ||
| 763 | // Output ports are listed at column 0, each followed by its links | ||
| 764 | // indented as " |-> peer:port". | ||
| 765 | 256 | bool ours = false; | |
| 766 | 256 | bool linked = false; | |
| 767 | char line[512]; | ||
| 768 |
3/4✓ Branch 0 taken 2273 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2017 times.
✓ Branch 3 taken 256 times.
|
2273 | while (fgets(line, sizeof(line), f)) { |
| 769 |
2/2✓ Branch 0 taken 1396 times.
✓ Branch 1 taken 621 times.
|
2017 | if (line[0] != ' ') { |
| 770 | 1396 | ours = strncmp(line, prefix.c_str(), prefix.size()) == 0; | |
| 771 |
3/4✓ Branch 0 taken 291 times.
✓ Branch 1 taken 330 times.
✓ Branch 2 taken 291 times.
✗ Branch 3 not taken.
|
621 | } else if (ours && strstr(line, "|->")) { |
| 772 | 291 | linked = true; | |
| 773 | } | ||
| 774 | } | ||
| 775 |
1/2✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
|
256 | pclose(f); |
| 776 | 256 | return linked; | |
| 777 | 256 | } | |
| 778 | |||
| 779 | // --------------------------------------------------------------------------- | ||
| 780 | // The card clock | ||
| 781 | // | ||
| 782 | // Nothing in this stream resamples: PipeWire takes the stream's samples at | ||
| 783 | // the card's pace, which is whatever the card's crystal makes it -- 432 ppm | ||
| 784 | // fast on an AM62x I2S cape, 47 ppm slow on a Pi 5 HAT, tens of ppm either way | ||
| 785 | // for USB. A feed paced by the system clock delivers at the system's pace | ||
| 786 | // instead. A fast card then drains the stream's cushion (one quantum) and | ||
| 787 | // plays a quantum of silence: on the 432 ppm cape, 12 ms every 27 s. A slow | ||
| 788 | // one builds a queue that delays the sound further behind the lights for as | ||
| 789 | // long as the stream is fed. | ||
| 790 | // | ||
| 791 | // So every pipeline feeding the stream runs on this clock, which is held to | ||
| 792 | // the card. Its rate is fitted to the card's own hardware pointer (hw_ptr in | ||
| 793 | // /proc/asound/.../status, which ALSA updates from the DMA position when the | ||
| 794 | // file is read) against CLOCK_MONOTONIC, the clock's internal time, over the | ||
| 795 | // last two minutes. On top of that, any accumulated phase error is closed | ||
| 796 | // over ~20 s, so the queue between the feed and the card stays where it | ||
| 797 | // started instead of wandering with every error in the fit. The media | ||
| 798 | // position the lights follow comes from the feed, so it is card time too; | ||
| 799 | // ClockRate() lets the channel output thread run its frames at the same pace. | ||
| 800 | // | ||
| 801 | // Without a card (a group of network sinks) the clock is left at the system | ||
| 802 | // rate. | ||
| 803 | // --------------------------------------------------------------------------- | ||
| 804 | |||
| 805 | 2084 | void PipeWireOutputStream::EnsureClocksLocked() { | |
| 806 |
2/2✓ Branch 0 taken 163 times.
✓ Branch 1 taken 1921 times.
|
2084 | if (!m_clock) { |
| 807 |
1/2✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
|
163 | std::string name = "fpp-card-clock-" + std::to_string(m_slot); |
| 808 |
4/8✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 163 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 163 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 163 times.
✗ Branch 7 not taken.
|
163 | m_clock = GST_CLOCK(g_object_new(GST_TYPE_SYSTEM_CLOCK, "name", name.c_str(), |
| 809 | "clock-type", GST_CLOCK_TYPE_MONOTONIC, NULL)); | ||
| 810 |
1/2✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
|
163 | gst_object_ref_sink(m_clock); |
| 811 | 163 | } | |
| 812 |
2/2✓ Branch 0 taken 163 times.
✓ Branch 1 taken 1921 times.
|
2084 | if (!m_feedClock) { |
| 813 | // Both read CLOCK_MONOTONIC as their internal time, and start out | ||
| 814 | // calibrated alike, so at speed 1 the feed clock reads exactly what | ||
| 815 | // the card clock does. | ||
| 816 |
1/2✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
|
163 | std::string name = "fpp-feed-clock-" + std::to_string(m_slot); |
| 817 |
4/8✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 163 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 163 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 163 times.
✗ Branch 7 not taken.
|
163 | m_feedClock = GST_CLOCK(g_object_new(GST_TYPE_SYSTEM_CLOCK, "name", name.c_str(), |
| 818 | "clock-type", GST_CLOCK_TYPE_MONOTONIC, NULL)); | ||
| 819 |
1/2✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
|
163 | gst_object_ref_sink(m_feedClock); |
| 820 | GstClockTime cInt, cExt, num, den; | ||
| 821 |
1/2✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
|
163 | gst_clock_get_calibration(m_clock, &cInt, &cExt, &num, &den); |
| 822 |
1/2✓ Branch 0 taken 163 times.
✗ Branch 1 not taken.
|
163 | gst_clock_set_calibration(m_feedClock, cInt, cExt, num, den); |
| 823 | 163 | } | |
| 824 | 2084 | } | |
| 825 | |||
| 826 | 203 | GstClock* PipeWireOutputStream::Clock() { | |
| 827 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | std::unique_lock<std::mutex> lock(m_lock); |
| 828 |
1/2✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
|
203 | EnsureClocksLocked(); |
| 829 |
3/6✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 203 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 203 times.
✗ Branch 5 not taken.
|
406 | return GST_CLOCK(gst_object_ref(m_clock)); |
| 830 | 203 | } | |
| 831 | |||
| 832 | 1881 | GstClock* PipeWireOutputStream::FeedClock() { | |
| 833 |
1/2✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
|
1881 | std::unique_lock<std::mutex> lock(m_lock); |
| 834 |
1/2✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
|
1881 | EnsureClocksLocked(); |
| 835 |
3/6✓ Branch 0 taken 1881 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1881 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1881 times.
✗ Branch 5 not taken.
|
3762 | return GST_CLOCK(gst_object_ref(m_feedClock)); |
| 836 | 1881 | } | |
| 837 | |||
| 838 | 10842 | void PipeWireOutputStream::RecalibrateFeedClockLocked(GstClockTime internal) { | |
| 839 |
2/4✓ Branch 0 taken 10842 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 10842 times.
|
10842 | if (!m_clock || !m_feedClock) { |
| 840 | 6966 | return; | |
| 841 | } | ||
| 842 | GstClockTime cInt, cExt, num, den; | ||
| 843 |
1/2✓ Branch 0 taken 10842 times.
✗ Branch 1 not taken.
|
10842 | gst_clock_get_calibration(m_clock, &cInt, &cExt, &num, &den); |
| 844 |
2/2✓ Branch 0 taken 6966 times.
✓ Branch 1 taken 3876 times.
|
10842 | if (!m_feedDiverged) { |
| 845 | // Not yet steered: keep it the card clock exactly, no rounding. | ||
| 846 |
1/2✓ Branch 0 taken 6966 times.
✗ Branch 1 not taken.
|
6966 | gst_clock_set_calibration(m_feedClock, cInt, cExt, num, den); |
| 847 | 6966 | return; | |
| 848 | } | ||
| 849 | // Where the feed clock is now on its current calibration, from here on | ||
| 850 | // running at the card clock's rate times the speed: continuous at | ||
| 851 | // `internal`, so a feed waiting on it never sees it step. | ||
| 852 | GstClockTime fInt, fExt, fNum, fDen; | ||
| 853 |
1/2✓ Branch 0 taken 3876 times.
✗ Branch 1 not taken.
|
3876 | gst_clock_get_calibration(m_feedClock, &fInt, &fExt, &fNum, &fDen); |
| 854 |
1/2✓ Branch 0 taken 3876 times.
✗ Branch 1 not taken.
|
3876 | GstClockTime external = gst_clock_adjust_with_calibration(m_feedClock, internal, fInt, fExt, fNum, fDen); |
| 855 |
1/2✓ Branch 0 taken 3876 times.
✗ Branch 1 not taken.
|
3876 | double rate = (double)num / (double)(den ? den : 1) * m_feedSpeed; |
| 856 |
1/2✓ Branch 0 taken 3876 times.
✗ Branch 1 not taken.
|
3876 | gst_clock_set_calibration(m_feedClock, internal, external, (GstClockTime)llround(rate * 1e9), 1000000000ULL); |
| 857 | } | ||
| 858 | |||
| 859 | 968 | void PipeWireOutputStream::SetFeedSpeed(GstElement* feed, double speed) { | |
| 860 |
1/2✓ Branch 0 taken 968 times.
✗ Branch 1 not taken.
|
968 | std::unique_lock<std::mutex> lock(m_lock); |
| 861 |
5/10✓ Branch 0 taken 968 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 968 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 968 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 968 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 968 times.
|
968 | if (!feed || feed != m_feed || !m_clock || !m_feedClock || speed <= 0.0) { |
| 862 | ✗ | return; | |
| 863 | } | ||
| 864 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 968 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
968 | if (speed == m_feedSpeed && m_feedDiverged) { |
| 865 | ✗ | return; | |
| 866 | } | ||
| 867 | // A speed of 1 before any other leaves everything untouched. | ||
| 868 |
3/4✓ Branch 0 taken 16 times.
✓ Branch 1 taken 952 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 16 times.
|
968 | if (speed == 1.0 && !m_feedDiverged) { |
| 869 | ✗ | return; | |
| 870 | } | ||
| 871 |
1/2✓ Branch 0 taken 968 times.
✗ Branch 1 not taken.
|
968 | GstClockTime internal = gst_clock_get_internal_time(m_clock); |
| 872 |
2/2✓ Branch 0 taken 15 times.
✓ Branch 1 taken 953 times.
|
968 | if (!m_feedDiverged) { |
| 873 | 15 | m_feedDiverged = true; | |
| 874 | 15 | m_rsActive = true; | |
| 875 | // The next output frame is the next buffer's first: everything up to | ||
| 876 | // it has gone out as it came, and m_rsHist holds the frames before. | ||
| 877 | 15 | m_rsPos = 0.0; | |
| 878 | } | ||
| 879 | // Recalibrate at the old speed first (a no-op continuation), then switch. | ||
| 880 |
1/2✓ Branch 0 taken 968 times.
✗ Branch 1 not taken.
|
968 | RecalibrateFeedClockLocked(internal); |
| 881 | 968 | m_feedSpeed = speed; | |
| 882 |
1/2✓ Branch 0 taken 968 times.
✗ Branch 1 not taken.
|
968 | RecalibrateFeedClockLocked(internal); |
| 883 |
1/2✓ Branch 0 taken 968 times.
✗ Branch 1 not taken.
|
968 | } |
| 884 | |||
| 885 | 16 | void PipeWireOutputStream::FlushFeed(GstElement* feed) { | |
| 886 | { | ||
| 887 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | std::unique_lock<std::mutex> lock(m_lock); |
| 888 |
2/4✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 16 times.
|
16 | if (!feed || feed != m_feed) { |
| 889 | ✗ | return; | |
| 890 | } | ||
| 891 | // What comes next doesn't follow on from what was resampled before. | ||
| 892 | 16 | m_rsHistFrames = 0; | |
| 893 | 16 | m_rsPos = 0.0; | |
| 894 |
1/2✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
|
16 | } |
| 895 | // Unprimed, as for an xrun: the seek is already a jump in the sound, and | ||
| 896 | // priming would only put silence ahead of where the feed lands. | ||
| 897 | 16 | Flush(false); | |
| 898 | } | ||
| 899 | |||
| 900 | // The feed's resampler (SetFeedSpeed()): a 16-tap Kaiser-windowed sinc, its | ||
| 901 | // fractional delay taken from a 256-phase table with linear interpolation | ||
| 902 | // between phases. A speed servo only moves the ratio a few tenths of a | ||
| 903 | // percent from 1, but every sample then sits at a fraction that sweeps | ||
| 904 | // through the whole range, and a short interpolator's response changes with | ||
| 905 | // that fraction: a 4-point cubic measured -33 dB THD+N on an 8 kHz tone, | ||
| 906 | // audible as warbling highs. This one measures -81 dB at 1 kHz, -88 dB at | ||
| 907 | // 8 kHz and -74 dB at 15 kHz (44.1 kHz, ratios 0.9995-1.003), and with NEON | ||
| 908 | // costs 1.7% of a BBB's core (0.3% of a PocketBeagle2's) for 44.1 kHz stereo. | ||
| 909 | namespace { | ||
| 910 | constexpr int kRsTaps = 16; | ||
| 911 | constexpr int kRsHalf = kRsTaps / 2; | ||
| 912 | constexpr int kRsPhases = 256; | ||
| 913 | constexpr int kRsHist = kRsTaps - 1; // frames before the next buffer it reaches back to | ||
| 914 | constexpr double kRsKaiserBeta = 7.0; | ||
| 915 | |||
| 916 | 12434 | const float* ResampleTable() { | |
| 917 | 20 | static const std::vector<float> table = [] { | |
| 918 | 82200 | auto bessel0 = [](double x) { | |
| 919 | 82200 | double sum = 1.0, term = 1.0; | |
| 920 |
2/2✓ Branch 0 taken 2383800 times.
✓ Branch 1 taken 82200 times.
|
2466000 | for (int k = 1; k < 30; k++) { |
| 921 | 2383800 | term *= (x / (2 * k)) * (x / (2 * k)); | |
| 922 | 2383800 | sum += term; | |
| 923 | } | ||
| 924 | 82200 | return sum; | |
| 925 | }; | ||
| 926 |
1/2✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
|
10 | std::vector<float> t((kRsPhases + 1) * kRsTaps); |
| 927 |
2/2✓ Branch 0 taken 2570 times.
✓ Branch 1 taken 10 times.
|
2580 | for (int ph = 0; ph <= kRsPhases; ph++) { |
| 928 | 2570 | double frac = (double)ph / kRsPhases; | |
| 929 | 2570 | double sum = 0.0; | |
| 930 |
2/2✓ Branch 0 taken 41120 times.
✓ Branch 1 taken 2570 times.
|
43690 | for (int k = 0; k < kRsTaps; k++) { |
| 931 | // Tap k reads input frame i + k - (kRsHalf - 1). | ||
| 932 | 41120 | double x = (double)(k - (kRsHalf - 1)) - frac; | |
| 933 |
2/2✓ Branch 0 taken 41100 times.
✓ Branch 1 taken 20 times.
|
41120 | double sinc = x == 0.0 ? 1.0 : std::sin(M_PI * x) / (M_PI * x); |
| 934 | 41120 | double r = x / kRsHalf; | |
| 935 |
2/2✓ Branch 0 taken 20 times.
✓ Branch 1 taken 41100 times.
|
41120 | double w = std::fabs(r) >= 1.0 ? 0.0 : bessel0(kRsKaiserBeta * std::sqrt(1.0 - r * r)) / bessel0(kRsKaiserBeta); |
| 936 | 41120 | t[ph * kRsTaps + k] = (float)(sinc * w); | |
| 937 | 41120 | sum += sinc * w; | |
| 938 | } | ||
| 939 |
2/2✓ Branch 0 taken 41120 times.
✓ Branch 1 taken 2570 times.
|
43690 | for (int k = 0; k < kRsTaps; k++) { |
| 940 | 41120 | t[ph * kRsTaps + k] = (float)(t[ph * kRsTaps + k] / sum); // unity gain at DC | |
| 941 | } | ||
| 942 | } | ||
| 943 | 10 | return t; | |
| 944 |
4/8✓ Branch 0 taken 10 times.
✓ Branch 1 taken 12424 times.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
12434 | }(); |
| 945 | 12434 | return table.data(); | |
| 946 | } | ||
| 947 | } | ||
| 948 | |||
| 949 | 53322 | void PipeWireOutputStream::KeepResampleHistoryLocked(GstBuffer* in) { | |
| 950 | GstMapInfo map; | ||
| 951 |
2/4✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 53322 times.
|
53322 | if (!gst_buffer_map(in, &map, GST_MAP_READ)) { |
| 952 | ✗ | return; | |
| 953 | } | ||
| 954 | 53322 | const float* x = reinterpret_cast<const float*>(map.data); | |
| 955 | 53322 | int n = (int)(map.size / kBytesPerFrame); | |
| 956 | 53322 | int take = std::min(n, kRsHist); | |
| 957 | // Shift out what the new frames replace, then append them. | ||
| 958 | 53322 | int keep = std::min(m_rsHistFrames, kRsHist - take); | |
| 959 | 53322 | std::memmove(m_rsHist, m_rsHist + (m_rsHistFrames - keep) * kChannels, keep * kChannels * sizeof(float)); | |
| 960 | 53322 | std::memcpy(m_rsHist + keep * kChannels, x + (n - take) * kChannels, take * kChannels * sizeof(float)); | |
| 961 | 53322 | m_rsHistFrames = keep + take; | |
| 962 |
1/2✓ Branch 0 taken 53322 times.
✗ Branch 1 not taken.
|
53322 | gst_buffer_unmap(in, &map); |
| 963 | } | ||
| 964 | |||
| 965 | 12434 | GstBuffer* PipeWireOutputStream::ResampleLocked(GstBuffer* in) { | |
| 966 | static_assert(sizeof(m_rsHist) == kRsHist * kChannels * sizeof(float), "m_rsHist holds kRsHist frames"); | ||
| 967 | GstMapInfo im; | ||
| 968 |
2/4✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 12434 times.
|
12434 | if (!gst_buffer_map(in, &im, GST_MAP_READ)) { |
| 969 | ✗ | return nullptr; | |
| 970 | } | ||
| 971 | 12434 | const float* x = reinterpret_cast<const float*>(im.data); | |
| 972 | 12434 | const long n = (long)(im.size / kBytesPerFrame); | |
| 973 | 12434 | const double step = m_feedSpeed; | |
| 974 |
1/2✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
|
12434 | const float* table = ResampleTable(); |
| 975 | // Input frame i of this buffer, or of the history for i in -kRsHist..-1 | ||
| 976 | // (a frame before the first of the track reads as silence). | ||
| 977 | 12434 | const int hist = m_rsHistFrames; | |
| 978 | 5962208 | auto at = [&](long i, int c) -> float { | |
| 979 |
2/2✓ Branch 0 taken 2982616 times.
✓ Branch 1 taken 2979592 times.
|
5962208 | if (i >= 0) { |
| 980 | 2982616 | return x[i * kChannels + c]; | |
| 981 | } | ||
| 982 | 2979592 | long h = hist + i; | |
| 983 |
2/2✓ Branch 0 taken 2978696 times.
✓ Branch 1 taken 896 times.
|
2979592 | return h >= 0 ? m_rsHist[h * kChannels + c] : 0.0f; |
| 984 | 12434 | }; | |
| 985 | 12434 | size_t cap = (size_t)((double)(n + kRsHist) / step) + 4; | |
| 986 |
1/2✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
|
12434 | GstBuffer* out = gst_buffer_new_allocate(nullptr, cap * kBytesPerFrame, nullptr); |
| 987 | GstMapInfo om; | ||
| 988 |
4/8✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12434 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 12434 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 12434 times.
|
12434 | if (!out || !gst_buffer_map(out, &om, GST_MAP_WRITE)) { |
| 989 | ✗ | gst_buffer_unmap(in, &im); | |
| 990 | ✗ | if (out) { | |
| 991 | ✗ | gst_buffer_unref(out); | |
| 992 | } | ||
| 993 | ✗ | return nullptr; | |
| 994 | } | ||
| 995 | 12434 | float* y = reinterpret_cast<float*>(om.data); | |
| 996 | 12434 | size_t m = 0; | |
| 997 | 12434 | double p = m_rsPos; | |
| 998 | float coef[kRsTaps]; | ||
| 999 |
1/2✓ Branch 0 taken 32532111 times.
✗ Branch 1 not taken.
|
32532111 | while (m < cap) { |
| 1000 | 32532111 | long i = (long)std::floor(p); | |
| 1001 |
2/2✓ Branch 0 taken 12434 times.
✓ Branch 1 taken 32519677 times.
|
32532111 | if (i + kRsHalf > n - 1) { |
| 1002 | 12434 | break; | |
| 1003 | } | ||
| 1004 | 32519677 | double fph = (p - (double)i) * kRsPhases; | |
| 1005 | 32519677 | int ph = std::min((int)fph, kRsPhases - 1); | |
| 1006 | 32519677 | float fr = (float)(fph - ph); | |
| 1007 | 32519677 | const float* c0 = table + ph * kRsTaps; | |
| 1008 | 32519677 | const float* c1 = c0 + kRsTaps; | |
| 1009 | 32519677 | long first = i - (kRsHalf - 1); | |
| 1010 | #if defined(__ARM_NEON) || defined(__ARM_NEON__) | ||
| 1011 |
2/2✓ Branch 0 taken 31459217 times.
✓ Branch 1 taken 179286 times.
|
31638503 | if (first >= 0) { |
| 1012 | // Both channels four taps at a time: 1.7% of a BBB's core for | ||
| 1013 | // 44.1 kHz stereo, against 8.7% for the plain loop below (GCC | ||
| 1014 | // won't vectorize a float sum without -ffast-math). | ||
| 1015 | static_assert(kChannels == 2 && kRsTaps % 4 == 0, "the NEON path is stereo, four taps a step"); | ||
| 1016 | 31459217 | const float* xs = x + first * kChannels; | |
| 1017 | 31459217 | float32x4_t accL = vdupq_n_f32(0.0f); | |
| 1018 | 31459217 | float32x4_t accR = vdupq_n_f32(0.0f); | |
| 1019 |
2/2✓ Branch 0 taken 125836868 times.
✓ Branch 1 taken 31459217 times.
|
157296085 | for (int k = 0; k < kRsTaps; k += 4) { |
| 1020 |
0/2✗ Branch 0 not taken.
✗ Branch 1 not taken.
|
125836868 | float32x4_t a = vld1q_f32(c0 + k); |
| 1021 |
0/6✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
377510604 | float32x4_t co = vmlaq_n_f32(a, vsubq_f32(vld1q_f32(c1 + k), a), fr); |
| 1022 |
0/2✗ Branch 0 not taken.
✗ Branch 1 not taken.
|
125836868 | float32x4x2_t lr = vld2q_f32(xs + 2 * k); |
| 1023 |
0/2✗ Branch 0 not taken.
✗ Branch 1 not taken.
|
125836868 | accL = vmlaq_f32(accL, lr.val[0], co); |
| 1024 |
0/2✗ Branch 0 not taken.
✗ Branch 1 not taken.
|
251673736 | accR = vmlaq_f32(accR, lr.val[1], co); |
| 1025 | } | ||
| 1026 |
0/6✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
94377651 | float32x2_t l2 = vadd_f32(vget_low_f32(accL), vget_high_f32(accL)); |
| 1027 |
0/4✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
62918434 | float32x2_t r2 = vadd_f32(vget_low_f32(accR), vget_high_f32(accR)); |
| 1028 |
0/4✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
62918434 | vst1_f32(y + m * kChannels, vpadd_f32(l2, r2)); |
| 1029 | 31459217 | m++; | |
| 1030 | 31459217 | p += step; | |
| 1031 | 31459217 | continue; | |
| 1032 | 31459217 | } | |
| 1033 | #endif | ||
| 1034 |
2/2✓ Branch 0 taken 16967360 times.
✓ Branch 1 taken 1060460 times.
|
18027820 | for (int k = 0; k < kRsTaps; k++) { |
| 1035 | 16967360 | coef[k] = c0[k] + fr * (c1[k] - c0[k]); | |
| 1036 | } | ||
| 1037 |
2/2✓ Branch 0 taken 2120920 times.
✓ Branch 1 taken 1060460 times.
|
3181380 | for (int c = 0; c < kChannels; c++) { |
| 1038 | 2120920 | float acc = 0.0f; | |
| 1039 |
2/2✓ Branch 0 taken 1748282 times.
✓ Branch 1 taken 372638 times.
|
2120920 | if (first >= 0) { |
| 1040 | 1748282 | const float* xs = x + first * kChannels + c; | |
| 1041 |
2/2✓ Branch 0 taken 27972512 times.
✓ Branch 1 taken 1748282 times.
|
29720794 | for (int k = 0; k < kRsTaps; k++) { |
| 1042 | 27972512 | acc += coef[k] * xs[k * kChannels]; | |
| 1043 | } | ||
| 1044 | } else { | ||
| 1045 |
2/2✓ Branch 0 taken 5962208 times.
✓ Branch 1 taken 372638 times.
|
6334846 | for (int k = 0; k < kRsTaps; k++) { |
| 1046 | 5962208 | acc += coef[k] * at(first + k, c); | |
| 1047 | } | ||
| 1048 | } | ||
| 1049 | 2120920 | y[m * kChannels + c] = acc; | |
| 1050 | } | ||
| 1051 | 1060460 | m++; | |
| 1052 | 1060460 | p += step; | |
| 1053 | } | ||
| 1054 | // At the exit p >= n - kRsHalf, so the next buffer reaches back no | ||
| 1055 | // further than the history's kRsHist frames. | ||
| 1056 | 12434 | m_rsPos = p - (double)n; | |
| 1057 |
1/2✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
|
12434 | gst_buffer_unmap(out, &om); |
| 1058 |
1/2✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
|
12434 | gst_buffer_unmap(in, &im); |
| 1059 |
1/2✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
|
12434 | gst_buffer_set_size(out, (gssize)(m * kBytesPerFrame)); |
| 1060 |
1/2✓ Branch 0 taken 12434 times.
✗ Branch 1 not taken.
|
12434 | KeepResampleHistoryLocked(in); |
| 1061 | 12434 | return out; | |
| 1062 | } | ||
| 1063 | |||
| 1064 | 21404 | void PipeWireOutputStream::ResetClockSyncLocked() { | |
| 1065 | 21404 | m_syncAnchored = false; | |
| 1066 | 21404 | m_syncWindow.clear(); | |
| 1067 | 21404 | } | |
| 1068 | |||
| 1069 | // `trigger` is when the stream was last started (trigger_time), "" if the | ||
| 1070 | // file has none: a restart changes it, hw_ptr wrapping doesn't. | ||
| 1071 | 7029 | static bool ReadCardPosition(const std::string& path, int64_t& hw, std::string& trigger) { | |
| 1072 |
1/2✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
|
7029 | std::string status = GetFileContents(path); |
| 1073 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 7025 times.
|
7029 | if (status.find("state: RUNNING") == std::string::npos) { |
| 1074 | 4 | return false; | |
| 1075 | } | ||
| 1076 | 7025 | std::size_t p = status.find("hw_ptr"); | |
| 1077 |
3/6✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7025 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 7025 times.
|
7025 | if (p == std::string::npos || (p = status.find(':', p)) == std::string::npos) { |
| 1078 | ✗ | return false; | |
| 1079 | } | ||
| 1080 | 7025 | hw = strtoll(status.c_str() + p + 1, nullptr, 10); | |
| 1081 | 7025 | trigger.clear(); | |
| 1082 | 7025 | std::size_t t = status.find("trigger_time"); | |
| 1083 |
3/6✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7025 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7025 times.
✗ Branch 5 not taken.
|
7025 | if (t != std::string::npos && (t = status.find(':', t)) != std::string::npos) { |
| 1084 | 7025 | std::size_t e = status.find('\n', t); | |
| 1085 |
2/4✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7025 times.
✗ Branch 3 not taken.
|
7025 | trigger = status.substr(t + 1, e == std::string::npos ? std::string::npos : e - t - 1); |
| 1086 |
1/2✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
|
7025 | TrimWhiteSpace(trigger); |
| 1087 | } | ||
| 1088 | 7025 | return true; | |
| 1089 | 7029 | } | |
| 1090 | |||
| 1091 | // Where the card's hw_ptr wraps back to 0 (sw_params "boundary"), 0 if it | ||
| 1092 | // can't be read. ALSA picks the largest power-of-two multiple of the buffer | ||
| 1093 | // below LONG_MAX: 2^62 frames with a 64-bit kernel, but 2^30 with a 32-bit | ||
| 1094 | // one (a BBB) -- about 6 h at 48 kHz, which a card kept running across songs | ||
| 1095 | // reaches in a long show. | ||
| 1096 | 2 | static int64_t ReadCardBoundary(const std::string& statusPath) { | |
| 1097 |
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 | std::string text = GetFileContents(statusPath.substr(0, statusPath.rfind('/')) + "/sw_params"); |
| 1098 | 2 | std::size_t p = text.find("boundary"); | |
| 1099 |
3/6✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
|
2 | if (p == std::string::npos || (p = text.find(':', p)) == std::string::npos) { |
| 1100 | ✗ | return 0; | |
| 1101 | } | ||
| 1102 | 2 | return strtoll(text.c_str() + p + 1, nullptr, 10); | |
| 1103 | 2 | } | |
| 1104 | |||
| 1105 | 28502 | bool PipeWireOutputStream::DisciplineClock() { | |
| 1106 | 28502 | std::string path; | |
| 1107 | { | ||
| 1108 |
1/2✓ Branch 0 taken 28502 times.
✗ Branch 1 not taken.
|
28502 | std::unique_lock<std::mutex> lock(m_lock); |
| 1109 |
8/8✓ Branch 0 taken 28344 times.
✓ Branch 1 taken 158 times.
✓ Branch 2 taken 24166 times.
✓ Branch 3 taken 4178 times.
✓ Branch 4 taken 17042 times.
✓ Branch 5 taken 7124 times.
✓ Branch 6 taken 21378 times.
✓ Branch 7 taken 7124 times.
|
28502 | if (!m_clock || m_state != State::Ready || m_cardStatusPath.empty()) { |
| 1110 |
1/2✓ Branch 0 taken 21378 times.
✗ Branch 1 not taken.
|
21378 | ResetClockSyncLocked(); |
| 1111 | 21378 | return false; | |
| 1112 | } | ||
| 1113 |
1/2✓ Branch 0 taken 7124 times.
✗ Branch 1 not taken.
|
7124 | GstClockTime nowInt = gst_clock_get_internal_time(m_clock); |
| 1114 |
4/4✓ Branch 0 taken 7098 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 95 times.
✓ Branch 3 taken 7003 times.
|
7124 | if (m_syncAnchored && nowInt - m_syncLastInt < (GstClockTime)kClockSampleMs * GST_MSECOND) { |
| 1115 | 95 | return false; | |
| 1116 | } | ||
| 1117 |
1/2✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
|
7029 | path = m_cardStatusPath; |
| 1118 |
2/2✓ Branch 0 taken 22 times.
✓ Branch 1 taken 7007 times.
|
7029 | if (path != m_syncPath) { |
| 1119 | // Another card: what was learnt about the last one doesn't apply. | ||
| 1120 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | m_syncPath = path; |
| 1121 | 22 | m_syncCardRate = 0; | |
| 1122 | 22 | m_clockRate = 1.0; | |
| 1123 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | ResetClockSyncLocked(); |
| 1124 | } | ||
| 1125 |
2/2✓ Branch 0 taken 7029 times.
✓ Branch 1 taken 21473 times.
|
28502 | } |
| 1126 |
2/2✓ Branch 0 taken 22 times.
✓ Branch 1 taken 7007 times.
|
7029 | if (m_syncCardRate <= 0) { |
| 1127 |
2/4✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
|
22 | std::string hwParams = path.substr(0, path.rfind('/')) + "/hw_params"; |
| 1128 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | std::string text = GetFileContents(hwParams); |
| 1129 | 22 | std::size_t p = text.find("rate:"); | |
| 1130 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | m_syncCardRate = p == std::string::npos ? 0 : atoi(text.c_str() + p + 5); |
| 1131 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
|
22 | if (m_syncCardRate <= 0) { |
| 1132 | ✗ | return false; | |
| 1133 | } | ||
| 1134 |
2/4✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
|
22 | } |
| 1135 | // The card's position and the clock's internal time, as close together | ||
| 1136 | // as they can be read: the midpoint of the read. | ||
| 1137 |
1/2✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
|
7029 | GstClockTime before = gst_clock_get_internal_time(m_clock); |
| 1138 | 7029 | int64_t hw = 0; | |
| 1139 | 7029 | std::string trigger; | |
| 1140 |
1/2✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
|
7029 | bool running = ReadCardPosition(path, hw, trigger); |
| 1141 |
1/2✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
|
7029 | GstClockTime after = gst_clock_get_internal_time(m_clock); |
| 1142 | 7029 | GstClockTime internal = before + (after - before) / 2; | |
| 1143 | |||
| 1144 |
1/2✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
|
7029 | std::unique_lock<std::mutex> lock(m_lock); |
| 1145 |
3/6✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7029 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 7029 times.
|
7029 | if (path != m_cardStatusPath || m_state != State::Ready) { |
| 1146 | ✗ | return false; | |
| 1147 | } | ||
| 1148 | // The card stopping, or restarting, while a track feeds the stream is an | ||
| 1149 | // xrun: PipeWire restarted the device, and what the feed delivered | ||
| 1150 | // meanwhile is now queued behind it (see Flush()). | ||
| 1151 | // | ||
| 1152 | // A stop or restart right after one of our own flushes is the flush's | ||
| 1153 | // doing, not lost time. Taken for an xrun, it was flushed again, which | ||
| 1154 | // restarted the card again: on a card that restarts on a flush, one xrun | ||
| 1155 | // -- or the flush before a track -- had the stream flushed every 250 ms | ||
| 1156 | // for as long as it played. | ||
| 1157 | 7029 | bool lostTime = false; | |
| 1158 |
1/2✓ Branch 0 taken 7029 times.
✗ Branch 1 not taken.
|
7029 | bool ownRestart = NowMs() - m_lastFlushMs < kFlushRestartMs; |
| 1159 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 7025 times.
|
7029 | if (!running) { |
| 1160 |
3/6✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
|
4 | lostTime = m_syncAnchored && m_feed && !ownRestart; |
| 1161 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | ResetClockSyncLocked(); |
| 1162 | 4 | return lostTime; | |
| 1163 | } | ||
| 1164 | // A restart resets hw_ptr and changes trigger_time. hw_ptr also goes back | ||
| 1165 | // when it wraps at the ALSA boundary, with the card running on -- on a | ||
| 1166 | // 32-bit kernel every ~6 h -- and taking that for an xrun flushed a song | ||
| 1167 | // in mid-play. A wrap is told apart by an unchanged trigger_time and an | ||
| 1168 | // advance across the boundary no longer than the time since the last | ||
| 1169 | // sample allows; the position is then carried on past it. | ||
| 1170 |
4/4✓ Branch 0 taken 6999 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 19 times.
✓ Branch 3 taken 6980 times.
|
7025 | bool restarted = m_syncAnchored && trigger != m_syncTrigger; |
| 1171 |
6/6✓ Branch 0 taken 6999 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 6980 times.
✓ Branch 3 taken 19 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 6978 times.
|
7025 | if (m_syncAnchored && !restarted && hw < m_syncLastHw) { |
| 1172 |
2/4✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | int64_t boundary = trigger.empty() ? 0 : ReadCardBoundary(path); |
| 1173 | 2 | double sinceS = (double)(internal - m_syncLastInt) / GST_SECOND; | |
| 1174 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | double advanceS = boundary > m_syncLastHw ? (double)(hw + boundary - m_syncLastHw) / m_syncCardRate : -1.0; |
| 1175 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | if (advanceS >= 0.0 && advanceS <= sinceS + 1.0) { |
| 1176 | 2 | m_syncWrap += boundary; | |
| 1177 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | LogDebug(VB_MEDIAOUT, "PipeWire output stream %d: card position wrapped at %lld\n", |
| 1178 | m_slot, (long long)boundary); | ||
| 1179 | } else { | ||
| 1180 | ✗ | restarted = true; | |
| 1181 | } | ||
| 1182 | } | ||
| 1183 |
1/2✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
|
7025 | m_syncTrigger = trigger; |
| 1184 | 7025 | int64_t pos = hw + m_syncWrap; | |
| 1185 | |||
| 1186 | GstClockTime cInt, cExt, num, den; | ||
| 1187 |
1/2✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
|
7025 | gst_clock_get_calibration(m_clock, &cInt, &cExt, &num, &den); |
| 1188 |
1/2✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
|
7025 | GstClockTime external = gst_clock_adjust_with_calibration(m_clock, internal, cInt, cExt, num, den); |
| 1189 | |||
| 1190 |
2/2✓ Branch 0 taken 6999 times.
✓ Branch 1 taken 26 times.
|
7025 | double cardS = m_syncAnchored ? (double)(pos - m_syncHw0) / m_syncCardRate : 0.0; |
| 1191 |
2/2✓ Branch 0 taken 6999 times.
✓ Branch 1 taken 26 times.
|
7025 | double clockS = m_syncAnchored ? (double)(GstClockTimeDiff)(external - m_syncExt0) / GST_SECOND : 0.0; |
| 1192 | 7025 | double phase = cardS - clockS; // > 0: the card is ahead of the clock | |
| 1193 |
5/6✓ Branch 0 taken 6999 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 6980 times.
✓ Branch 3 taken 19 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 6980 times.
|
7025 | if (!m_syncAnchored || restarted || std::fabs(phase) > kClockMaxPhaseError) { |
| 1194 |
6/10✓ Branch 0 taken 19 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 19 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 17 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
45 | lostTime = m_syncAnchored && m_feed && (restarted ? !ownRestart : phase < -kClockMaxPhaseError); |
| 1195 |
2/2✓ Branch 0 taken 19 times.
✓ Branch 1 taken 26 times.
|
45 | if (m_syncAnchored) { |
| 1196 |
1/2✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
|
19 | LogDebug(VB_MEDIAOUT, "PipeWire output stream %d: card clock re-anchored (phase %.1f ms)\n", |
| 1197 | m_slot, phase * 1000.0); | ||
| 1198 | } | ||
| 1199 | // Start (again) from here, keeping the rate learnt so far. | ||
| 1200 | 45 | m_syncAnchored = true; | |
| 1201 | 45 | m_syncHw0 = pos; | |
| 1202 | 45 | m_syncInt0 = internal; | |
| 1203 | 45 | m_syncExt0 = external; | |
| 1204 | 45 | m_syncWindow.clear(); | |
| 1205 | 45 | cardS = clockS = phase = 0.0; | |
| 1206 | } | ||
| 1207 | 7025 | m_syncLastHw = hw; | |
| 1208 | 7025 | m_syncLastInt = internal; | |
| 1209 | |||
| 1210 | 7025 | double intS = (double)(internal - m_syncInt0) / GST_SECOND; | |
| 1211 |
1/2✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
|
7025 | m_syncWindow.emplace_back(intS, cardS); |
| 1212 |
6/6✓ Branch 0 taken 8482 times.
✓ Branch 1 taken 79 times.
✓ Branch 2 taken 1536 times.
✓ Branch 3 taken 6946 times.
✓ Branch 4 taken 1536 times.
✓ Branch 5 taken 7025 times.
|
8561 | while (m_syncWindow.size() > 2 && intS - m_syncWindow.front().first > kClockWindowS) { |
| 1213 | 1536 | m_syncWindow.pop_front(); | |
| 1214 | } | ||
| 1215 | 7025 | double rate = m_clockRate.load(std::memory_order_relaxed); | |
| 1216 |
2/2✓ Branch 0 taken 6478 times.
✓ Branch 1 taken 547 times.
|
7025 | if (intS - m_syncWindow.front().first >= kClockMinFitS) { |
| 1217 | 6478 | double mx = 0, my = 0; | |
| 1218 |
2/2✓ Branch 0 taken 1724233 times.
✓ Branch 1 taken 6478 times.
|
1730711 | for (auto& [x, y] : m_syncWindow) { |
| 1219 | 1724233 | mx += x; | |
| 1220 | 1724233 | my += y; | |
| 1221 | } | ||
| 1222 | 6478 | mx /= m_syncWindow.size(); | |
| 1223 | 6478 | my /= m_syncWindow.size(); | |
| 1224 | 6478 | double sxy = 0, sxx = 0; | |
| 1225 |
2/2✓ Branch 0 taken 1724233 times.
✓ Branch 1 taken 6478 times.
|
1730711 | for (auto& [x, y] : m_syncWindow) { |
| 1226 | 1724233 | sxy += (x - mx) * (y - my); | |
| 1227 | 1724233 | sxx += (x - mx) * (x - mx); | |
| 1228 | } | ||
| 1229 |
1/2✓ Branch 0 taken 6478 times.
✗ Branch 1 not taken.
|
6478 | if (sxx > 0) { |
| 1230 |
1/2✓ Branch 0 taken 6478 times.
✗ Branch 1 not taken.
|
6478 | rate = std::clamp(sxy / sxx, 1.0 - kClockMaxOffset, 1.0 + kClockMaxOffset); |
| 1231 | 6478 | m_clockRate = rate; | |
| 1232 | } | ||
| 1233 | } | ||
| 1234 |
1/2✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
|
7025 | double applied = rate + std::clamp(phase * kClockPhaseGain, -kClockMaxPhaseSlew, kClockMaxPhaseSlew); |
| 1235 |
1/2✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
|
7025 | applied = std::clamp(applied, 1.0 - kClockMaxOffset, 1.0 + kClockMaxOffset); |
| 1236 | // Continuous at `internal`: only the rate from here on changes. | ||
| 1237 |
1/2✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
|
7025 | gst_clock_set_calibration(m_clock, internal, external, (GstClockTime)llround(applied * 1e9), 1000000000ULL); |
| 1238 |
1/2✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
|
7025 | RecalibrateFeedClockLocked(internal); |
| 1239 |
1/2✓ Branch 0 taken 7025 times.
✗ Branch 1 not taken.
|
7025 | LogExcess(VB_MEDIAOUT, "PipeWire output stream %d: card %+.1f ppm, phase %+.2f ms, clock %+.1f ppm\n", |
| 1240 | m_slot, (rate - 1.0) * 1e6, phase * 1000.0, (applied - 1.0) * 1e6); | ||
| 1241 | 7025 | return lostTime; | |
| 1242 | 28502 | } | |
| 1243 | |||
| 1244 | 142 | bool PipeWireOutputStream::CardRunning() { | |
| 1245 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 142 times.
|
142 | if (m_cardStatusPath.empty()) { |
| 1246 | ✗ | return false; | |
| 1247 | } | ||
| 1248 |
1/2✓ Branch 0 taken 142 times.
✗ Branch 1 not taken.
|
142 | return GetFileContents(m_cardStatusPath).find("state: RUNNING") != std::string::npos; |
| 1249 | } | ||
| 1250 | |||
| 1251 | 181 | void PipeWireOutputStream::CloseLocked(const char* why) { | |
| 1252 |
2/2✓ Branch 0 taken 153 times.
✓ Branch 1 taken 28 times.
|
181 | if (m_pipeline) { |
| 1253 | 153 | LogInfo(VB_MEDIAOUT, "PipeWire output stream %d closing (%s)\n", m_slot, why); | |
| 1254 | 153 | GstElement* pipeline = m_pipeline; | |
| 1255 | 153 | GstElement* src = m_src; | |
| 1256 | // Off this thread: a pipewiresink state change can block on the | ||
| 1257 | // daemon, and the worker services every slot. | ||
| 1258 | 306 | std::thread([pipeline, src]() { | |
| 1259 |
2/4✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 153 times.
✗ Branch 3 not taken.
|
153 | SetThreadName("FPP-PWStreamEnd"); |
| 1260 | 153 | gst_element_set_state(pipeline, GST_STATE_NULL); | |
| 1261 |
1/2✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
|
153 | if (src) { |
| 1262 | 153 | gst_object_unref(src); | |
| 1263 | } | ||
| 1264 | 153 | gst_object_unref(pipeline); | |
| 1265 |
2/4✓ Branch 0 taken 153 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 153 times.
✗ Branch 3 not taken.
|
306 | }).detach(); |
| 1266 | } | ||
| 1267 | 181 | gst_caps_replace(&m_caps, nullptr); | |
| 1268 | 181 | m_rate = 0; | |
| 1269 | 181 | m_queuedUntilUs = 0; | |
| 1270 | 181 | m_pipeline = nullptr; | |
| 1271 | 181 | m_src = nullptr; | |
| 1272 | // m_feed stays: it belongs to the track using the slot (AttachFeed() / | ||
| 1273 | // Release()), not to this stream. Clearing it here orphaned a track | ||
| 1274 | // whose start triggered a reopen -- a new target -- and the whole track | ||
| 1275 | // played silent. Its samples are dropped while there is no m_src. | ||
| 1276 | 181 | m_target.clear(); | |
| 1277 | 181 | m_linked = false; | |
| 1278 | 181 | m_state = State::Closed; | |
| 1279 | 181 | m_stateChanged.notify_all(); | |
| 1280 | 181 | } | |
| 1281 | |||
| 1282 | #endif | ||
| 1283 |