Program Listing for File sync_manifest.hpp#

Return to documentation for file (src/analysis/sync_manifest.hpp)

#pragma once
#include <QJsonObject>
#include <QString>
#include <QVector>
#include <memory>

namespace mosaic {

// ── CameraSync ─────────────────────────────────────────────────────────────

struct CameraSync {
    int index = 0;
    QString videoFile;
    int framesCaptured   = 0;
    double fpsActual     = 0.0;
    double coveragePct   = 0.0; // % of master ticks with a "fresh" frame
    double meanDeltaMs   = 0.0; // mean |timing error| across all ticks
    double maxDeltaMs    = 0.0; // worst-case timing error
    int64_t firstWallNs  = 0;
    int64_t lastWallNs   = 0;
    int64_t seekOffsetMs = 0; // seek position in the video at master t = 0
};

// ── SyncManifest ───────────────────────────────────────────────────────────
//
// Aligns N camera streams to a uniform master timeline using per-frame
// monotonic (elapsed_ns) timestamps stored in timestamps_cam_N.csv.  All
// VideoGrabber threads share one process-wide elapsed_ns() origin, so these
// values are directly comparable across cameras without any cross-camera
// clock reconciliation — unlike wall_ns (system_clock), which can jump if
// the OS clock is adjusted mid-session and is therefore only used here for
// the audio-seek computation, which is inherently wall-clock-relative.
//
// For each master tick the nearest frame in every camera is assigned and
// its signed timing error (delta_ms) is recorded.  The result is stored in
// sync_manifest.json and used by SessionPlayerW for frame-accurate,
// synchronised multi-camera playback.
//
// Usage:
//   auto m = SyncManifest::generate(sessionPath);
//   m.save(sessionPath);
//   ...
//   auto m2 = SyncManifest::load(sessionPath);
//   player->seek(camIdx, m2.seek_offset_ms(camIdx));

class SyncManifest {
   public:
    SyncManifest() = default;

    // ── Factory ───────────────────────────────────────────────────────────
    // Generate from timestamps_cam_N.csv files inside sessionPath.
    // masterFps: the uniform output timeline rate (default 25 fps).
    static SyncManifest generate(const QString& sessionPath, double masterFps = 25.0);

    // Load existing sync_manifest.json from sessionPath.
    static SyncManifest load(const QString& sessionPath);

    // ── Persistence ───────────────────────────────────────────────────────
    bool save(const QString& sessionPath) const;

    // ── Validity ──────────────────────────────────────────────────────────
    [[nodiscard]] bool is_valid() const;

    // ── Timeline metadata ─────────────────────────────────────────────────
    [[nodiscard]] int camera_count() const;
    [[nodiscard]] int total_ticks() const;
    [[nodiscard]] double master_fps() const;
    [[nodiscard]] int64_t duration_ms() const;
    [[nodiscard]] int64_t t_origin_ns() const;      // master t=0 on the elapsed_ns clock
    [[nodiscard]] int64_t t_origin_wall_ns() const; // same instant, wall-clock (for reference)

    // ── Per-camera info ────────────────────────────────────────────────────
    [[nodiscard]] const CameraSync& camera_info(int idx) const;

    // ── Frame lookup ──────────────────────────────────────────────────────
    // frame_id assigned to master tick `tick` for camera `cameraIdx`.
    // Returns -1 if there is no data for that camera.
    [[nodiscard]] int frame_at_tick(int cameraIdx, int tick) const;

    // Signed timing error (ms) for the frame at the given tick.
    // Positive = frame was captured after the tick's ideal time.
    [[nodiscard]] double delta_ms_at_tick(int cameraIdx, int tick) const;

    // Nearest tick index for a master position in ms.
    [[nodiscard]] int tick_for_ms(int64_t posMs) const;

    // How far into the video file the player must seek to be at master t = 0.
    // Positive for cameras that started before t_origin (most cameras).
    [[nodiscard]] int64_t seek_offset_ms(int cameraIdx) const;

    // How far into the audio file the player must seek to be at master t = 0.
    // Derived from the gap between session_start_utc and t_origin.
    [[nodiscard]] int64_t audio_seek_ms() const;

    // Human-readable quality summary for all cameras.
    [[nodiscard]] QString quality_report() const;

   private:
    // Columnar storage: index = cameraIdx * totalTicks_ + tick
    QVector<int> frameIds_;  // -1 = no frame
    QVector<float> deltaMs_; // signed timing error per (camera, tick)

    QVector<CameraSync> cameras_;
    int totalTicks_        = 0;
    double masterFps_      = 25.0;
    int64_t durationMs_    = 0;
    int64_t tOriginNs_     = 0; // master t=0, elapsed_ns clock — alignment key
    int64_t tOriginWallNs_ = 0; // same instant, wall_ns — audio seek only
    int64_t stepNs_        = 0;
    int64_t audioSeekMs_   = 0;
    QString generatedAt_;

    struct FrameTs {
        int frameId       = 0;
        int64_t elapsedNs = 0;
        int64_t wallNs    = 0;
    };
    static QVector<FrameTs> read_timestamps(const QString& csvPath);
    static int64_t session_start_wall_ns(const QString& sessionPath);
};

} // namespace mosaic