libpromeki 1.0.0-alpha
PROfessional MEdia toolKIt
 
Loading...
Searching...
No Matches
v4l2mediaio.h
Go to the documentation of this file.
1
8#pragma once
9
10
11#include <promeki/config.h>
12#if PROMEKI_ENABLE_V4L2
13#include <promeki/namespace.h>
14#include <promeki/atomic.h>
15#include <promeki/audiobuffer.h>
16#include <promeki/basicthread.h>
17#include <promeki/audiodesc.h>
19#include <promeki/framerate.h>
20#include <promeki/imagedesc.h>
21#include <promeki/list.h>
24#include <promeki/mutex.h>
26#include <promeki/pixelformat.h>
27#include <promeki/queue.h>
29
30struct _snd_pcm;
31typedef struct _snd_pcm snd_pcm_t;
32
33PROMEKI_NAMESPACE_BEGIN
34
131class V4l2MediaIO : public DedicatedThreadMediaIO {
132 PROMEKI_OBJECT(V4l2MediaIO, DedicatedThreadMediaIO)
133 public:
135 static inline const MediaIOStats::ID StatsCaptured{"V4l2Captured"};
137 static inline const MediaIOStats::ID StatsAlsaOverruns{"V4l2AlsaOverruns"};
138
140 V4l2MediaIO(ObjectBase *parent = nullptr);
141
143 ~V4l2MediaIO() override;
144
156 int instanceID() const { return _instanceId; }
157
159 static PixelFormat::ID v4l2ToPixelFormat(uint32_t v4l2fmt);
160
162 static uint32_t pixelFormatToV4l2(PixelFormat::ID pd);
163
164 protected:
165 Error executeCmd(MediaIOCommandOpen &cmd) override;
166 Error executeCmd(MediaIOCommandClose &cmd) override;
167 Error executeCmd(MediaIOCommandRead &cmd) override;
168 Error executeCmd(MediaIOCommandStats &cmd) override;
169
170 // Wakes the executeCmd(Read) pop-loop so close() can
171 // proceed past an in-flight blocked read. Sets the
172 // _readCancelled flag — the loop polls it on each
173 // short-timeout pop wakeup.
174 void cancelBlockingWork() override;
175
176 private:
177
178 // describe() / proposeOutput overrides intentionally
179 // omitted for v1: the V4L2 device-mode enumeration
180 // path lives in v4l2QueryDevice and is already exposed
181 // through @ref MediaIOFactory::queryDevice (used by
182 // @c mediaplay @c --probe). A future revision will
183 // surface it here so the planner can pick a device
184 // mode without paying the cost of opening the
185 // capture pipeline. Today the planner's
186 // open()/close() fallback handles V4L2 sources
187 // correctly — at the cost of one ALSA + V4L2 open
188 // cycle during planning.
189
190 // -- V4L2 helpers --
191 Error openVideo(const MediaIO::Config &cfg);
192 Error startStreaming();
193 void stopStreaming();
194 void closeVideo();
195
196 // -- ALSA helpers --
197 Error openAudio(const MediaIO::Config &cfg);
198 void closeAudio();
199
200 // -- Capture threads --
201 void stopThreads();
202 void videoCaptureLoop();
203 void audioCaptureLoop();
204
205 // -- V4L2 state --
206 struct MmapBuffer {
207 void *start = nullptr;
208 size_t length = 0;
209 };
210 int _fd = -1;
211 List<MmapBuffer> _buffers;
212 bool _streaming = false;
213 ImageDesc _imageDesc;
214
215 // -- ALSA state --
216 snd_pcm_t *_pcm = nullptr;
217 AudioDesc _audioDesc;
218 bool _audioEnabled = false;
219
220 // -- Capture threads --
221 Atomic<bool> _stopFlag{false};
222 Atomic<int> _deviceError{0}; // errno from device failure (ENODEV etc.)
223 // Set by cancelBlockingWork() when MediaIO::close()
224 // wants to unwind an in-flight Read whose strand
225 // worker is parked in _videoQueue.pop(). The pop
226 // uses a short timeout so the loop notices this
227 // flag (and _deviceError) within one tick of being
228 // raised. Reset at the start of every Open so a
229 // closed-then-reopened MediaIO doesn't carry the
230 // previous instance's cancellation forward.
231 Atomic<bool> _readCancelled{false};
232 BasicThread _videoThread;
233 BasicThread _audioThread;
234 int _instanceId = 0;
235
236 // -- Video frame queue (video thread → read command) --
237 static constexpr int VideoQueueDepth = 2;
238 Queue<VideoPayload::Ptr> _videoQueue;
239
240 // -- Audio ring buffer (audio thread → read command) --
241 //
242 // Each ALSA capture batch is pushed with a wall-clock
243 // MediaTimeStamp anchor; the ring's anchor queue
244 // recovers the PTS of the first popped sample on
245 // drain, so downstream drift correction sees real
246 // ALSA delivery rate via samples / ts_delta.
247 AudioBuffer _audioRing;
248
249 // -- Telemetry (updated atomically by capture threads) --
250 Atomic<int64_t> _framesCaptured{0};
251 Atomic<int64_t> _alsaOverruns{0};
252
253 // -- Debug reporting --
254 PeriodicCallback _debugReport;
255 int64_t _ringAccum = 0; // sum of ring levels sampled each frame
256 int64_t _ringAccumFrames = 0; // frames sampled since last report
257 double _ringAvgBaseline = 0.0; // first report's average
258 TimeStamp _ringBaselineTime; // V4L2 timestamp at first report
259 bool _ringBaselineSet = false;
260
261 // -- Capture timestamp tracking (from V4L2 DQBUF) --
262 TimeStamp _lastCaptureTime; // most recent frame's V4L2 timestamp
263 TimeStamp _firstCaptureTime; // first frame's V4L2 timestamp
264 int64_t _firstCaptureFrame = -1; // frame index of first timestamp
265 double _frameDeltaSum = 0.0; // sum of inter-frame intervals (sec) this period
266 double _frameDeltaSqSum = 0.0; // sum of squared deltas for jitter
267 int64_t _frameDeltaCount = 0; // deltas accumulated this period
268 double _prevPeriodFps = 0.0; // previous period's measured fps
269
270 // -- Stall-overflow protection --
271 bool _ringOverflowWarned = false;
272
273 // -- Capture-thread instrumentation --
274 //
275 // Populated by the V4L2 capture thread and drained
276 // each period by the debug report on the strand. All
277 // fields reset to zero at the end of each report so
278 // averages reflect a single 1s window.
279 //
280 // Sequence tracking uses @c vbuf.sequence from
281 // VIDIOC_DQBUF to detect kernel-side frame drops
282 // (where the camera produced a frame but no buffer
283 // was available to hold it). Loop-iteration timing
284 // measures how long our capture loop spends between
285 // successive DQBUF returns — if it's significantly
286 // longer than the nominal frame period the capture
287 // thread is the bottleneck. DQBUF lag compares
288 // @c vbuf.timestamp to wall-clock @c now() so we can
289 // tell how stale the frame is by the time we see it.
290 Atomic<uint32_t> _lastVbufSequence{0};
291 Atomic<int64_t> _kernelDroppedPeriod{0};
292 Atomic<int64_t> _loopIterationsPeriod{0};
293 Atomic<int64_t> _loopTimeSumUsPeriod{0};
294 Atomic<int64_t> _loopTimeMaxUsPeriod{0};
295 Atomic<int64_t> _dqbufLagSumUsPeriod{0};
296 Atomic<int64_t> _dqbufLagMaxUsPeriod{0};
297 Atomic<int64_t> _dqbufLagCountPeriod{0};
298 // Count of frames whose hardware SOE timestamp was so
299 // far off the wall clock that we substituted the
300 // dequeue time instead. Should be ~1 at stream
301 // startup; any steady-state non-zero value indicates
302 // the UVC PTS regression isn't locking (driver bug or
303 // camera glitch) and deserves investigation.
304 Atomic<int64_t> _timestampSubstitutedPeriod{0};
305 // Capture-thread-only state (no atomic needed):
306 bool _seqInitialized = false;
307 int64_t _prevIterUs = 0;
308
309 // -- General state --
310 FrameRate _frameRate;
311 FrameCount _frameCount{0};
312};
313
318class V4l2Factory : public MediaIOFactory {
319 public:
320 V4l2Factory() = default;
321
322 String name() const override { return String("V4L2"); }
323 String displayName() const override { return String("V4L2 Capture"); }
324 String description() const override {
325 return String("V4L2 video capture with optional ALSA audio (Linux)");
326 }
327
328 bool canBeSource() const override { return true; }
329
330 bool canHandlePath(const String &path) const override;
331 StringList enumerate() const override;
332 Config::SpecMap configSpecs() const override;
333 List<MediaDesc> queryDevice(const Config &config) const override;
334 void printDeviceInfo(const Config &config) const override;
335
336 MediaIO *create(const Config &config, ObjectBase *parent = nullptr) const override;
337};
338
339PROMEKI_NAMESPACE_END
340
341
342#endif // PROMEKI_ENABLE_V4L2