Program Listing for File video_manager.hpp#
↰ Return to documentation for file (src/video/video_manager.hpp)
#pragma once
#include <QImage>
#include <QObject>
#include <memory>
#include "core/settings.hpp"
#include "video/video_frame.hpp"
namespace mosaic {
/// @brief Orchestrates one VideoGrabber + VideoEncoder pair per configured camera.
///
/// VideoManager owns the complete video pipeline for a session. It matches
/// the lifecycle of the cameras: open them once at startup, then start/stop
/// recording repeatedly without reopening.
///
/// @par Thread model
/// Each camera runs two background threads:
/// - **VideoGrabber** — grabs frames from Pylon (or generates test patterns).
/// - **VideoEncoder** — encodes frames via FFmpeg and writes the timestamp CSV.
///
/// Frames are passed between threads through a lock-free SPSC RingBuffer.
/// All *public* methods of VideoManager itself must be called from the main
/// thread.
///
/// @par Lifecycle
/// @code{.cpp}
/// VideoManager vm;
/// int opened = vm.open(settings.video); // opens cameras once
///
/// // Start/stop can repeat throughout the session
/// vm.start("recordings/2026-06-04_14-32-05", "video", settings.video);
/// // ... recording in progress ...
/// vm.stop(); // blocks until all encoders have flushed and closed their files
///
/// vm.close(); // release camera handles (called by destructor)
/// @endcode
///
/// @see VideoGrabber, VideoEncoder, FrameTimestampWriter, RecordManager
class VideoManager : public QObject {
Q_OBJECT
public:
explicit VideoManager(QObject* parent = nullptr);
~VideoManager() override;
/// @brief Opens camera devices (or stub generators) for all configured cameras.
///
/// @param settings Video settings from AppSettings. The @c cameras vector
/// determines how many devices are opened.
/// @returns The number of cameras successfully opened. May be less
/// than @c settings.cameras.size() if some devices fail.
int open(const VideoSettings& settings);
/// @brief Closes all open camera handles.
///
/// Calls stop() first if a recording is active.
void close();
/// @brief Starts the grab loop for all cameras without creating encoders.
///
/// Call this after open() to see live preview frames in the QML monitor
/// before any recording session begins. Safe to call if grabbers are
/// already running (no-op for those cameras).
void start_preview();
/// @brief Starts grabbing and encoding for all open cameras.
///
/// Output files:
/// - @c \<sessionDir\>/\<videoBasename\>_N.mp4
/// - @c \<sessionDir\>/timestamps_camN.csv
///
/// @param sessionDir Absolute path to the session folder (must exist).
/// @param videoBasename Basename for video files (e.g. @c "video").
/// @param settings Video settings (codec, preset, per-camera FPS/resolution).
void start(const QString& sessionDir, const QString& videoBasename,
const VideoSettings& settings);
/// @brief Stops all grabbers and encoders, flushing and closing every file.
///
/// Blocks until all encoder threads have exited (up to 10 s per camera).
void stop();
/// @brief Re-applies exposure/gain/gamma/black-level/white-balance/
/// auto-target/digital-shift for one camera to already-open hardware,
/// without stopping or reopening it.
///
/// Call this after the caller has mutated the corresponding
/// CameraParameters in place (e.g. via the settings UI). Structural
/// parameters (resolution, pixel format, frame rate, hardware trigger)
/// still require a full close()+open() to take effect and are not
/// touched by this call.
///
/// @param configIndex Position in the *configured* settings.cameras
/// array (VideoManager::CameraUnit::configIndex),
/// not the camera's position among successfully
/// opened units. No-op if no open unit matches.
void apply_live_params(int configIndex);
/// @brief Requests one full-resolution frame from the given camera for
/// room (extrinsic) calibration — see VideoGrabber::request_calibration_frame().
/// Delivered asynchronously via calibration_frame_ready(), echoing token
/// back verbatim so a caller that requests one frame per "shot" can
/// reject a reply for a shot it has already moved past. No-op if no
/// open unit matches configIndex.
void request_calibration_frame(int configIndex, uint64_t token = 0);
/// @returns @c true while a recording session is active.
[[nodiscard]] bool is_recording() const;
/// @returns @c true while a live preview session is active (started via
/// start_preview(), not yet stopped by start()/close()). Used to guard
/// actions that would race with a running ActionCommandTicker, which
/// touches Pylon's CTlFactory from a background thread for as long as
/// either preview or recording keeps it alive.
[[nodiscard]] bool is_previewing() const;
/// @returns The number of cameras that were successfully opened.
[[nodiscard]] int camera_count() const;
/// @returns Total frames encoded across all cameras since the last start().
[[nodiscard]] int64_t total_frames_encoded() const;
/// @returns Total frames dropped (ring buffer overflow) since last start().
[[nodiscard]] int64_t total_frames_dropped() const;
/// @brief Per-camera real-time performance snapshot.
struct CameraStats {
double fps = 0.0; ///< Measured grab rate (frames/s).
int64_t framesGrabbed = 0; ///< Total frames grabbed since start().
int64_t framesEncoded = 0; ///< Total frames encoded since start().
int64_t framesDropped = 0; ///< Frames lost to ring-buffer overflow.
int ringFillPct = 0; ///< Ring buffer fill level, 0–100.
bool grabberRunning = false;
int64_t lastFrameElapsedNs = -1; ///< elapsed_ns() of the most recent frame, -1 if none yet.
double configuredFps = 0.0; ///< VideoGrabber::configured_fps().
double achievableFps = -1.0; ///< VideoGrabber::achievable_fps(), -1 = not yet measured.
int64_t incompleteFrames =
0; ///< VideoGrabber::incomplete_frames_total() (GVSP packet loss).
};
/// @brief Returns a performance snapshot for one camera.
///
/// @param index Zero-based camera index. Returns a zeroed struct if out of range.
[[nodiscard]] CameraStats camera_stats(int index) const;
/// @returns The number of GigE Vision Action Command ticks fired during
/// this camera group's most recently *attempted* arm_and_fire_action_commands()
/// call (recording or preview, whichever ran most recently), or -1 if
/// that call didn't use Action1 triggering at all — reset to -1 at the
/// start of every arm_and_fire_action_commands() call, before a ticker
/// is even created, so a session that doesn't use Action1 never reports
/// a stale count left over from an earlier session that did. Snapshotted
/// in stop_action_ticker() right before the ticker is destroyed, since
/// the live tick count itself is otherwise lost the instant the ticker
/// object goes away. This is one shared, group-wide count — see
/// camera_action_command_ready() for whether it's even meaningful for a
/// particular camera. Combined with a camera's own frames_grabbed() by
/// SessionHealthReport to report how many trigger broadcasts a camera
/// missed, not just how many frames it captured.
[[nodiscard]] int64_t action_ticks_fired() const;
/// @returns Whether this camera was Action1-ready (and therefore part of
/// the Action Command target group whose shared tick count
/// action_ticks_fired() reports) as of its last open()/probe. False for
/// out-of-range indices and for any camera not opened for Action1
/// triggering, in which case action_ticks_fired()'s count is unrelated
/// to it and callers must not apply it to that camera (a session can mix
/// Action1-armed and free-running cameras).
[[nodiscard]] bool camera_action_command_ready(int index) const;
/// @brief One camera's final counters for the recording that just ended.
///
/// Deliberately a *snapshot*, not a live read: every live counter above is
/// zeroed again by VideoGrabber::start_grabbing() the moment preview
/// resumes, and Application::initialize() connects its own preview-restart
/// handler to RecordManager::recording_stopped *before* MainWindow exists,
/// so it always runs first. Anything reading camera_stats() from a
/// recording_stopped handler therefore sees zeros for every camera. Use
/// last_recording_snapshot() instead for post-recording reporting.
struct RecordingCameraSnapshot {
/// Position in settings.cameras this unit was opened from — NOT its
/// position in the internal (compacted) unit list. See
/// CameraUnit::configIndex.
int configIndex = 0;
int64_t framesGrabbed = 0;
int64_t framesEncoded = 0;
int64_t framesDropped = 0;
int64_t incompleteFrames = 0;
double configuredFps = 0.0;
double achievableFps = -1.0; ///< -1 = not measured
bool actionCommandReady = false;
};
/// @returns One entry per camera that was actually open for the recording
/// that just ended, captured at the end of stop(). Cameras that were
/// configured but never opened (duplicate serial, failed open(), dead
/// link) have no entry at all — callers wanting to report them must
/// reconcile against settings.cameras via each entry's configIndex.
/// Empty until the first stop().
[[nodiscard]] const std::vector<RecordingCameraSnapshot>& last_recording_snapshot() const;
/// @returns action_ticks_fired() as captured at the end of the recording
/// that just ended, for the same reason last_recording_snapshot() exists:
/// the live value is reset to -1 by the preview restart that follows
/// recording_stopped. -1 if that recording didn't use Action1 triggering.
[[nodiscard]] int64_t last_recording_action_ticks() const;
/// @brief Discards the previous recording's snapshot.
///
/// Must be called when a recording *starts*, not only relied on being
/// overwritten when one stops: stop() early-returns unless this manager is
/// actually recording, and a session can end without it ever having
/// started (video disabled for the session, or start() bailing because no
/// camera is open). Without this, such a session's health report would
/// present the *previous* recording's counters as its own.
void clear_recording_snapshot();
signals:
/// Emitted on the main thread when a camera device is successfully opened.
void camera_opened(int cameraIndex, int width, int height, double fps);
/// Emitted when a camera device is closed.
void camera_closed(int cameraIndex);
/// Emitted each time a frame is dropped because the ring buffer was full.
/// @param cameraIndex Which camera dropped the frame.
/// @param frameId The frame counter value of the dropped frame.
void frame_dropped(int cameraIndex, int64_t frameId);
/// Emitted when a camera grab or encode error occurs.
void camera_error(int cameraIndex, QString message);
/// Emitted when all encoders have finished (files closed and flushed).
void recording_stopped();
/// Throttled (~15 fps) BGR preview for live QML display.
void frame_preview(int cameraIndex, QImage frame);
/// Full-resolution frame delivered in response to request_calibration_frame().
/// token is whatever was passed to request_calibration_frame().
void calibration_frame_ready(int cameraIndex, QImage frame, uint64_t token);
/// Passthrough of VideoGrabber::action_command_capability() — reports
/// whether a camera's firmware supports GigE Vision Action Command
/// triggering, once probed at open() time. Only fires for cameras that
/// actually requested Action1 triggering (hwTriggerEnabled &&
/// hwTriggerSource == "Action1").
void action_command_capability(int cameraIndex, bool supported);
/// Passthrough of VideoGrabber::achievable_fps_changed() — the camera's
/// own measured ResultingFrameRate, for the achievable-rate readout beside
/// the exposure controls. `cameraIndex` is a *config* index (a position in
/// settings.cameras), like every other signal here and unlike
/// camera_stats()'s unit index. A closing camera reports -1.0 so a stale
/// reading can't outlive the session that measured it.
void achievable_fps_changed(int cameraIndex, double fps);
private slots:
void on_encoder_stopped(int cameraIndex, int64_t frames);
private:
// Arms every unit's grabber (start_grabbing()) first, then, for every
// Action1-ready unit, starts a background ActionCommandTicker that
// fires one GigE Vision IssueActionCommand() per frame, continuously,
// for as long as the ticker runs — shared by start_preview() and
// start(), since a camera configured for Action1 hangs identically
// (produces zero frames) in either mode until action commands start
// arriving. See gige_action_command.hpp.
void arm_and_fire_action_commands();
// Stops and destroys the current ActionCommandTicker, if any. Called
// from every path that stops grabbers (stop(), close(), and start()'s
// preview-teardown step) so the ticker never outlives the cameras it's
// firing at. No-op if no ticker is running.
void stop_action_ticker();
struct Impl;
std::unique_ptr<Impl> d;
};
} // namespace mosaic