diff --git a/AGENTS.md b/AGENTS.md index 54b09aa..16a84bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,6 +57,9 @@ Directory auto-created at startup. ## Video recordings - Recordings are written to `SHM_DIR` as `batch_pending_*.mp4`, confirmed when ayam detected, and moved via `fork()` (non-blocking) to `OUTPUT_DIR//batch___.mp4`. +- **Live sources**: recording speculates (SPECULATING) and is discarded after `RECORD_DELAY_SECONDS` if no ayam confirms; only CONFIRMED/STOPPING recordings are saved. +- **File sources** (`file_source_` flag): recording opens at frame 0, is **never discarded** by the SPECULATING timer, and any active recording is saved on batch close and on shutdown — so the output's first frame matches the source mp4's frame 0 (no `RECORD_DELAY` offset). +- `feed_frame` is called **every frame** (outside the detections block), so recordings are continuous with no gaps on detection-less frames. - The day folder uses the **batch counting date** (today before `DAILY_CUTOFF_TIME`, tomorrow after), forwarded from `BatchStore` through the batch-closed callback — matching the SQLite `counting_date`. - Retention cleanup compares day folders with `std::localtime` to match folder naming. - H.264 encode quality is set via `RECORD_CRF` (lower = better, default unset) and `RECORD_PRESET` — `start_recording()` writes `OPENCV_FFMPEG_WRITE_OPTIONS` (`crf;|preset;

