diff --git a/AGENTS.md b/AGENTS.md index be1455e..01b8e63 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,3 +53,17 @@ touch /dev/shm/bytetrack-counter/.reset - `.reset` trigger file 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_N_HHMMSS.mp4`. +- 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. + +## Source override + +```bash +bytetrack-counter --source /tmp/test.mp4 # local file (single pass) +bytetrack-counter --source rtsp://cam:554/x # live stream +bytetrack-counter -s path # or --source=path +``` diff --git a/SPEC.md b/SPEC.md index b4800a4..d6b3de9 100644 --- a/SPEC.md +++ b/SPEC.md @@ -23,13 +23,13 @@ env: .env KEY=VALUE file — all config per config.env.example env: SHM_DIR → shared memory directory (/dev/shm/bytetrack-counter) env: LIVE_STREAM_FRAME_PATH → JPEG snapshot path env: RECORD_VIDEO, RECORD_DELAY_SECONDS, RECORD_END_DELAY, RECORD_RETENTION_DAYS -cmd: bytetrack-counter +cmd: bytetrack-counter [--source ] db: SQLite batches(id,counting_date,batch_number,camera_name,object_label,count,start_time,end_time) db: SQLite daily_summaries(id,counting_date,camera_name,object_label,total_count,total_batches) file: JSON state — batch_number,count,start_time,last_detection_time,counted_event_ids[] file: CSV crossing log — batch,frame,timestamp,chicken_id file: SHM_DIR/.reset → trigger full state reset -file: SHM_DIR/batch_N_HHMMSS.mp4 → per-batch video recording (moved to OUTPUT_DIR/YYYY-MM-DD/) +file: SHM_DIR/batch_N_HHMMSS.mp4 → per-batch video recording (moved to OUTPUT_DIR//) sig: SIGTERM|SIGINT → shutdown_requested → graceful exit ``` @@ -48,8 +48,8 @@ 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/YYYY-MM-DD/ -V14: video retention: day-dirs > RECORD_RETENTION_DAYS old → removed @ startup & @ each CUTOFF_TIME +V13: video recording: motion → SPECULATING (≤RECORD_DELAY_SECONDS), ayam → CONFIRMED → talenan/batch-end → STOPPING (+RECORD_END_DELAY) → move to OUTPUT_DIR// via fork +V14: video retention: day-dirs > RECORD_RETENTION_DAYS old → removed @ startup & @ each CUTOFF_TIME (localtime cutoff matches folder names) ``` ## §T — Tasks @@ -71,8 +71,9 @@ 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/YYYY-MM-DD/ via fork, retention cleanup at startup & cutoff. Unconfirmed recordings deleted.|V13,V14 +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. Unconfirmed recordings deleted.|V13,V14 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 ``` ## §B — Bugs diff --git a/build/bytetrack-counter b/build/bytetrack-counter index d35c911..5d9f9c5 100755 Binary files a/build/bytetrack-counter and b/build/bytetrack-counter differ diff --git a/build/version.hpp b/build/version.hpp index 6e66457..b757c10 100644 --- a/build/version.hpp +++ b/build/version.hpp @@ -4,11 +4,11 @@ namespace version { -const std::string GIT_COMMIT = "eefff489"; +const std::string GIT_COMMIT = "a94c3dff"; const std::string GIT_BRANCH = "main"; const std::string GIT_TAG = ""; const std::string BUILD_DATE = "2026-08-04"; -const std::string BUILD_TIME = "23:35:10"; +const std::string BUILD_TIME = "23:55:47"; const int BUILD_YEAR = 2026; const bool BUILD_RKNN = 1; diff --git a/src/batch_store.cpp b/src/batch_store.cpp index 314d56b..c73ba9d 100644 --- a/src/batch_store.cpp +++ b/src/batch_store.cpp @@ -328,12 +328,13 @@ bool BatchStore::end_batch_locked(const std::string& closed_by) { int count = state["count"].get(); int batch_number = state["batch_number"].get(); + std::string counting_date = state.value("counting_date", ""); if (count < min_object_per_batch_ || duration_sec < min_duration_per_batch_) { current_state_ = nullptr; save_state(); batch_timer_running_ = false; - if (batch_closed_cb_) batch_closed_cb_(batch_number); + if (batch_closed_cb_) batch_closed_cb_(batch_number, counting_date); return false; } @@ -348,7 +349,7 @@ bool BatchStore::end_batch_locked(const std::string& closed_by) { current_state_ = nullptr; save_state(); batch_timer_running_ = false; - if (batch_closed_cb_) batch_closed_cb_(batch_number); + if (batch_closed_cb_) batch_closed_cb_(batch_number, counting_date); return true; } @@ -409,7 +410,7 @@ int BatchStore::display_total() { return get_closed_total_for_day(get_counting_date()) + current_batch_count(); } -void BatchStore::set_batch_closed_callback(std::function cb) { +void BatchStore::set_batch_closed_callback(std::function cb) { batch_closed_cb_ = std::move(cb); } diff --git a/src/batch_store.hpp b/src/batch_store.hpp index a6eefc8..7daca73 100644 --- a/src/batch_store.hpp +++ b/src/batch_store.hpp @@ -31,7 +31,7 @@ public: int current_batch_count() const; int display_total(); - void set_batch_closed_callback(std::function cb); + void set_batch_closed_callback(std::function cb); void reset(); bool check_cutoff_reset(); @@ -70,7 +70,7 @@ private: int min_duration_per_batch_; int carry_ids_ = 50; std::function logger_; - std::function batch_closed_cb_; + std::function batch_closed_cb_; sqlite3* db_ = nullptr; std::mutex state_mutex_; diff --git a/src/main.cpp b/src/main.cpp index b0e98f0..d145db0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -266,7 +266,7 @@ int main(int argc, char* argv[]) { g_config.record_end_delay_seconds, g_config.record_retention_days); auto* vw = video_writer.get(); - store.set_batch_closed_callback([vw](int bn) { vw->on_batch_closed(bn); }); + store.set_batch_closed_callback([vw](int bn, const std::string& date) { vw->on_batch_closed(bn, date); }); } int reconnect_count = 0; @@ -617,8 +617,8 @@ int main(int argc, char* argv[]) { } cap.release(); - if (video_writer) video_writer->shutdown(); store.shutdown(); + if (video_writer) video_writer->shutdown(); std::cout << "\n=== Batch Summary (SQLite) ===" << std::endl; std::cout << "Database: " << g_config.db_path << std::endl; diff --git a/src/video_writer.cpp b/src/video_writer.cpp index 745e48e..efb0467 100644 --- a/src/video_writer.cpp +++ b/src/video_writer.cpp @@ -41,7 +41,7 @@ void VideoBatchWriter::cleanup_retention() { auto cutoff = std::chrono::system_clock::now() - std::chrono::hours(retention_days_ * 24); auto cutoff_t = std::chrono::system_clock::to_time_t(cutoff); std::ostringstream cutoff_ss; - cutoff_ss << std::put_time(std::gmtime(&cutoff_t), "%Y-%m-%d"); + cutoff_ss << std::put_time(std::localtime(&cutoff_t), "%Y-%m-%d"); std::string cutoff_str = cutoff_ss.str(); for (const auto& entry : fs::directory_iterator(output_dir_)) { @@ -86,8 +86,9 @@ void VideoBatchWriter::note_ayam_detection() { } } -void VideoBatchWriter::on_batch_closed(int batch_number) { +void VideoBatchWriter::on_batch_closed(int batch_number, const std::string& counting_date) { current_batch_ = batch_number; + current_date_ = counting_date; if (state_ == CONFIRMED) { state_ = STOPPING; state_start_ = std::chrono::steady_clock::now(); @@ -175,7 +176,7 @@ void VideoBatchWriter::stop_and_move(int batch_number) { std::string src = current_path_; current_path_.clear(); - std::string day_dir = output_dir_ + "/" + today_str(); + std::string day_dir = output_dir_ + "/" + (current_date_.empty() ? today_str() : current_date_); ensure_dir(day_dir); std::ostringstream name; @@ -205,7 +206,7 @@ void VideoBatchWriter::shutdown() { current_path_.clear(); if (prev_state == CONFIRMED || prev_state == STOPPING) { - std::string day_dir = output_dir_ + "/" + today_str(); + std::string day_dir = output_dir_ + "/" + (current_date_.empty() ? today_str() : current_date_); ensure_dir(day_dir); std::string dst = day_dir + "/" + fs::path(src).filename().string(); diff --git a/src/video_writer.hpp b/src/video_writer.hpp index 5f34d7f..6d67196 100644 --- a/src/video_writer.hpp +++ b/src/video_writer.hpp @@ -16,7 +16,7 @@ public: void note_motion(); void note_ayam_detection(); - void on_batch_closed(int batch_number); + void on_batch_closed(int batch_number, const std::string& counting_date); void feed_frame(const cv::Mat& raw); void cleanup_retention(); void shutdown(); @@ -41,4 +41,5 @@ private: cv::VideoWriter writer_; std::string current_path_; int current_batch_ = 0; + std::string current_date_; };