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>
28#include <promeki/sharedptr.h>
30
31struct _snd_pcm;
32typedef struct _snd_pcm snd_pcm_t;
33
34PROMEKI_NAMESPACE_BEGIN
35
36// Shared re-queue gate for the dma-buf zero-copy capture path; defined
37// in v4l2mediaio.cpp. Held by SharedPtr so it can outlive the
38// V4l2MediaIO while in-flight frames still reference exported buffers.
39struct V4l2RequeueGate;
40
151class V4l2MediaIO : public DedicatedThreadMediaIO {
152 PROMEKI_OBJECT(V4l2MediaIO, DedicatedThreadMediaIO)
153 public:
155 static inline const MediaIOStats::ID StatsCaptured{"V4l2Captured"};
157 static inline const MediaIOStats::ID StatsAlsaOverruns{"V4l2AlsaOverruns"};
158
160 V4l2MediaIO(ObjectBase *parent = nullptr);
161
163 ~V4l2MediaIO() override;
164
176 int instanceID() const { return _instanceId; }
177
179 static PixelFormat::ID v4l2ToPixelFormat(uint32_t v4l2fmt);
180
182 static uint32_t pixelFormatToV4l2(PixelFormat::ID pd);
183
202 static bool captureFormatAllowsZeroCopy(const PixelFormat &pd) { return !pd.isCompressed(); }
203
204 protected:
205 Error executeCmd(MediaIOCommandOpen &cmd) override;
206 Error executeCmd(MediaIOCommandClose &cmd) override;
207 Error executeCmd(MediaIOCommandRead &cmd) override;
208 Error executeCmd(MediaIOCommandStats &cmd) override;
209
210 // Wakes the executeCmd(Read) pop-loop so close() can
211 // proceed past an in-flight blocked read. Sets the
212 // _readCancelled flag — the loop polls it on each
213 // short-timeout pop wakeup.
214 void cancelBlockingWork() override;
215
216 private:
217
218 // describe() / proposeOutput overrides intentionally
219 // omitted for v1: the V4L2 device-mode enumeration
220 // path lives in v4l2QueryDevice and is already exposed
221 // through @ref MediaIOFactory::queryDevice (used by
222 // @c mediaplay @c --probe). A future revision will
223 // surface it here so the planner can pick a device
224 // mode without paying the cost of opening the
225 // capture pipeline. Today the planner's
226 // open()/close() fallback handles V4L2 sources
227 // correctly — at the cost of one ALSA + V4L2 open
228 // cycle during planning.
229
230 // -- V4L2 helpers --
231 Error openVideo(const MediaIO::Config &cfg);
232 Error startStreaming();
233 void stopStreaming();
234 void closeVideo();
235
236 // -- ALSA helpers --
237 Error openAudio(const MediaIO::Config &cfg);
238 void closeAudio();
239
240 // -- Capture threads --
241 void stopThreads();
242 void videoCaptureLoop();
243 void audioCaptureLoop();
244
245 // -- V4L2 state --
246 //
247 // One entry per V4L2 capture (vb2) buffer. In the MMAP
248 // path @c start / @c length describe the host mapping; in
249 // the dma-buf zero-copy path @c dmabufFd holds the
250 // device-lifetime fd exported once via VIDIOC_EXPBUF
251 // (@c start stays null), bounding open fds to pool depth.
252 struct MmapBuffer {
253 void *start = nullptr;
254 size_t length = 0;
255 int dmabufFd = -1;
256 };
257 int _fd = -1;
258 List<MmapBuffer> _buffers;
259 bool _streaming = false;
260 ImageDesc _imageDesc;
261
262 // -- dma-buf zero-copy path --
263 //
264 // _zeroCopy is set when the EXPBUF probe in openVideo
265 // succeeds; _bufLength is the per-buffer allocation size
266 // (the dma-buf plane size); _requeueGate decouples the
267 // per-frame re-queue callbacks from this object's
268 // lifetime (callbacks may fire on a downstream thread
269 // after close).
270 bool _zeroCopy = false;
271 size_t _bufLength = 0;
272 SharedPtr<V4l2RequeueGate> _requeueGate;
273
274 // -- ALSA state --
275 snd_pcm_t *_pcm = nullptr;
276 AudioDesc _audioDesc;
277 bool _audioEnabled = false;
278
279 // -- Capture threads --
280 Atomic<bool> _stopFlag{false};
281 Atomic<int> _deviceError{0}; // errno from device failure (ENODEV etc.)
282 // Set by cancelBlockingWork() when MediaIO::close()
283 // wants to unwind an in-flight Read whose strand
284 // worker is parked in _videoQueue.pop(). The pop
285 // uses a short timeout so the loop notices this
286 // flag (and _deviceError) within one tick of being
287 // raised. Reset at the start of every Open so a
288 // closed-then-reopened MediaIO doesn't carry the
289 // previous instance's cancellation forward.
290 Atomic<bool> _readCancelled{false};
291 BasicThread _videoThread;
292 BasicThread _audioThread;
293 int _instanceId = 0;
294
295 // -- Video frame queue (video thread → read command) --
296 static constexpr int VideoQueueDepth = 2;
297 Queue<VideoPayload::Ptr> _videoQueue;
298
299 // -- Audio ring buffer (audio thread → read command) --
300 //
301 // Each ALSA capture batch is pushed with a wall-clock
302 // MediaTimeStamp anchor; the ring's anchor queue
303 // recovers the PTS of the first popped sample on
304 // drain, so downstream drift correction sees real
305 // ALSA delivery rate via samples / ts_delta.
306 AudioBuffer _audioRing;
307
308 // -- Telemetry (updated atomically by capture threads) --
309 Atomic<int64_t> _framesCaptured{0};
310 Atomic<int64_t> _alsaOverruns{0};
311
312 // -- Debug reporting --
313 PeriodicCallback _debugReport;
314 int64_t _ringAccum = 0; // sum of ring levels sampled each frame
315 int64_t _ringAccumFrames = 0; // frames sampled since last report
316 double _ringAvgBaseline = 0.0; // first report's average
317 TimeStamp _ringBaselineTime; // V4L2 timestamp at first report
318 bool _ringBaselineSet = false;
319
320 // -- Capture timestamp tracking (from V4L2 DQBUF) --
321 TimeStamp _lastCaptureTime; // most recent frame's V4L2 timestamp
322 TimeStamp _firstCaptureTime; // first frame's V4L2 timestamp
323 int64_t _firstCaptureFrame = -1; // frame index of first timestamp
324 double _frameDeltaSum = 0.0; // sum of inter-frame intervals (sec) this period
325 double _frameDeltaSqSum = 0.0; // sum of squared deltas for jitter
326 int64_t _frameDeltaCount = 0; // deltas accumulated this period
327 double _prevPeriodFps = 0.0; // previous period's measured fps
328
329 // -- Stall-overflow protection --
330 bool _ringOverflowWarned = false;
331
332 // -- Capture-thread instrumentation --
333 //
334 // Populated by the V4L2 capture thread and drained
335 // each period by the debug report on the strand. All
336 // fields reset to zero at the end of each report so
337 // averages reflect a single 1s window.
338 //
339 // Sequence tracking uses @c vbuf.sequence from
340 // VIDIOC_DQBUF to detect kernel-side frame drops
341 // (where the camera produced a frame but no buffer
342 // was available to hold it). Loop-iteration timing
343 // measures how long our capture loop spends between
344 // successive DQBUF returns — if it's significantly
345 // longer than the nominal frame period the capture
346 // thread is the bottleneck. DQBUF lag compares
347 // @c vbuf.timestamp to wall-clock @c now() so we can
348 // tell how stale the frame is by the time we see it.
349 Atomic<uint32_t> _lastVbufSequence{0};
350 Atomic<int64_t> _kernelDroppedPeriod{0};
351 Atomic<int64_t> _loopIterationsPeriod{0};
352 Atomic<int64_t> _loopTimeSumUsPeriod{0};
353 Atomic<int64_t> _loopTimeMaxUsPeriod{0};
354 Atomic<int64_t> _dqbufLagSumUsPeriod{0};
355 Atomic<int64_t> _dqbufLagMaxUsPeriod{0};
356 Atomic<int64_t> _dqbufLagCountPeriod{0};
357 // Count of frames whose hardware SOE timestamp was so
358 // far off the wall clock that we substituted the
359 // dequeue time instead. Should be ~1 at stream
360 // startup; any steady-state non-zero value indicates
361 // the UVC PTS regression isn't locking (driver bug or
362 // camera glitch) and deserves investigation.
363 Atomic<int64_t> _timestampSubstitutedPeriod{0};
364 // Capture-thread-only state (no atomic needed):
365 bool _seqInitialized = false;
366 int64_t _prevIterUs = 0;
367
368 // -- General state --
369 FrameRate _frameRate;
370 FrameCount _frameCount{0};
371};
372
377class V4l2Factory : public MediaIOFactory {
378 public:
379 V4l2Factory() = default;
380
381 String name() const override { return String("V4L2"); }
382 String displayName() const override { return String("V4L2 Capture"); }
383 String description() const override {
384 return String("V4L2 video capture with optional ALSA audio (Linux)");
385 }
386
387 bool canBeSource() const override { return true; }
388
389 bool canHandlePath(const String &path) const override;
390 StringList enumerate() const override;
391 Config::SpecMap configSpecs() const override;
392 List<MediaDesc> queryDevice(const Config &config) const override;
393 void printDeviceInfo(const Config &config) const override;
394
395 MediaIO *create(const Config &config, ObjectBase *parent = nullptr) const override;
396};
397
398PROMEKI_NAMESPACE_END
399
400
401#endif // PROMEKI_ENABLE_V4L2