#include "video_writer.hpp" #include #include #include #include #include #include #include #include namespace fs = std::filesystem; VideoBatchWriter::VideoBatchWriter(const std::string& shm_dir, const std::string& output_dir, int w, int h, int fps, float delay_sec, float end_delay_sec, int retention_days) : 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) { ensure_dir(shm_dir_); ensure_dir(output_dir_); for (const auto& entry : fs::directory_iterator(shm_dir_)) { if (entry.path().extension() == ".mp4") { fs::remove(entry.path()); } } cleanup_retention(); } VideoBatchWriter::~VideoBatchWriter() { shutdown(); } 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::localtime(&cutoff_t), "%Y-%m-%d"); std::string cutoff_str = cutoff_ss.str(); for (const auto& entry : fs::directory_iterator(output_dir_)) { if (!entry.is_directory()) continue; std::string dirname = entry.path().filename().string(); if (dirname < cutoff_str) { std::error_code ec; fs::remove_all(entry.path(), ec); } } } void VideoBatchWriter::ensure_dir(const std::string& dir) { fs::create_directories(dir); } static std::string today_str() { auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); std::ostringstream ss; ss << std::put_time(std::localtime(&t), "%Y-%m-%d"); return ss.str(); } static std::string timestamp_str() { auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); std::ostringstream ss; ss << std::put_time(std::localtime(&t), "%H%M%S"); return ss.str(); } void VideoBatchWriter::note_motion() { if (state_ == IDLE) { start_recording(); state_ = SPECULATING; state_start_ = std::chrono::steady_clock::now(); } } void VideoBatchWriter::note_ayam_detection() { if (state_ == SPECULATING) { state_ = CONFIRMED; } } 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(); } } void VideoBatchWriter::feed_frame(const cv::Mat& raw) { auto now = std::chrono::steady_clock::now(); if (state_ == SPECULATING) { float elapsed = std::chrono::duration(now - state_start_).count(); if (elapsed >= delay_sec_) { stop_and_delete(); state_ = IDLE; return; } } if (state_ == STOPPING) { float elapsed = std::chrono::duration(now - state_start_).count(); if (elapsed >= end_delay_sec_) { stop_and_move(current_batch_); state_ = IDLE; return; } } if (state_ != IDLE && writer_.isOpened()) { writer_.write(raw); } } void VideoBatchWriter::start_recording() { if (writer_.isOpened()) writer_.release(); auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); std::ostringstream name; name << "batch_pending_" << std::put_time(std::localtime(&t), "%Y%m%d_%H%M%S") << ".mp4"; current_path_ = shm_dir_ + "/" + name.str(); writer_.open(current_path_, cv::VideoWriter::fourcc('a','v','c','1'), fps_, cv::Size(w_, h_)); if (writer_.isOpened()) { std::cout << "[RECORD] Speculating: " << current_path_ << std::endl; } else { std::cerr << "[RECORD] Failed to open: " << current_path_ << std::endl; } } static void copy_and_clean(const std::string& src, const std::string& dst) { if (src.empty() || dst.empty()) return; { std::ifstream ifs(src, std::ios::binary); if (ifs) { std::ofstream ofs(dst, std::ios::binary); ofs << ifs.rdbuf(); ofs.flush(); } } std::error_code ec; fs::remove(src, ec); } void VideoBatchWriter::stop_and_delete() { if (writer_.isOpened()) { writer_.release(); } if (!current_path_.empty()) { std::cout << "[RECORD] Discarding: " << current_path_ << std::endl; std::error_code ec; fs::remove(current_path_, ec); current_path_.clear(); } } void VideoBatchWriter::stop_and_move(int batch_number) { if (writer_.isOpened()) { writer_.release(); std::cout << "[RECORD] Stopped: " << current_path_ << std::endl; } std::string src = current_path_; current_path_.clear(); std::string day_dir = output_dir_ + "/" + (current_date_.empty() ? today_str() : current_date_); ensure_dir(day_dir); std::ostringstream name; name << "batch_" << batch_number << "_" << timestamp_str() << ".mp4"; std::string dst = day_dir + "/" + name.str(); pid_t pid = fork(); if (pid == 0) { close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); copy_and_clean(src, dst); _exit(0); } } void VideoBatchWriter::shutdown() { if (writer_.isOpened()) { writer_.release(); } auto prev_state = state_; state_ = IDLE; if (!current_path_.empty()) { std::string src = current_path_; current_path_.clear(); if (prev_state == CONFIRMED || prev_state == STOPPING) { 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(); pid_t pid = fork(); if (pid == 0) { close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); copy_and_clean(src, dst); _exit(0); } } else { std::error_code ec; fs::remove(src, ec); } } }