/*
* This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "zm_catch2.h"
#include "zm_ffmpeg_camera.h"
#include "zm_time.h"
// ComputeRealtimePace() is the pure decision behind FfmpegCamera's "realtime=1"
// (ffmpeg -re style) pacing: given the current packet timestamp, the active
// anchor timestamp, the wall-clock time elapsed since that anchor, and the
// discontinuity cap, it decides whether to re-anchor and how long to sleep.
static const Microseconds kCap = std::chrono::duration_cast(Seconds(10));
TEST_CASE("ComputeRealtimePace: sleeps the full interval when no time has elapsed") {
// 40ms into the stream with zero wall-clock elapsed -> wait the whole 40ms.
RealtimePaceDecision d = ComputeRealtimePace(40000, 0, Microseconds(0), kCap);
REQUIRE_FALSE(d.reanchor);
REQUIRE(d.sleep == Microseconds(40000));
}
TEST_CASE("ComputeRealtimePace: sleeps only the remaining interval when partially elapsed") {
// Target is 40ms ahead of the anchor, 15ms has already passed -> sleep 25ms.
RealtimePaceDecision d = ComputeRealtimePace(40000, 0, Microseconds(15000), kCap);
REQUIRE_FALSE(d.reanchor);
REQUIRE(d.sleep == Microseconds(25000));
}
TEST_CASE("ComputeRealtimePace: non-zero anchor only the delta matters") {
// Anchor at 1s, packet at 1.040s, 10ms elapsed -> 30ms remaining.
RealtimePaceDecision d = ComputeRealtimePace(1040000, 1000000, Microseconds(10000), kCap);
REQUIRE_FALSE(d.reanchor);
REQUIRE(d.sleep == Microseconds(30000));
}
TEST_CASE("ComputeRealtimePace: behind schedule delivers immediately without re-anchoring") {
// Only 40ms into the stream but 100ms of wall-clock has passed: we are behind,
// so deliver now (no sleep) and keep the anchor so we can catch back up.
RealtimePaceDecision d = ComputeRealtimePace(40000, 0, Microseconds(100000), kCap);
REQUIRE_FALSE(d.reanchor);
REQUIRE(d.sleep == Microseconds(0));
}
TEST_CASE("ComputeRealtimePace: exactly on schedule does not sleep") {
RealtimePaceDecision d = ComputeRealtimePace(40000, 0, Microseconds(40000), kCap);
REQUIRE_FALSE(d.reanchor);
REQUIRE(d.sleep == Microseconds(0));
}
TEST_CASE("ComputeRealtimePace: backward timestamp re-anchors instead of sleeping") {
// A timestamp before the anchor (discontinuity/reset) must never produce a
// negative sleep; it re-anchors so pacing restarts from the new position.
RealtimePaceDecision d = ComputeRealtimePace(500000, 1000000, Microseconds(0), kCap);
REQUIRE(d.reanchor);
REQUIRE(d.sleep == Microseconds(0));
}
TEST_CASE("ComputeRealtimePace: gap beyond the cap re-anchors instead of stalling") {
// 30s ahead of schedule with a 10s cap is treated as a discontinuity, not a
// genuine 30s frame interval, so we re-anchor rather than sleep 30s.
RealtimePaceDecision d =
ComputeRealtimePace(30 * 1000000LL, 0, Microseconds(0), kCap);
REQUIRE(d.reanchor);
REQUIRE(d.sleep == Microseconds(0));
}
TEST_CASE("ComputeRealtimePace: a delay right at the cap still sleeps") {
// Boundary: delay == cap is allowed (only delays strictly greater re-anchor).
RealtimePaceDecision d = ComputeRealtimePace(10 * 1000000LL, 0, Microseconds(0), kCap);
REQUIRE_FALSE(d.reanchor);
REQUIRE(d.sleep == kCap);
}