GCC Code Coverage Report


Directory: ./
File: mediaoutput/VideoInputManager.h
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 28 28 100.0%
Functions: 3 3 100.0%
Branches: 1 2 50.0%

Line Branch Exec Source
1 #pragma once
2 /*
3 * This file is part of the Falcon Player (FPP) and is Copyright (C)
4 * 2013-2026 by the Falcon Player Developers.
5 *
6 * The Falcon Player (FPP) is free software, and is covered under
7 * multiple Open Source licenses. Please see the included 'LICENSES'
8 * file for descriptions of what files are covered by each license.
9 *
10 * This source file is covered under the LGPL v2.1 as described in the
11 * included LICENSE.LGPL file.
12 */
13
14 #include <atomic>
15 #include <chrono>
16 #include <condition_variable>
17 #include <cstdint>
18 #include <map>
19 #include <mutex>
20 #include <string>
21 #include <thread>
22 #include <vector>
23
24 #if __has_include(<gst/gst.h>)
25 #define HAS_GSTREAMER_VIDEO_INPUT
26 #include <gst/gst.h>
27 #endif
28
29 /**
30 * VideoInputManager — manages persistent PipeWire video source pipelines.
31 *
32 * Each video input source runs a GStreamer producer pipeline:
33 * <source> → videoconvert → pipewiresink (mode=provide, Video/Source)
34 *
35 * Sources start at Init() and persist independently of media playback.
36 * They appear as Video/Source nodes in the PipeWire graph. Consumers
37 * (VideoOutputManager) target them via the pipewiresrc target-object
38 * property using the source's node name.
39 *
40 * Requires pipewiresink mode=provide support (PipeWire GStreamer plugin
41 * >= 1.7.0 or equivalent patched build).
42 *
43 * Config file: $mediaDirectory/config/pipewire-video-input-sources.json
44 */
45 class VideoInputManager {
46 public:
47 static VideoInputManager& Instance();
48
49 /// Load config and start enabled sources, on the reload worker (see
50 /// RequestReload()), so this returns before they are running.
51 void Init();
52
53 /// Reload config — stops all sources, reloads, restarts enabled ones.
54 void Reload();
55
56 /// Reload() on the manager's own worker thread, so whoever asks (fppd's
57 /// main loop, for the reloadVideoInputs command) doesn't wait out yt-dlp
58 /// and pipeline teardown. A request made while a reload is running
59 /// makes the worker go round once more.
60 void RequestReload();
61
62 /// Called by a source's run thread when its pipeline fails to start,
63 /// errors or ends: restart that source on the reload worker after a
64 /// backoff (2s, doubling to 60s; back to 2s once it has run for five
65 /// minutes). Takes only m_reloadMutex -- the run thread must never
66 /// wait on m_mutex, since RestartSource() joins it under that lock.
67 void ScheduleSourceRestart(int sourceId);
68
69 /// Stop all source pipelines and clean up.
70 void Shutdown();
71
72 /// Check if any sources are configured.
73 bool HasSources() const;
74
75 /// Check if any sources are currently running.
76 bool HasActiveSources() const;
77
78 /// Get count of running source pipelines.
79 int ActiveSourceCount() const;
80
81 /// Get the PipeWire node name for a source by its config ID.
82 /// Returns empty string if not found or not running.
83 std::string GetSourceNodeName(int sourceId) const;
84
85 /// Get list of all configured source node names (for UI/API).
86 std::vector<std::pair<int, std::string>> GetSourceList() const;
87
88 /// Get the framerate of a source by its intervideo channel name.
89 /// Returns the detected (or configured) fps, or 0 if not found.
90 int GetSourceFramerate(const std::string& channelName) const;
91
92 /// True when the source publishing `channelName` has a live pipeline.
93 /// Lets a consumer that missed NotifyProducerReady -- because the source
94 /// was already up when the consumer was (re)configured -- start anyway.
95 bool IsSourceRunning(const std::string& channelName) const;
96
97 /// Grab a single JPEG frame from a running source, for UI preview.
98 ///
99 /// Taps the source's intervideo channel with a short-lived
100 /// intervideosrc, so it costs the source pipeline nothing and — unlike
101 /// opening /dev/videoN a second time — never contends with the capture
102 /// device (UVC cameras allow a single opener).
103 ///
104 /// Returns false if the source isn't running or no frame arrived within
105 /// timeoutMs.
106 bool GrabSnapshotJPEG(int sourceId, int maxWidth, int timeoutMs,
107 std::vector<uint8_t>& jpegOut);
108
109 private:
110 1887 VideoInputManager() = default;
111 ~VideoInputManager();
112
113 VideoInputManager(const VideoInputManager&) = delete;
114 VideoInputManager& operator=(const VideoInputManager&) = delete;
115
116 struct SourceInfo {
117 int id = 0;
118 std::string name;
119 std::string type; // "videotestsrc", "v4l2src", "rtspsrc", "urisrc", "rtpsrc"
120 std::string pipeWireNodeName;
121 bool enabled = true;
122
123 // videotestsrc settings
124 std::string pattern; // "smpte", "snow", "black", etc.
125
126 // v4l2src settings
127 std::string device; // "/dev/video0"
128
129 // v4l2src device controls. Defaults mean "leave the camera's own
130 // setting alone", so a config written before these existed behaves
131 // exactly as it did. See V4L2Device.h for why these are device
132 // controls and not something the pipeline can do downstream.
133 int powerLineFrequency = -1; // -1 camera default, 0 off, 1 = 50Hz, 2 = 60Hz
134 std::string exposureMode = "camera"; // "camera", "auto", "manual"
135 int exposureTime100us = -1; // manual shutter, 100us units
136 int dynamicFramerate = -1; // -1 camera default, 0 hold fps, 1 allow AE to drop it
137
138 // rtspsrc settings
139 std::string uri; // "rtsp://host/path"
140 int latency = 200; // ms
141
142 // urisrc settings (uri field shared with rtspsrc)
143 double bufferSec = 3.0; // seconds of buffering
144
145 // rtpsrc settings
146 int port = 5004; // UDP port
147 std::string encoding; // "H264", "H265", "MP2T", "RAW", "JPEG"
148 std::string multicastGroup; // optional multicast address
149
150 // Common settings
151 int width = 320;
152 int height = 240;
153 int framerate = 10;
154
155 // Audio extraction settings
156 bool audioEnabled = false; // Extract and publish audio from this source
157 std::string audioPipeWireNodeName; // Generated: PipeWire audio source node name
158
159 #ifdef HAS_GSTREAMER_VIDEO_INPUT
160 GstElement* pipeline = nullptr;
161 GstElement* audioPipeline = nullptr;
162 #endif
163 std::thread runThread;
164 std::thread audioRunThread;
165 std::atomic<bool> shutdownRequested{false};
166 std::atomic<bool> audioShutdownRequested{false};
167 // atomic: read/written from both the owning thread and the bus
168 // monitor thread spawned in StartSource()/StartSourceWithAudio()
169 // without holding m_mutex.
170 std::atomic<bool> running{false};
171
172
1/2
✓ Branch 0 taken 86 times.
✗ Branch 1 not taken.
258 SourceInfo() = default;
173 108 SourceInfo(SourceInfo&& o) noexcept
174 108 : id(o.id), name(std::move(o.name)), type(std::move(o.type)),
175 108 pipeWireNodeName(std::move(o.pipeWireNodeName)),
176 108 enabled(o.enabled), pattern(std::move(o.pattern)),
177 108 device(std::move(o.device)),
178 108 powerLineFrequency(o.powerLineFrequency),
179 108 exposureMode(std::move(o.exposureMode)),
180 108 exposureTime100us(o.exposureTime100us),
181 108 dynamicFramerate(o.dynamicFramerate),
182 108 uri(std::move(o.uri)), latency(o.latency),
183 108 bufferSec(o.bufferSec),
184 108 port(o.port), encoding(std::move(o.encoding)),
185 108 multicastGroup(std::move(o.multicastGroup)),
186 108 width(o.width), height(o.height), framerate(o.framerate),
187 108 audioEnabled(o.audioEnabled),
188 108 audioPipeWireNodeName(std::move(o.audioPipeWireNodeName)),
189 #ifdef HAS_GSTREAMER_VIDEO_INPUT
190 108 pipeline(o.pipeline),
191 108 audioPipeline(o.audioPipeline),
192 #endif
193 108 runThread(std::move(o.runThread)),
194 108 audioRunThread(std::move(o.audioRunThread)),
195 108 shutdownRequested(o.shutdownRequested.load()),
196 108 audioShutdownRequested(o.audioShutdownRequested.load()),
197 108 running(o.running.load()) {
198 #ifdef HAS_GSTREAMER_VIDEO_INPUT
199 108 o.pipeline = nullptr;
200 108 o.audioPipeline = nullptr;
201 #endif
202 108 }
203 SourceInfo(const SourceInfo&) = delete;
204 SourceInfo& operator=(const SourceInfo&) = delete;
205 };
206
207 /// Load source config from JSON file.
208 bool LoadConfig();
209
210 /// Start a single source pipeline.
211 bool StartSource(SourceInfo& source);
212
213 /// Build and start pipeline with separate video + audio streams.
214 bool StartSourceWithAudio(SourceInfo& source);
215
216 /// Build the capsfilter constraining what we ask the capture device
217 /// for, or "" to leave the device unconstrained. See the call site
218 /// for why an unsupported mode must not be pinned.
219 std::string BuildDeviceCaps(const SourceInfo& source);
220
221 /// Start every enabled source just loaded into m_sources. Called with
222 /// m_mutex held through `lock`; releases it while yt-dlp resolves any
223 /// YouTube sources.
224 void StartLoadedSources(std::unique_lock<std::mutex>& lock);
225
226 /// Stop a single source pipeline.
227 void StopSource(SourceInfo& source);
228
229 /// Restart one dead source in place (reload worker only).
230 void RestartSource(int sourceId);
231
232 /// Start the reload worker if it isn't running. m_reloadMutex held.
233 void EnsureReloadWorkerLocked();
234
235 /// Take every source out of m_sources (under m_mutex), then stop them
236 /// with the lock released. StopSource() joins each source's run thread,
237 /// and a run thread can be waiting on m_mutex: it calls
238 /// VideoOutputManager::NotifyProducerReady(), which reads the framerate
239 /// back through GetSourceFramerate(). Joining it under m_mutex
240 /// deadlocks both managers.
241 void StopAllSources();
242
243 /// Join (and discard) any pipeline-teardown threads queued up by
244 /// StopSource(). Must be called before re-opening the same device/URI
245 /// (start of Reload()) and at Shutdown() so nothing outlives the
246 /// manager.
247 void JoinTeardownThreads();
248
249 mutable std::mutex m_mutex;
250 std::vector<SourceInfo> m_sources;
251 bool m_initialized = false;
252 // Guards Shutdown() so the destructor doesn't re-run it after main() already
253 // did (the "Shutdown complete" was logged twice). Keeps teardown single-shot.
254 std::atomic<bool> m_shutdownDone{false};
255
256 // StopSource() offloads gst_element_set_state(..., GST_STATE_NULL) +
257 // unref to a background thread so callers (Shutdown/Reload) don't block
258 // on a wedged pipeline. Those threads used to be detach()'d, which let
259 // them (and their pipeline refs) pile up across repeated Reloads and
260 // race a subsequent StartSource() trying to reopen the same device.
261 // They're tracked here instead and joined before the device can be
262 // reopened, and at Shutdown().
263 std::mutex m_teardownMutex;
264 std::vector<std::thread> m_teardownThreads;
265
266 // RequestReload()'s worker, which also runs the restarts that
267 // ScheduleSourceRestart() queues. m_reloadMutex guards all of these.
268 struct RestartState {
269 std::chrono::steady_clock::time_point due;
270 std::chrono::steady_clock::time_point lastFailure;
271 int delayMs = 0;
272 bool pending = false;
273 };
274 std::mutex m_reloadMutex;
275 std::condition_variable m_reloadCv;
276 std::thread m_reloadThread;
277 bool m_reloadPending = false;
278 bool m_reloadRunning = false;
279 bool m_stopReloads = false;
280 std::map<int, RestartState> m_restarts;
281
282 static constexpr int RESTART_MIN_DELAY_MS = 2000;
283 static constexpr int RESTART_MAX_DELAY_MS = 60000;
284 static constexpr int RESTART_QUIET_RESET_MS = 5 * 60 * 1000;
285 };
286