#include "record_manager.hpp" #include #include #include #include #include #include #include "logger.hpp" namespace { std::optional getEarliestRecordMsInternal( const std::unordered_map>& index_) { int64_t earliest = -1; for (auto& kv : index_) { for (auto& f : kv.second) { if (earliest < 0 || f.start_ms < earliest) earliest = f.start_ms; } } if (earliest < 0) return std::nullopt; return earliest; } } // namespace std::shared_ptr g_record_manager = nullptr; extern std::atomic g_running; namespace fs = std::filesystem; // // 构造函数:自动加载 SRS 配置 // RecordManager::RecordManager(const std::string& srs_record_cfg_path) : srs_record_cfg_path_(srs_record_cfg_path) { LOG_INFO("[RecordManager] Loading SRS config: " + srs_record_cfg_path_); bool ok = loadSrsConfig(); if (!ok) { LOG_ERROR("[RecordManager] Failed to load SRS config."); return; } LOG_INFO("[RecordManager] SRS config loaded successfully."); LOG_INFO("record_dir = " + record_dir_); LOG_INFO("dvr_duration_sec = " + std::to_string(dvr_duration_sec_)); LOG_INFO("http_port = " + std::to_string(http_port_)); // 自动根据 dvr_duration 启动扫描线程 startAutoScan(dvr_duration_sec_); } RecordManager::~RecordManager() { stopAutoScan(); // 线程退出 } // // 解析 SRS DVR 配置,提取 record_dir_ 与 dvr_duration_sec_ // bool RecordManager::loadSrsConfig() { std::ifstream ifs(srs_record_cfg_path_); if (!ifs.is_open()) { LOG_ERROR("[RecordManager] Cannot open SRS config file: " + srs_record_cfg_path_); return false; } std::string line; std::string dvr_path; bool in_http_server = false; while (std::getline(ifs, line)) { line.erase(0, line.find_first_not_of(" \t")); if (line.empty() || line[0] == '#') continue; // ---------- 进入 http_server 块 ---------- if (line.find("http_server") != std::string::npos && line.find("{") != std::string::npos) { in_http_server = true; continue; } if (in_http_server && line == "}") { in_http_server = false; continue; } if (in_http_server) { if (line.rfind("listen", 0) == 0) { int port = 0; if (sscanf(line.c_str(), "listen %d;", &port) == 1) { http_port_ = port; } } } // 解析 dvr_path if (line.find("dvr_path") != std::string::npos) { auto pos = line.find('/'); auto semicolon = line.find(';'); if (pos != std::string::npos && semicolon != std::string::npos) dvr_path = line.substr(pos, semicolon - pos); } // 解析 dvr_duration else if (line.find("dvr_duration") != std::string::npos) { int sec = 0; if (sscanf(line.c_str(), "dvr_duration %d", &sec) == 1) dvr_duration_sec_ = sec; } } if (dvr_path.empty()) { LOG_ERROR("[RecordManager] dvr_path not found in config."); return false; } // /sata/record/[stream]/[2006]-[01]-[02]/... auto pos = dvr_path.find("[stream]"); if (pos != std::string::npos) record_dir_ = dvr_path.substr(0, pos); else { LOG_ERROR("[RecordManager] Cannot extract record_dir from dvr_path: " + dvr_path); return false; } return true; } void RecordManager::removeExpiredDays() { namespace fs = std::filesystem; auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); for (auto& streamEntry : fs::directory_iterator(record_dir_)) { if (!streamEntry.is_directory()) continue; for (auto& dayEntry : fs::directory_iterator(streamEntry)) { if (!dayEntry.is_directory()) continue; std::string dayName = dayEntry.path().filename().string(); // 2025-11-14 std::tm tm{}; if (sscanf(dayName.c_str(), "%d-%d-%d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday) != 3) continue; tm.tm_year -= 1900; tm.tm_mon -= 1; time_t dayTime = mktime(&tm); double daysOld = difftime(now, dayTime) / 86400.0; if (daysOld > retention_days_) { LOG_WARN("[RecordManager] Removing expired day: " + dayEntry.path().string()); fs::remove_all(dayEntry.path()); } } } } void RecordManager::removeOldestHoursUntilSafe() { namespace fs = std::filesystem; while (true) { // 1) 检查磁盘 struct statvfs vfs{}; if (statvfs(record_dir_.c_str(), &vfs) != 0) return; double used = 1.0 - (double)vfs.f_bavail / (double)vfs.f_blocks; if (used < usage_threshold_) break; // 2) 找全盘最旧的 day fs::path oldestDay; time_t oldestTime = 0; for (auto& streamDir : fs::directory_iterator(record_dir_)) { if (!streamDir.is_directory()) continue; for (auto& dayDir : fs::directory_iterator(streamDir)) { if (!dayDir.is_directory()) continue; std::string dayName = dayDir.path().filename().string(); std::tm tm{}; if (sscanf(dayName.c_str(), "%d-%d-%d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday) != 3) continue; tm.tm_year -= 1900; tm.tm_mon -= 1; time_t t = mktime(&tm); if (oldestDay.empty() || t < oldestTime) { oldestTime = t; oldestDay = dayDir.path(); } } } if (oldestDay.empty()) { LOG_WARN("[RecordManager] No day folder found for hour cleanup."); break; } // ---- 找该 day 下最旧的 hour ---- fs::path oldestHour; int oldestHourValue = -1; for (auto& hourDir : fs::directory_iterator(oldestDay)) { if (!hourDir.is_directory()) continue; std::string hourName = hourDir.path().filename().string(); int h = -1; if (sscanf(hourName.c_str(), "%d", &h) != 1) continue; if (oldestHourValue == -1 || h < oldestHourValue) { oldestHourValue = h; oldestHour = hourDir.path(); } } if (oldestHour.empty()) { LOG_WARN("[RecordManager] Day empty, remove whole day: " + oldestDay.string()); fs::remove_all(oldestDay); continue; } // ---- 删除最旧 hour ---- LOG_WARN("[RecordManager] Removing oldest hour: " + oldestHour.string()); fs::remove_all(oldestHour); // 如果此 day 已空 → 删除 day 目录 if (fs::is_empty(oldestDay)) { LOG_WARN("[RecordManager] Removing empty day: " + oldestDay.string()); fs::remove_all(oldestDay); } } } void RecordManager::cleanupStorage() { struct statvfs vfs{}; if (statvfs(record_dir_.c_str(), &vfs) != 0) { LOG_ERROR("[RecordManager] statvfs failed for " + record_dir_); return; } double used = 1.0 - (double)vfs.f_bavail / (double)vfs.f_blocks; // 1) 删除超期天 removeExpiredDays(); // 重新检查磁盘 if (statvfs(record_dir_.c_str(), &vfs) == 0) { used = 1.0 - (double)vfs.f_bavail / (double)vfs.f_blocks; } // 2) 使用率仍超 → 按小时清理 if (used >= usage_threshold_) { removeOldestHoursUntilSafe(); } } void RecordManager::startAutoScan(int interval_sec) { scan_interval_sec_ = interval_sec; if (running_) return; running_.store(true); scan_thread_ = std::thread( [this]() { LOG_INFO("[RecordManager] Auto-scan thread started, interval = " + std::to_string(scan_interval_sec_) + " seconds."); while (running_.load() && g_running.load()) { auto t0 = std::chrono::steady_clock::now(); this->scanAll(); // ======= 新增:打印最早录像时间 + 磁盘使用率 ======= std::optional earliestOpt; { std::lock_guard lock(index_mutex_); earliestOpt = getEarliestRecordMsInternal(index_); } if (earliestOpt.has_value()) { std::string tReadable = RecordManager::toReadable(earliestOpt.value()); struct statvfs vfs{}; double usedPct = -1; if (statvfs(record_dir_.c_str(), &vfs) == 0) { double used = 1.0 - (double)vfs.f_bavail / (double)vfs.f_blocks; usedPct = used * 100.0; } LOG_INFO("[RecordManager] Earliest record time=" + tReadable + ", disk used=" + std::to_string(usedPct) + "%"); } else { LOG_INFO("[RecordManager] No video files found."); } // ======= 再调用清理 ======= this->cleanupStorage(); LOG_INFO("[RecordManager] scanAll() completed."); // 休眠剩余时间(支持快速退出) auto t1 = std::chrono::steady_clock::now(); auto used_ms = std::chrono::duration_cast(t1 - t0).count(); int sleep_ms = scan_interval_sec_ * 1000 - used_ms; if (sleep_ms < 100) sleep_ms = 100; int remain = sleep_ms; while (remain > 0 && running_.load() && g_running.load()) { int chunk = std::min(remain, 200); std::this_thread::sleep_for(std::chrono::milliseconds(chunk)); remain -= chunk; } } LOG_INFO("[RecordManager] Auto-scan thread stopped."); }); } void RecordManager::stopAutoScan() { if (!running_) return; running_ = false; if (scan_thread_.joinable()) scan_thread_.join(); } // // 扫描所有文件 // void RecordManager::scanAll() { std::lock_guard lock(index_mutex_); index_.clear(); if (!fs::exists(record_dir_)) { LOG_WARN("[RecordManager] record_dir not exist: " + record_dir_); return; } for (auto const& entry : fs::recursive_directory_iterator(record_dir_)) { if (!entry.is_regular_file()) continue; auto p = entry.path(); if (p.extension() != ".mp4") continue; auto info = parseFile(p); if (info.start_ms <= 0) continue; index_[info.stream].push_back(info); } for (auto& kv : index_) { sortStream(kv.first); } LOG_INFO("[RecordManager] scanAll completed. streams=" + std::to_string(index_.size())); } // // 排序函数:按 start_ms 升序 // void RecordManager::sortStream(const std::string& stream) { auto& files = index_[stream]; std::sort(files.begin(), files.end(), [](const RecordFileInfo& a, const RecordFileInfo& b) { return a.start_ms < b.start_ms; }); } // // 解析路径得到文件时间戳 // RecordFileInfo RecordManager::parseFile(const fs::path& p) { RecordFileInfo info; info.path = p.string(); try { auto filename = p.filename().string(); // 10-23-17.mp4 auto date_dir = p.parent_path().parent_path().filename().string(); // yyyy-mm-dd auto stream_dir = p.parent_path().parent_path().parent_path().filename().string(); info.stream = stream_dir; int y = 0, mon = 0, d = 0, h = 0, m = 0, s = 0; sscanf(date_dir.c_str(), "%d-%d-%d", &y, &mon, &d); sscanf(filename.c_str(), "%d-%d-%d", &h, &m, &s); std::tm tm{}; tm.tm_year = y - 1900; tm.tm_mon = mon - 1; tm.tm_mday = d; tm.tm_hour = h; tm.tm_min = m; tm.tm_sec = s; int64_t start = std::mktime(&tm) * 1000LL; info.start_ms = start; // 读取 mp4 时长 MP4FileHandle hFile = MP4Read(info.path.c_str()); if (hFile != MP4_INVALID_FILE_HANDLE) { double durSec = MP4GetDuration(hFile) / (double)MP4GetTimeScale(hFile); MP4Close(hFile); info.end_ms = start + (int64_t)(durSec * 1000.0); } else { info.end_ms = start + 60000; // fallback } } catch (...) { info.start_ms = -1; info.end_ms = -1; } return info; } // // 查询录像段 // std::vector RecordManager::querySegments(const std::string& stream, int64_t start_ms, int64_t end_ms) { std::lock_guard lock(index_mutex_); std::vector result; if (start_ms <= 0 || end_ms <= 0 || start_ms >= end_ms) return result; if (!index_.count(stream)) return result; const auto& files = index_[stream]; std::vector hits; for (const auto& f : files) { bool overlap = !(f.end_ms <= start_ms || f.start_ms >= end_ms); if (overlap) hits.push_back(f); } if (hits.empty()) return result; std::sort(hits.begin(), hits.end(), [](auto& a, auto& b) { return a.start_ms < b.start_ms; }); int idx = 1; RecordSegment seg{}; seg.index = idx; seg.start_ms = hits[0].start_ms; seg.end_ms = hits[0].end_ms; seg.files.push_back(hits[0]); for (size_t i = 1; i < hits.size(); ++i) { auto& prev = hits[i - 1]; auto& cur = hits[i]; bool continuous = (cur.start_ms - prev.end_ms) <= 2000; if (continuous) { seg.files.push_back(cur); seg.end_ms = cur.end_ms; } else { seg.segment_id = stream + "_" + std::to_string(seg.start_ms) + "_" + std::to_string(seg.end_ms); result.push_back(seg); idx++; seg = RecordSegment{}; seg.index = idx; seg.start_ms = cur.start_ms; seg.end_ms = cur.end_ms; seg.files.push_back(cur); } } seg.segment_id = stream + "_" + std::to_string(seg.start_ms) + "_" + std::to_string(seg.end_ms); result.push_back(seg); return result; } // // 根据 segmentId 获取 segment // RecordSegment RecordManager::getSegment(const std::string& segmentId) { std::lock_guard lock(index_mutex_); // 1) 找最后一个 "_" size_t pos_last = segmentId.rfind('_'); if (pos_last == std::string::npos) return {}; // 2) 再往前找一个 "_" size_t pos_mid = segmentId.rfind('_', pos_last - 1); if (pos_mid == std::string::npos) return {}; // 3) 三段拆分 std::string stream = segmentId.substr(0, pos_mid); std::string startStr = segmentId.substr(pos_mid + 1, pos_last - pos_mid - 1); std::string endStr = segmentId.substr(pos_last + 1); int64_t start_ms = 0; int64_t end_ms = 0; try { start_ms = std::stoll(startStr); end_ms = std::stoll(endStr); } catch (...) { LOG_ERROR("[RecordManager] segmentId parse failed: " + segmentId); return {}; } // 确认 stream 在 index_ 里 if (!index_.count(stream)) { LOG_WARN("[RecordManager] stream not exist in index: " + stream); return {}; } // 4) 根据时间区间收集属于该 segment 的所有文件 const auto& files = index_[stream]; RecordSegment seg; seg.segment_id = segmentId; seg.index = 1; seg.start_ms = start_ms; seg.end_ms = end_ms; for (const auto& f : files) { // 与 segment 时间区间有重叠的文件都属于这个 segment bool overlap = !(f.end_ms <= start_ms || f.start_ms >= end_ms); if (overlap) seg.files.push_back(f); } if (seg.files.empty()) { LOG_WARN("[RecordManager] No files found for segmentId: " + segmentId); return {}; } return seg; } int64_t RecordManager::toMsTimestamp(const std::string& s) { std::tm tm{}; char* ret = strptime(s.c_str(), "%Y-%m-%d %H:%M:%S", &tm); if (!ret) return -1; time_t t = mktime(&tm); return (int64_t)t * 1000; } std::string RecordManager::toReadable(int64_t ms) { time_t t = ms / 1000; std::tm* tm = localtime(&t); char buf[32]; strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm); return buf; } std::string RecordManager::loc_to_stream(int loc) { // loc 必须 0~7 if (loc < 0 || loc > 7) return ""; return "AHD" + std::to_string(loc + 1) + "_main"; }