| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | /* | ||
| 3 | * This file is part of the Falcon Player (FPP) and is Copyright (C) | ||
| 4 | * 2013-2025 by the Falcon Player Developers. | ||
| 5 | * | ||
| 6 | * The Falcon Player (FPP) is free software, and is covered under | ||
| 7 | * multiple Open Source licenses. Please see the included 'LICENSES' | ||
| 8 | * file for descriptions of what files are covered by each license. | ||
| 9 | * | ||
| 10 | * This source file is covered under the LGPL v2.1 as described in the | ||
| 11 | * included LICENSE.LGPL file. | ||
| 12 | */ | ||
| 13 | |||
| 14 | #include "MediaOutputBase.h" | ||
| 15 | |||
| 16 | #if __has_include(<gst/gst.h>) | ||
| 17 | #define HAS_GSTREAMER | ||
| 18 | |||
| 19 | #include <gst/gst.h> | ||
| 20 | #include <gst/app/gstappsink.h> | ||
| 21 | #include <array> | ||
| 22 | #include <atomic> | ||
| 23 | #include <condition_variable> | ||
| 24 | #include <list> | ||
| 25 | #include <map> | ||
| 26 | #include <memory> | ||
| 27 | #include <mutex> | ||
| 28 | #include <set> | ||
| 29 | #include <vector> | ||
| 30 | |||
| 31 | #include "AES67Manager.h" | ||
| 32 | #include "AtomicSharedPtr.h" | ||
| 33 | #include "DrmCards.h" | ||
| 34 | #include "FeedSyncWindow.h" | ||
| 35 | #include "OpusRTPManager.h" | ||
| 36 | |||
| 37 | class PixelOverlayModel; | ||
| 38 | class PipeWireOutputStream; | ||
| 39 | |||
| 40 | class GStreamerOutput : public MediaOutputBase { | ||
| 41 | public: | ||
| 42 | GStreamerOutput(const std::string& mediaFilename, MediaOutputStatus* status, const std::string& videoOut = "", int streamSlot = 1); | ||
| 43 | virtual ~GStreamerOutput(); | ||
| 44 | |||
| 45 | virtual int Start(int msTime = 0) override; | ||
| 46 | virtual int Stop(void) override; | ||
| 47 | virtual int Process(void) override; | ||
| 48 | virtual int Close(void) override; | ||
| 49 | virtual int IsPlaying(void) override; | ||
| 50 | virtual int AdjustSpeed(float masterPos) override; | ||
| 51 | // True once the reported position -- already backed up to what is leaving | ||
| 52 | // the card -- has moved past the start of the media. | ||
| 53 | 686 | virtual bool IsAudible(void) override { return m_audible; } | |
| 54 | |||
| 55 | /// Flush-seek to an absolute position in seconds. Audible/visible glitch, | ||
| 56 | /// so this is for repairing a stream that has genuinely left the show | ||
| 57 | /// timeline, not for routine correction. Returns false if the seek failed. | ||
| 58 | bool SeekTo(float seconds); | ||
| 59 | virtual void SetVolume(int volume) override; | ||
| 60 | /// SetVolume() that says whether the output took the volume: false when | ||
| 61 | /// it has no volume element to set -- before Start(), once its teardown | ||
| 62 | /// (Stop() or Close()) has begun, and for media with no sound. | ||
| 63 | bool TrySetVolume(int volume); | ||
| 64 | |||
| 65 | /// Ask the output's owner to stop it, from a thread that doesn't own it | ||
| 66 | /// (a command, or the slot manager stopping a playlist's background | ||
| 67 | /// slots). Only the owner may Stop()/Close()/delete an output: the base | ||
| 68 | /// version flags it for the next Process() on the owner's thread; Play | ||
| 69 | /// Media's outputs override it to stop through their own registry. | ||
| 70 | 1 | virtual void RequestStop() { m_stopRequested = true; } | |
| 71 | |||
| 72 | /// Whether StreamSlotManager must call Process() for this output. False | ||
| 73 | /// for outputs whose owner already does (a playlist entry, the remote's | ||
| 74 | /// synced media); Play Media's outputs have no other pump. | ||
| 75 | 48676 | virtual bool NeedsSlotPump() const { return false; } | |
| 76 | |||
| 77 | // Static methods matching SDLOutput interface | ||
| 78 | static bool IsOverlayingVideo(); | ||
| 79 | static bool ProcessVideoOverlay(unsigned int msTimestamp); | ||
| 80 | static bool GetAudioSamples(float* samples, int numSamples, int& sampleRate); | ||
| 81 | |||
| 82 | // One-time GStreamer + PipeWire env initialization (safe to call repeatedly) | ||
| 83 | static void EnsureGStreamerInit(); | ||
| 84 | // Take `pipeline` to NULL on a reaper thread, waiting at most timeoutMs. | ||
| 85 | // False if it wedged: the reaper keeps its own ref and the pipeline (with | ||
| 86 | // whatever it holds) is left to it. The caller's ref is untouched. | ||
| 87 | static bool SetPipelineToNullBounded(GstElement* pipeline, int timeoutMs); | ||
| 88 | // Set the environment now, on the calling thread, and run gst_init() in the | ||
| 89 | // background so the first play doesn't pay for it (~1 s on an AM335x, and | ||
| 90 | // ~20 s the very first time, when GStreamer builds its plugin registry). | ||
| 91 | static void PreloadAsync(); | ||
| 92 | // Make every videoconvert in `pipeline` -- including ones added later -- | ||
| 93 | // read decoded frames from padded system memory rather than straight out | ||
| 94 | // of a hardware decoder's buffers. See the comment at the definition. | ||
| 95 | // Also has each decodebin note a hardware video decoder it chooses, for | ||
| 96 | // the wedge ladder. | ||
| 97 | static void GuardVideoConvertInputs(GstElement* pipeline); | ||
| 98 | |||
| 99 | // The PipeWire sink a slot's audio is routed to ("" = PipeWire's default). | ||
| 100 | static std::string PipeWireSinkNameForSlot(int slot); | ||
| 101 | // The rate the PipeWire graph runs at (48000 if it can't be determined). | ||
| 102 | static int PipeWireGraphRate(); | ||
| 103 | // /proc/asound status file of a card the PipeWire sink `sinkName` (a group | ||
| 104 | // or an fpp_alsa_* adapter) plays into, or "" if it reaches no card -- a | ||
| 105 | // group of network senders only, or PipeWire's unnamed default sink. | ||
| 106 | static std::string AlsaSinkStatusPath(const std::string& sinkName); | ||
| 107 | |||
| 108 | // Resolve connector name (e.g., "HDMI-A-1") to DRM card path, connector ID, | ||
| 109 | // and display resolution by scanning sysfs. Works on all Pi models. | ||
| 110 | struct DrmConnectorInfo { | ||
| 111 | std::string cardPath; // e.g. "/dev/dri/card1" | ||
| 112 | int connectorId = -1; // integer ID for kmssink | ||
| 113 | bool connected = false; | ||
| 114 | int displayWidth = 0; | ||
| 115 | int displayHeight = 0; | ||
| 116 | int primaryPlaneId = -1; // DRM primary plane for this connector's CRTC | ||
| 117 | }; | ||
| 118 | static DrmConnectorInfo ResolveDrmConnector(const std::string& connectorName); | ||
| 119 | |||
| 120 | |||
| 121 | // GStreamer-specific | ||
| 122 | 1837 | void SetLoopCount(int loops) { m_loopCount = loops; } | |
| 123 | // Whether this output's position paces the running sequence (the channel | ||
| 124 | // output servo). True for the show's own media -- a playlist entry's, or | ||
| 125 | // a remote's synced media -- and false for media played alongside it. | ||
| 126 | 1837 | void SetDrivesShowClock(bool drives) { m_drivesShowClock = drives; } | |
| 127 | void SetVolumeAdjustment(int volAdj); | ||
| 128 | |||
| 129 | // Event callbacks (matching VLCOutput pattern). | ||
| 130 | // | ||
| 131 | // Playing(), Stopping() and Stopped() are called from Stop() and also from | ||
| 132 | // BusSyncHandler, on the GStreamer streaming thread that delivered the | ||
| 133 | // message, while that thread still holds the sink's internal locks. An override must only | ||
| 134 | // record state or hand work to the main loop (Timers): anything that | ||
| 135 | // seeks, changes pipeline state, stops or closes an output -- including | ||
| 136 | // running a command preset, which can do all of those -- deadlocks there. | ||
| 137 | 1966 | virtual void Starting() {} | |
| 138 | 413 | virtual void Playing() {} | |
| 139 | 2084 | virtual void Stopping() {} | |
| 140 | 1961 | virtual void Stopped() {} | |
| 141 | |||
| 142 | private: | ||
| 143 | // Serializes Start(), Stop() and Close() against each other and against | ||
| 144 | // Process(), SeekTo() and AdjustSpeed(), which a slot's pump and MultiSync | ||
| 145 | // run on the main loop while a command may be stopping the same output on | ||
| 146 | // an HTTP thread. The pump side only ever try-locks and skips a tick when | ||
| 147 | // the output is busy, so it never waits here; holders may take | ||
| 148 | // StreamSlotManager's lock, never the other way round. | ||
| 149 | std::recursive_mutex m_opMutex; | ||
| 150 | std::atomic<bool> m_stopRequested{false}; | ||
| 151 | |||
| 152 | // An optional video branch -- a secondary display's kmssink, or the | ||
| 153 | // PipeWire video tap -- whose sink failed. Its GST_FLOW_ERROR would | ||
| 154 | // otherwise reach the tee and fail the whole pipeline, so the bus sync | ||
| 155 | // handler cuts it off at the tee (CutFailedBranch, on the failing sink's | ||
| 156 | // streaming thread) and Process() takes it out of the pipeline | ||
| 157 | // (RemoveFailedBranches, op lock held). Holds a ref on every object. | ||
| 158 | struct FailedBranch { | ||
| 159 | GstElement* tee = nullptr; | ||
| 160 | GstPad* teePad = nullptr; | ||
| 161 | std::vector<GstElement*> elements; // sink first, back to the queue | ||
| 162 | }; | ||
| 163 | std::mutex m_failedBranchLock; | ||
| 164 | std::vector<FailedBranch> m_failedBranches; | ||
| 165 | std::atomic<bool> m_haveFailedBranches{false}; | ||
| 166 | bool CutFailedBranch(GstElement* sink); | ||
| 167 | // The head of an optional branch, between the tee and the branch's | ||
| 168 | // queue: turns a sink's GST_FLOW_ERROR into OK, so even a buffer already | ||
| 169 | // waiting on the branch's full queue can't carry it back to the tee. | ||
| 170 | // nullptr where errorignore isn't installed. | ||
| 171 | static GstElement* MakeBranchErrorGuard(); | ||
| 172 | void RemoveFailedBranches(); | ||
| 173 | void ReleaseFailedBranches(); | ||
| 174 | bool m_drivesShowClock = true; | ||
| 175 | // Cuts the decodebin and bus callbacks off from this object and waits for | ||
| 176 | // any that are running. See CallbackGuard. | ||
| 177 | void DetachCallbacks(); | ||
| 178 | // This output's id for its PixelOverlayManager model listener. | ||
| 179 | std::string OverlayListenerId() const; | ||
| 180 | |||
| 181 | GstElement* m_pipeline = nullptr; | ||
| 182 | GstElement* m_volume = nullptr; | ||
| 183 | GstElement* m_appsink = nullptr; // audio sample tap | ||
| 184 | GstElement* m_videoAppsink = nullptr; // video frame sink for PixelOverlay | ||
| 185 | GstBus* m_bus = nullptr; | ||
| 186 | std::string m_videoOut; | ||
| 187 | int m_loopCount = 0; | ||
| 188 | int m_volumeAdjust = 0; | ||
| 189 | // Per-file loudness gain from the File Manager's Normalize (linear, 1.0 | ||
| 190 | // = as recorded), looked up in media_gain.json when the song starts and | ||
| 191 | // multiplied into every value set on m_volume. See LookupNormalizeGain(). | ||
| 192 | double m_fileGain = 1.0; | ||
| 193 | std::atomic<bool> m_playing{false}; // written from GStreamer thread (BusSyncHandler), read from main loop | ||
| 194 | std::atomic<bool> m_shutdownFlag{false}; // guards callbacks during teardown | ||
| 195 | bool m_teardownComplete = false; // makes Stop() idempotent — the expensive pipeline teardown (silence flush + DRM release sleeps) must run only once even when Stop() is called both directly and via Close()/dtor | ||
| 196 | // Set when the supervised pipeline-NULL transition timed out and the | ||
| 197 | // pipeline was abandoned to a reaper thread (wedged V4L2 decoder, issue | ||
| 198 | // #2695). Close() must then leak the pipeline's DRM planes and shared | ||
| 199 | // DRM fd refs — the still-alive kmssinks own them in the kernel. | ||
| 200 | bool m_teardownAbandoned = false; | ||
| 201 | // Cancellation token for the detached Start() PLAYING-transition thread. | ||
| 202 | // Shared (shared_ptr) so the thread can safely outlive this object: Stop() | ||
| 203 | // sets it to abort the volume ramp / deferred attach early, and the thread | ||
| 204 | // captures only this token (never `this`), eliminating use-after-free if it | ||
| 205 | // is still running when the object is torn down. | ||
| 206 | std::shared_ptr<std::atomic<bool>> m_startThreadCancel; | ||
| 207 | // Set while the Start() thread has yet to seek to a non-zero start | ||
| 208 | // position. The pipeline prerolls at 0 first, and Process() must not | ||
| 209 | // report that 0 as the media position (see Start()). Shared for the same | ||
| 210 | // reason as m_startThreadCancel. | ||
| 211 | std::shared_ptr<std::atomic<bool>> m_startSeekPending; | ||
| 212 | gulong m_appsinkSignalId = 0; // audio appsink signal handler ID | ||
| 213 | gulong m_videoAppsinkSignalId = 0; // video appsink signal handler ID | ||
| 214 | |||
| 215 | // Stall watchdog — detects when PipeWire sink stops consuming data. | ||
| 216 | // The *Ms stamps here are GetMonotonicTimeMS(): on the wall clock a step | ||
| 217 | // either way made the watchdogs stop playing media as stalled. | ||
| 218 | gint64 m_lastPosition = -1; | ||
| 219 | gint64 m_maxDuration = 0; // highest observed duration (handles VBR fluctuations) | ||
| 220 | uint64_t m_durationQueriedMs = 0; | ||
| 221 | uint64_t m_stallStartMs = 0; | ||
| 222 | uint64_t m_wallStartMs = 0; | ||
| 223 | // A SeekTo() that arrived before the pipeline prerolled, applied by | ||
| 224 | // Process() at the first position. -1 when none is waiting. | ||
| 225 | gint64 m_pendingSeekNs = -1; | ||
| 226 | uint64_t m_lastWallLogMs = 0; | ||
| 227 | |||
| 228 | // Audio still queued between pipewiresink's reported position and the DAC. | ||
| 229 | // Cached: the depth barely moves while a track plays, and Process() runs at | ||
| 230 | // frame rate. | ||
| 231 | int64_t AudioSinkLatencyNs(); | ||
| 232 | // One-shot note that the position is going out uncorrected, and why. | ||
| 233 | void NoteNoSinkLatency(const std::string& why); | ||
| 234 | int64_t m_sinkLatencyNs = 0; | ||
| 235 | uint64_t m_sinkLatencyCheckedMs = 0; | ||
| 236 | std::string m_alsaStatusPath; | ||
| 237 | std::string m_alsaHwParamsPath; | ||
| 238 | int m_alsaRate = 0; | ||
| 239 | bool m_loggedSinkLatency = false; | ||
| 240 | bool m_warnedNoSinkLatency = false; | ||
| 241 | uint64_t m_playStartMs = 0; // when Start() set m_playing=true (GetMonotonicTimeMS()) — anchors the preroll watchdog | ||
| 242 | static constexpr int STALL_TIMEOUT_MS = 5000; // 5 seconds before declaring stall | ||
| 243 | // Preroll watchdog: if the pipeline is set to PLAYING but never produces a | ||
| 244 | // valid position (never actually starts), it is wedged in preroll/PAUSED — | ||
| 245 | // e.g. the Pi's V4L2 hardware decoder can stop producing frames after a very | ||
| 246 | // long video-only loop (issue #2695), so the video sink never prerolls and | ||
| 247 | // the pipeline stays PAUSED forever. The regular stall watchdog can't catch | ||
| 248 | // this because it only runs once a position is available. Generous timeout: | ||
| 249 | // normal preroll completes in well under 1s even on slow HW decoders. | ||
| 250 | static constexpr int PREROLL_TIMEOUT_MS = 15000; | ||
| 251 | |||
| 252 | void ProcessMessages(); | ||
| 253 | static GstBusSyncReply BusSyncHandler(GstBus* bus, GstMessage* msg, gpointer userData); | ||
| 254 | |||
| 255 | // Audio sample tap for WLED audio-reactive effects | ||
| 256 | static constexpr int SAMPLE_BUFFER_SIZE = 4096; // circular buffer size | ||
| 257 | static std::array<float, SAMPLE_BUFFER_SIZE> s_sampleBuffer; | ||
| 258 | static int s_sampleWritePos; | ||
| 259 | static int s_sampleRate; | ||
| 260 | // Guards the ring above and m_currentInstance, the one output allowed to | ||
| 261 | // write it. | ||
| 262 | static std::mutex s_sampleMutex; | ||
| 263 | // When the tap owner last wrote the ring. A paused or stalled owner | ||
| 264 | // stops writing, and its last half second of audio must not read as | ||
| 265 | // live. | ||
| 266 | static uint64_t s_sampleLastWriteMs; | ||
| 267 | static constexpr int SAMPLE_STALE_MS = 500; | ||
| 268 | // Make `o` the tap owner and empty the ring. s_sampleMutex held. | ||
| 269 | static void SetSampleTapOwnerLocked(GStreamerOutput* o); | ||
| 270 | static GstFlowReturn OnNewSample(GstAppSink* appsink, gpointer userData); | ||
| 271 | // The tap's branch only carries audio while something is reading it. | ||
| 272 | static constexpr int SAMPLE_TAP_IDLE_MS = 1000; | ||
| 273 | static std::atomic<uint64_t> s_sampleTapReadMs; | ||
| 274 | static GstPadProbeReturn GateSampleTap(GstPad* pad, GstPadProbeInfo* info, gpointer userData); | ||
| 275 | |||
| 276 | // Video overlay for PixelOverlayModel (Phase 3) | ||
| 277 | PixelOverlayModel* m_videoOverlayModel = nullptr; | ||
| 278 | std::mutex m_videoOverlayModelLock; | ||
| 279 | int m_videoOverlayWidth = 0; | ||
| 280 | int m_videoOverlayHeight = 0; | ||
| 281 | // Read by the channel output thread through the overlay registry | ||
| 282 | // (IsOverlayingVideo(), ProcessVideoOverlay()) while Close() and a removed | ||
| 283 | // video branch clear it, before the output leaves the registry. | ||
| 284 | std::atomic<bool> m_hasVideoStream{false}; | ||
| 285 | bool m_wasOverlayDisabled = false; | ||
| 286 | |||
| 287 | // Latest video frame buffer — written by OnNewVideoSample, read by ProcessVideoOverlay | ||
| 288 | std::vector<uint8_t> m_videoFrameData; | ||
| 289 | int m_videoFrameWidth = 0; // dimensions of m_videoFrameData (packed RGB) | ||
| 290 | int m_videoFrameHeight = 0; | ||
| 291 | std::mutex m_videoFrameMutex; | ||
| 292 | bool m_videoFrameReady = false; | ||
| 293 | uint64_t m_videoFramesReceived = 0; // diagnostic counter | ||
| 294 | uint64_t m_videoFramesDelivered = 0; // diagnostic counter | ||
| 295 | |||
| 296 | static GstFlowReturn OnNewVideoSample(GstAppSink* appsink, gpointer userData); | ||
| 297 | |||
| 298 | // HDMI/DRM video output via kmssink (Phase 4) | ||
| 299 | bool m_wantHDMI = false; // true when outputting to HDMI via kmssink | ||
| 300 | bool m_macVideoWindow = false; // macOS: the "HDMI" output is this slot's video window | ||
| 301 | GstElement* m_kmssink = nullptr; // kmssink element (owned by pipeline bin) | ||
| 302 | int m_hdmiConnectorId = -1; // DRM connector ID from sysfs | ||
| 303 | std::string m_hdmiCardPath; // e.g. "/dev/dri/card1" | ||
| 304 | // The DRM lease of each of this pipeline's kmssinks (the primary one and | ||
| 305 | // any dkms_* branches). Released in Close(), strictly after the pipeline | ||
| 306 | // (and therefore every kmssink) has been unreffed. | ||
| 307 | std::vector<DrmCards::Lease> m_drmLeases; | ||
| 308 | // HDMI connectors this pipeline claimed from the video output manager's | ||
| 309 | // persistent consumers (ClaimConnectorForMedia); released in Close() | ||
| 310 | // after the leases, which brings those consumers back. | ||
| 311 | std::vector<int> m_mediaConnectors; | ||
| 312 | int m_hdmiDisplayWidth = 0; // display resolution | ||
| 313 | int m_hdmiDisplayHeight = 0; | ||
| 314 | |||
| 315 | // PipeWire video routing — when PipeWireVideoSinkName is set, video is | ||
| 316 | // tee'd to both kmssink (HDMI) and pipewiresink (PipeWire graph node). | ||
| 317 | std::string m_pwVideoSinkName; // target-object for video pipewiresink | ||
| 318 | bool m_videoPipeWireRouting = false; // true when video goes through PipeWire | ||
| 319 | std::set<int> m_directConnectorIds; // HDMI connectors driven by direct kmssink on vtee | ||
| 320 | |||
| 321 | // Dynamic pad linking for decodebin (video requires pad-added signal) | ||
| 322 | // | ||
| 323 | // decodebin emits these from its own streaming thread, on a pipeline that | ||
| 324 | // can outlive this GStreamerOutput: Start() hands an owning pipeline ref to | ||
| 325 | // a detached state-change thread, so Close()/~GStreamerOutput can complete | ||
| 326 | // while decodebin is still exposing pads. The callbacks therefore must not | ||
| 327 | // dereference `this` directly. Each Start() allocates a fresh guard whose | ||
| 328 | // `self` Close() clears while holding `mtx`; a callback holding a shared_ptr | ||
| 329 | // to it either runs to completion before that clear or sees nullptr and | ||
| 330 | // returns. Old pipelines keep their own (already cleared) guard, so a | ||
| 331 | // restarted output is never reached through a stale connection. | ||
| 332 | // | ||
| 333 | // The bus sync handler runs on streaming threads too, and gets the same | ||
| 334 | // guard -- but it does not hold `mtx` while it works. It registers itself | ||
| 335 | // in `busInFlight` instead, and Close() waits for that to drain after | ||
| 336 | // clearing `self`. Holding the lock across a handler would let one | ||
| 337 | // streaming thread block another inside GStreamer. `mtx` is recursive | ||
| 338 | // because OnNoMorePads changes element state while holding it, and the | ||
| 339 | // resulting bus messages reach BusSyncHandler on that same thread. | ||
| 340 | struct CallbackGuard { | ||
| 341 | std::recursive_mutex mtx; | ||
| 342 | GStreamerOutput* self = nullptr; | ||
| 343 | int busInFlight = 0; | ||
| 344 | std::condition_variable_any busIdle; | ||
| 345 | }; | ||
| 346 | // Written only under m_opMutex (Start(), DetachCallbacks()), but read | ||
| 347 | // without it by RefVolumeElement() and from streaming threads. | ||
| 348 | AtomicSharedPtr<CallbackGuard> m_cbGuard; | ||
| 349 | GstElement* RefVolumeElement(); | ||
| 350 | // Connects pad-added (and optionally no-more-pads) on `decoder`, passing a | ||
| 351 | // guard reference as the closure data. Takes no ref on `decoder`. | ||
| 352 | void ConnectPadSignals(GstElement* decoder, bool wantNoMorePads); | ||
| 353 | static void ReleaseCallbackGuard(gpointer data, GClosure* closure); | ||
| 354 | // GDestroyNotify for the guard reference handed to gst_bus_set_sync_handler. | ||
| 355 | static void ReleaseBusCallbackGuard(gpointer data); | ||
| 356 | // Restarts the output from the top for its next loop. Called from | ||
| 357 | // BusSyncHandler on EOS, it does the seek on the main loop: a flushing | ||
| 358 | // seek issued from the thread that delivered the EOS deadlocks, because | ||
| 359 | // that thread still holds the sink's preroll lock and the flush needs it. | ||
| 360 | static void ScheduleLoopSeek(const std::shared_ptr<CallbackGuard>& guard); | ||
| 361 | // Resolves the closure data back to a live output. Returns nullptr (with | ||
| 362 | // `lock` left unlocked) once the owning Close() has run. | ||
| 363 | static GStreamerOutput* LockCallbackGuard(gpointer userData, | ||
| 364 | std::shared_ptr<CallbackGuard>& guard, | ||
| 365 | std::unique_lock<std::recursive_mutex>& lock); | ||
| 366 | // Keeps the output alive for one BusSyncHandler call. `self` is nullptr | ||
| 367 | // once Close() has run; otherwise Close() blocks until this is destroyed. | ||
| 368 | struct BusCallbackScope { | ||
| 369 | explicit BusCallbackScope(gpointer userData); | ||
| 370 | ~BusCallbackScope(); | ||
| 371 | std::shared_ptr<CallbackGuard> guard; | ||
| 372 | GStreamerOutput* self = nullptr; | ||
| 373 | }; | ||
| 374 | static void OnPadAdded(GstElement* element, GstPad* pad, gpointer userData); | ||
| 375 | static void OnNoMorePads(GstElement* element, gpointer userData); | ||
| 376 | GstElement* m_audioChain = nullptr; // audio sub-bin for pad linking | ||
| 377 | GstElement* m_videoChain = nullptr; // video sub-bin for pad linking | ||
| 378 | bool m_audioLinked = false; // true when audio pad was connected | ||
| 379 | bool m_videoLinked = false; // true when video pad was connected | ||
| 380 | |||
| 381 | // True when Start() built the audio-only fallback (no video output for this | ||
| 382 | // item), which is the one pipeline shape that cannot survive a file with no | ||
| 383 | // playable audio -- see the no-audio handling in BusSyncHandler(). | ||
| 384 | bool m_audioOnlyPipeline = false; | ||
| 385 | // Whether decodebin ever exposed a decoded audio pad. Written on the | ||
| 386 | // streaming thread from OnPadAdded, read on the bus thread. | ||
| 387 | std::atomic<bool> m_sawAudioPad{false}; | ||
| 388 | // Set once the "file has no playable audio" case has been reported, so the | ||
| 389 | // demuxer's follow-on "Internal data stream error" is not logged as a second, | ||
| 390 | // unrelated-looking failure. | ||
| 391 | std::atomic<bool> m_reportedNoAudio{false}; | ||
| 392 | |||
| 393 | // A banner is up for this run's failure. The first one wins: the errors | ||
| 394 | // after it (a demuxer's follow-on) describe the same event. | ||
| 395 | std::atomic<bool> m_reportedFailure{false}; | ||
| 396 | void ReportFileFailure(const char* what, const std::string& reason); | ||
| 397 | void ReportVideoOutputFailure(const char* sinkName, const std::string& reason); | ||
| 398 | // dkms_<id> -> its connector's name, for that banner. Built before the pipeline runs. | ||
| 399 | std::map<int, std::string> m_dkmsConnectorNames; | ||
| 400 | |||
| 401 | #ifdef HAS_AES67_GSTREAMER | ||
| 402 | // Zero-hop AES67 RTP branches attached to the audio tee (Phase 7.9) | ||
| 403 | std::vector<AES67Manager::InlineRTPBranch> m_aes67Branches; | ||
| 404 | void AttachAES67Branches(GstElement* tee); | ||
| 405 | void DetachAES67Branches(); | ||
| 406 | #endif | ||
| 407 | |||
| 408 | // Flush PipeWire filter-chain delay buffers between songs | ||
| 409 | static void FlushPipeWireDelayBuffers(); | ||
| 410 | static int FindPipeWireNodeId(const std::string& key, const std::string& value); | ||
| 411 | static void SetPipeWireStreamRate(const std::string& streamId, float rate); | ||
| 412 | |||
| 413 | // ── Phase 5: MultiSync rate adjustment (ported from VLCOut) ── | ||
| 414 | bool m_allowSpeedAdjust = true; | ||
| 415 | float m_currentRate = 1.0f; | ||
| 416 | |||
| 417 | // Circular buffer of last MAX_DIFFS diff/rate pairs for trend detection | ||
| 418 | static constexpr int MAX_DIFFS = 10; | ||
| 419 | std::array<std::pair<int, float>, MAX_DIFFS> m_diffs{}; | ||
| 420 | int m_diffsSize = 0; | ||
| 421 | int m_diffIdx = 0; | ||
| 422 | int m_diffSum = 0; | ||
| 423 | float m_rateSum = 0.0f; | ||
| 424 | |||
| 425 | void pushDiff(int diff, float rate); | ||
| 426 | |||
| 427 | int m_lastDiff = -1; // init at -1 so speedup/slowdown logic initially assumes slightly behind master | ||
| 428 | int m_rateDiff = 0; | ||
| 429 | |||
| 430 | // Running rate average (smooths jitter) | ||
| 431 | static constexpr int RATE_AVERAGE_COUNT = 20; | ||
| 432 | std::list<float> m_lastRates; | ||
| 433 | float m_lastRatesSum = 0.0f; | ||
| 434 | |||
| 435 | // Apply playback rate change via GStreamer instant-rate-change seek | ||
| 436 | void ApplyRate(float rate); | ||
| 437 | // fpp.stream.id of this pipeline's own pipewiresink, if it has one | ||
| 438 | std::string m_pwSinkStreamId; | ||
| 439 | |||
| 440 | // A video whose sound feeds the output stream: its picture is held back by | ||
| 441 | // the card's queue (AlignPictureToSound()), and a video that turns out to | ||
| 442 | // have no sound hands the stream back from the main loop. | ||
| 443 | void AlignPictureToSound(int64_t latNs); | ||
| 444 | int64_t m_pictureDelayNs = 0; | ||
| 445 | std::atomic<bool> m_pwFeedOrphaned{ false }; | ||
| 446 | |||
| 447 | // A MultiSync remote's media through the persistent output stream: held to | ||
| 448 | // the master by the speed of the stream's feed (see AdjustFeedSpeed()). | ||
| 449 | int AdjustFeedSpeed(float masterMediaPosition); | ||
| 450 | double m_feedSpeedErrInt = 0.0; // integral of the error, s*s | ||
| 451 | uint64_t m_feedSpeedLastMs = 0; // when the servo last ran | ||
| 452 | uint64_t m_feedResyncMs = 0; // when the last resync seek was made | ||
| 453 | bool m_feedResyncLanding = false; // the next error is how far it landed off | ||
| 454 | FeedSyncWindow m_feedErrWindow; // the last two seconds of error readings | ||
| 455 | // How far ahead of the master a resync seek aims: what a flushing seek | ||
| 456 | // takes to be heard again. Learnt from each landing, for the whole run. | ||
| 457 | static double s_feedResyncLeadS; | ||
| 458 | |||
| 459 | int m_streamSlot = 1; // stream slot number (1-5), determines PipeWire node name | ||
| 460 | |||
| 461 | // Audio-only PipeWire playback feeds the slot's persistent output stream | ||
| 462 | // through this appsink instead of owning a pipewiresink (see | ||
| 463 | // PipeWireOutputStream.h). Both null on every other path. | ||
| 464 | PipeWireOutputStream* m_pwStream = nullptr; | ||
| 465 | GstElement* m_pwFeed = nullptr; | ||
| 466 | void ReleasePipeWireStream(); | ||
| 467 | |||
| 468 | std::atomic<bool> m_audible{false}; | ||
| 469 | |||
| 470 | // The output whose audio feeds the WLED tap. Only read or written | ||
| 471 | // under s_sampleMutex, and never dereferenced by the tap's reader: the | ||
| 472 | // owner can be closing on another thread. | ||
| 473 | static GStreamerOutput* m_currentInstance; | ||
| 474 | |||
| 475 | // Every output currently driving a PixelOverlay model. The overlay push | ||
| 476 | // is per-output, unlike the WLED audio tap, so it cannot hang off | ||
| 477 | // m_currentInstance -- slot 1 always claims that, which left a companion | ||
| 478 | // stream's model receiving nothing. | ||
| 479 | static std::mutex m_overlayOutputsLock; | ||
| 480 | static std::vector<GStreamerOutput*> m_overlayOutputs; | ||
| 481 | bool PushVideoOverlayFrame(); | ||
| 482 | }; | ||
| 483 | |||
| 484 | #endif | ||
| 485 |