`) before `writer_.open()`. diff --git a/SPEC.md b/SPEC.md index 82281a5..de25df3 100644 --- a/SPEC.md +++ b/SPEC.md @@ -49,10 +49,11 @@ V9: live_stream_enabled → parent dir of LIVE_STREAM_FRAME_PATH created @ star V10: stale track (> TRACKED_PRUNE_SEC unseen) → prune from external tracking maps V11: all dirs (output_dir, shm_dir, live_stream parent) created @ startup V12: SHM_DIR/.reset → BatchStore::reset() — end active batch, clear memory, delete state_file, write blank live JPEG -V13: video recording: motion → SPECULATING (≤RECORD_DELAY_SECONDS), ayam → CONFIRMED → talenan/batch-end → STOPPING (+RECORD_END_DELAY) → move to OUTPUT_DIR// via fork +V13: video recording: motion → SPECULATING (≤RECORD_DELAY_SECONDS, live only; file sources never discard) → CONFIRMED → talenan/batch-end → STOPPING (+RECORD_END_DELAY) → move to OUTPUT_DIR// via fork. feed_frame called every frame (no detection gaps) V14: video retention: day-dirs > RECORD_RETENTION_DAYS old → removed @ startup & @ each CUTOFF_TIME (localtime cutoff matches folder names) V15: RECORD_CRF (lower = better, -1 unset) & RECORD_PRESET → OPENCV_FFMPEG_WRITE_OPTIONS before writer_.open() (crf;|preset;

) V16: pause_on_first_frame (env/--pause-on-first-frame, file source only; live ignored w/ warn): process frame 0 → idle-loop, re-write annotated frame to LIVE_STREAM_FRAME_PATH, wait for SHM_DIR/.continue → delete → resume +V17: file sources (non-live) record from frame 0: SPECULATING never discarded, any active recording saved on batch close and shutdown → output first frame = source frame 0 (no RECORD_DELAY offset) ``` ## §T — Tasks @@ -74,7 +75,7 @@ T12|x|CsvLogger for crossing events|I.file T13|x|Live JPEG snapshot to shared memory|V9 T14|x|systemd service with ProtectSystem + ReadWritePaths|I.cmd T15|x|Graceful shutdown: SIGTERM → close batch → save state|I.sig -T16|x|Video per-batch recording: motion-triggered → speculating in shm, confirmed when ayam detected ≤ RECORD_DELAY_SECONDS, stopped RECORD_END_DELAY after batch close, moved to OUTPUT_DIR// via fork, retention cleanup at startup & cutoff, H.264 quality via RECORD_CRF/RECORD_PRESET (OPENCV_FFMPEG_WRITE_OPTIONS). Unconfirmed recordings deleted.|V13,V14,V15 +T16|x|Video per-batch recording: motion-triggered → speculating in shm (live only, discarded after RECORD_DELAY_SECONDS unconfirmed), confirmed when ayam detected, stopped RECORD_END_DELAY after batch close, moved to OUTPUT_DIR// via fork, retention cleanup at startup & cutoff, H.264 quality via RECORD_CRF/RECORD_PRESET (OPENCV_FFMPEG_WRITE_OPTIONS). Every frame fed (no detection gaps). File sources: record from frame 0, never discard, save on close & shutdown.|V13,V14,V15,V17 T17|~?|Python DEBUG_TRACKING equivalent (per-frame score/track dump)|- T18|x|CLI --source/-s/--source= override of SOURCE (rtsp|http|local mp4; file = single pass)|I.cmd T19|x|Pause-on-first-frame for file source: process frame 0, hold annotated frame as live view, wait for SHM_DIR/.continue, resume. Env PAUSE_ON_FIRST_FRAME + --pause-on-first-frame; stale .continue cleared @ startup; live source ignored w/ warning.|V16 diff --git a/build/bytetrack-counter b/build/bytetrack-counter index aad6932..0b8a165 100755 Binary files a/build/bytetrack-counter and b/build/bytetrack-counter differ diff --git a/build/version.hpp b/build/version.hpp index b036263..2dd6e82 100644 --- a/build/version.hpp +++ b/build/version.hpp @@ -4,11 +4,11 @@ namespace version { -const std::string GIT_COMMIT = "139cff74"; +const std::string GIT_COMMIT = "32921ea3"; const std::string GIT_BRANCH = "main"; const std::string GIT_TAG = ""; const std::string BUILD_DATE = "2026-08-10"; -const std::string BUILD_TIME = "10:11:04"; +const std::string BUILD_TIME = "10:51:48"; const int BUILD_YEAR = 2026; const bool BUILD_RKNN = 1; diff --git a/src/main.cpp b/src/main.cpp index 1f493e4..d35319b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -282,7 +282,8 @@ int main(int argc, char* argv[]) { g_config.record_end_delay_seconds, g_config.record_retention_days, g_config.record_crf, - g_config.record_preset); + g_config.record_preset, + !is_live); auto* vw = video_writer.get(); store.set_batch_closed_callback([vw](int bn, const std::string& date) { vw->on_batch_closed(bn, date); }); } @@ -377,6 +378,8 @@ int main(int argc, char* argv[]) { if (video_writer) video_writer->note_motion(); } + if (video_writer) video_writer->feed_frame(frame); + std::vector detections; float inf_ms = 0.0f; if (!skip_inference) { @@ -549,8 +552,6 @@ int main(int argc, char* argv[]) { ayam_tracked[tid] = {cx, mono}; } - if (video_writer) video_writer->feed_frame(frame); - // Draw talenan for (int di = 0; di < nt; ++di) { auto it = talenan_tr.det_to_track.find(di); diff --git a/src/video_writer.cpp b/src/video_writer.cpp index 37fe326..f7ba79f 100644 --- a/src/video_writer.cpp +++ b/src/video_writer.cpp @@ -18,11 +18,13 @@ VideoBatchWriter::VideoBatchWriter(const std::string& shm_dir, float end_delay_sec, int retention_days, int crf, - const std::string& preset) + const std::string& preset, + bool file_source) : shm_dir_(shm_dir), output_dir_(output_dir), w_(w), h_(h), fps_(fps), delay_sec_(delay_sec), end_delay_sec_(end_delay_sec), - retention_days_(retention_days), crf_(crf), preset_(preset) + retention_days_(retention_days), crf_(crf), preset_(preset), + file_source_(file_source) { ensure_dir(shm_dir_); ensure_dir(output_dir_); @@ -96,7 +98,8 @@ void VideoBatchWriter::note_ayam_detection() { void VideoBatchWriter::on_batch_closed(int batch_number, const std::string& counting_date) { current_batch_ = batch_number; current_date_ = counting_date; - if (state_ == CONFIRMED) { + bool save = file_source_ ? (state_ != IDLE) : (state_ == CONFIRMED); + if (save) { state_ = STOPPING; state_start_ = std::chrono::steady_clock::now(); } @@ -105,7 +108,7 @@ void VideoBatchWriter::on_batch_closed(int batch_number, const std::string& coun void VideoBatchWriter::feed_frame(const cv::Mat& raw) { auto now = std::chrono::steady_clock::now(); - if (state_ == SPECULATING) { + if (state_ == SPECULATING && !file_source_) { float elapsed = std::chrono::duration(now - state_start_).count(); if (elapsed >= delay_sec_) { stop_and_delete(); @@ -220,7 +223,9 @@ void VideoBatchWriter::shutdown() { std::string src = current_path_; current_path_.clear(); - if (prev_state == CONFIRMED || prev_state == STOPPING) { + bool keep = file_source_ ? (prev_state != IDLE) + : (prev_state == CONFIRMED || prev_state == STOPPING); + if (keep) { std::string day_dir = output_dir_ + "/" + (current_date_.empty() ? today_str() : current_date_); ensure_dir(day_dir); std::string dst = day_dir + "/" + build_mp4_name(current_batch_, current_date_); diff --git a/src/video_writer.hpp b/src/video_writer.hpp index 37463ef..0c91bbf 100644 --- a/src/video_writer.hpp +++ b/src/video_writer.hpp @@ -12,7 +12,8 @@ public: float end_delay_sec, int retention_days, int crf, - const std::string& preset); + const std::string& preset, + bool file_source); ~VideoBatchWriter(); @@ -39,6 +40,7 @@ private: int retention_days_; int crf_; std::string preset_; + bool file_source_ = false; State state_ = IDLE; std::chrono::steady_clock::time_point state_start_;