修复功能bug,对齐prd
Some checks failed
CI / host-build (push) Has been cancelled
CI / rk3588-cross-build (push) Has been cancelled

This commit is contained in:
sladro 2026-01-03 16:12:03 +08:00
parent 7fe7b34f6c
commit a12f91da63
12 changed files with 218 additions and 65 deletions

View File

@ -2,9 +2,10 @@ name: CI
on:
push:
branches: [ main ]
branches: [ master, main ]
pull_request:
branches: [ main ]
branches: [ master, main ]
workflow_dispatch:
jobs:
host-build:

View File

@ -54,7 +54,7 @@ struct Frame {
int stride = 0;
int dma_fd = -1;
uint64_t pts = 0;
uint64_t pts = 0; // microseconds
uint64_t frame_id = 0;
uint8_t* data = nullptr; // base pointer if mapped to CPU

View File

@ -2,6 +2,7 @@
#include <chrono>
#include <cstddef>
#include <cctype>
#include <deque>
#include <iomanip>
#include <iostream>
@ -14,6 +15,37 @@ namespace rk3588 {
enum class LogLevel { Debug, Info, Warn, Error };
inline const char* LogLevelToString(LogLevel lvl) {
switch (lvl) {
case LogLevel::Debug: return "debug";
case LogLevel::Info: return "info";
case LogLevel::Warn: return "warn";
case LogLevel::Error: return "error";
}
return "info";
}
inline bool ParseLogLevel(std::string s, LogLevel& out) {
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (s == "debug" || s == "d") {
out = LogLevel::Debug;
return true;
}
if (s == "info" || s == "i") {
out = LogLevel::Info;
return true;
}
if (s == "warn" || s == "warning" || s == "w") {
out = LogLevel::Warn;
return true;
}
if (s == "error" || s == "e") {
out = LogLevel::Error;
return true;
}
return false;
}
class Logger {
public:
static Logger& Instance() {
@ -27,7 +59,21 @@ public:
TrimLocked();
}
void SetLevel(LogLevel lvl) {
std::lock_guard<std::mutex> lock(mu_);
min_level_ = lvl;
}
LogLevel GetLevel() const {
std::lock_guard<std::mutex> lock(mu_);
return min_level_;
}
void Log(LogLevel lvl, const std::string& msg) {
{
std::lock_guard<std::mutex> lock(mu_);
if (static_cast<int>(lvl) < static_cast<int>(min_level_)) return;
}
const std::string line = FormatLine(lvl, msg);
{
std::lock_guard<std::mutex> lock(mu_);
@ -89,6 +135,7 @@ private:
mutable std::mutex mu_;
std::deque<std::string> lines_;
size_t max_lines_ = 2000;
LogLevel min_level_ = LogLevel::Info;
};
inline void LogDebug(const std::string& msg) { Logger::Instance().Log(LogLevel::Debug, msg); }

View File

@ -3,8 +3,8 @@
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <deque>
#include <mutex>
#include <vector>
#include <utility>
namespace rk3588 {
@ -28,14 +28,18 @@ public:
};
SpscQueue(size_t capacity, QueueDropStrategy strategy)
: capacity_(capacity), strategy_(strategy) {}
: capacity_(capacity == 0 ? 1 : capacity), strategy_(strategy) {
buf_.resize(capacity_);
}
bool Push(T item) {
std::unique_lock<std::mutex> lock(mu_);
if (stop_) return false;
if (queue_.size() >= capacity_) {
if (size_ >= capacity_) {
if (strategy_ == QueueDropStrategy::DropOldest) {
queue_.pop_front();
// Drop the oldest item.
head_ = (head_ + 1) % capacity_;
--size_;
++dropped_;
} else if (strategy_ == QueueDropStrategy::DropNewest) {
// Drop the incoming item.
@ -43,11 +47,13 @@ public:
return true;
} else {
// Block until space is available
space_cv_.wait(lock, [&] { return queue_.size() < capacity_ || stop_; });
space_cv_.wait(lock, [&] { return size_ < capacity_ || stop_; });
if (stop_) return false;
}
}
queue_.push_back(std::move(item));
buf_[tail_] = std::move(item);
tail_ = (tail_ + 1) % capacity_;
++size_;
++pushed_;
data_cv_.notify_one();
return true;
@ -55,12 +61,13 @@ public:
bool Pop(T& out, std::chrono::milliseconds timeout) {
std::unique_lock<std::mutex> lock(mu_);
if (!data_cv_.wait_for(lock, timeout, [&] { return !queue_.empty() || stop_; })) {
if (!data_cv_.wait_for(lock, timeout, [&] { return size_ > 0 || stop_; })) {
return false;
}
if (queue_.empty()) return false;
out = std::move(queue_.front());
queue_.pop_front();
if (size_ == 0) return false;
out = std::move(buf_[head_]);
head_ = (head_ + 1) % capacity_;
--size_;
++popped_;
space_cv_.notify_one();
return true;
@ -80,7 +87,7 @@ public:
size_t Size() const {
std::lock_guard<std::mutex> lock(mu_);
return queue_.size();
return size_;
}
size_t Capacity() const { return capacity_; }
@ -103,7 +110,7 @@ public:
Stats GetStats() const {
std::lock_guard<std::mutex> lock(mu_);
Stats s;
s.size = queue_.size();
s.size = size_;
s.capacity = capacity_;
s.dropped = dropped_;
s.pushed = pushed_;
@ -118,7 +125,10 @@ private:
mutable std::mutex mu_;
std::condition_variable data_cv_;
std::condition_variable space_cv_;
std::deque<T> queue_;
std::vector<T> buf_;
size_t head_ = 0;
size_t tail_ = 0;
size_t size_ = 0;
bool stop_ = false;
size_t dropped_ = 0;
size_t pushed_ = 0;

View File

@ -148,19 +148,55 @@ std::vector<uint8_t> SnapshotAction::EncodeJpeg(const std::shared_ptr<Frame>& fr
uint8_t* src_data[4] = {nullptr};
int src_linesize[4] = {0};
auto plane_ptr = [&](int idx) -> uint8_t* {
if (idx < 0 || idx >= frame->plane_count) return nullptr;
if (frame->planes[idx].data) return frame->planes[idx].data;
if (!frame->data) return nullptr;
const int off = frame->planes[idx].offset;
if (off < 0 || static_cast<size_t>(off) >= frame->data_size) return nullptr;
return frame->data + off;
};
auto plane_stride = [&](int idx, int fallback) -> int {
if (idx >= 0 && idx < frame->plane_count && frame->planes[idx].stride > 0) {
return frame->planes[idx].stride;
}
if (frame->stride > 0) return frame->stride;
return fallback;
};
auto validate_bounds = [&](uint8_t* ptr, int stride, int h) -> bool {
if (!frame->data || frame->data_size == 0) return true;
if (!ptr || stride <= 0 || h <= 0) return false;
const uint8_t* base = frame->data;
const uint8_t* end = base + frame->data_size;
if (ptr < base || ptr >= end) return true; // Unknown ownership; cannot validate.
const size_t off = static_cast<size_t>(ptr - base);
const size_t need = off + static_cast<size_t>(stride) * static_cast<size_t>(h);
return need <= frame->data_size;
};
if (frame->format == PixelFormat::RGB || frame->format == PixelFormat::BGR) {
src_data[0] = frame->data;
src_linesize[0] = frame->width * 3;
src_data[0] = plane_ptr(0) ? plane_ptr(0) : frame->data;
src_linesize[0] = plane_stride(0, frame->width * 3);
if (!validate_bounds(src_data[0], src_linesize[0], frame->height)) {
sws_freeContext(sws);
av_frame_free(&av_frame);
avcodec_free_context(&ctx);
std::cerr << "[SnapshotAction] invalid RGB buffer size/stride (data_size=" << frame->data_size
<< ", stride=" << src_linesize[0] << ", h=" << frame->height << ")\n";
return output;
}
} else if (frame->format == PixelFormat::NV12) {
src_data[0] = frame->planes[0].data ? frame->planes[0].data : frame->data;
src_data[1] = frame->planes[1].data ? frame->planes[1].data
: frame->data + frame->width * frame->height;
src_data[0] = plane_ptr(0) ? plane_ptr(0) : frame->data;
src_data[1] = plane_ptr(1);
if (!src_data[1] && frame->data) {
src_data[1] = frame->data + static_cast<size_t>(frame->width) * frame->height;
}
src_linesize[0] = frame->planes[0].stride > 0 ? frame->planes[0].stride : frame->width;
src_linesize[1] = frame->planes[1].stride > 0 ? frame->planes[1].stride : frame->width;
} else if (frame->format == PixelFormat::YUV420) {
src_data[0] = frame->planes[0].data ? frame->planes[0].data : frame->data;
src_data[1] = frame->planes[1].data;
src_data[2] = frame->planes[2].data;
src_data[0] = plane_ptr(0) ? plane_ptr(0) : frame->data;
src_data[1] = plane_ptr(1);
src_data[2] = plane_ptr(2);
src_linesize[0] = frame->planes[0].stride > 0 ? frame->planes[0].stride : frame->width;
src_linesize[1] = frame->planes[1].stride > 0 ? frame->planes[1].stride : frame->width / 2;
src_linesize[2] = frame->planes[2].stride > 0 ? frame->planes[2].stride : frame->width / 2;

View File

@ -160,7 +160,7 @@ private:
static_cast<int>(y_size)};
frame->frame_id = ++frame_id_;
frame->pts = duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
frame->pts = duration_cast<microseconds>(steady_clock::now().time_since_epoch()).count();
PushToDownstream(frame);
if (realtime_ && interval.count() > 0) {
@ -263,7 +263,7 @@ private:
const int64_t best_pts = f.pts;
frame->pts = best_pts == AV_NOPTS_VALUE
? 0
: static_cast<uint64_t>(av_rescale_q(best_pts, time_base, {1, 1000}));
: static_cast<uint64_t>(av_rescale_q(best_pts, time_base, {1, 1000000}));
if (fmt == PixelFormat::NV12 && plane_count == 2) {
int y_size = width * height;
@ -396,7 +396,7 @@ private:
}
}
bool Decode(const uint8_t* data, size_t size, bool eos, int64_t pts_ms,
bool Decode(const uint8_t* data, size_t size, bool eos, int64_t pts_us,
const std::function<void(MppFrame)>& on_frame) {
if (!ctx || !mpi || !data || size == 0) return false;
@ -408,7 +408,7 @@ private:
mpp_packet_set_size(packet, size);
mpp_packet_set_pos(packet, pkt_data);
mpp_packet_set_length(packet, size);
mpp_packet_set_pts(packet, pts_ms);
mpp_packet_set_pts(packet, pts_us);
if (eos) mpp_packet_set_eos(packet);
bool pkt_done = false;
@ -586,9 +586,9 @@ private:
av_packet_unref(pkt);
continue;
}
int64_t pts_ms = pkt->pts == AV_NOPTS_VALUE ? 0
: av_rescale_q(pkt->pts, time_base, {1, 1000});
dec.Decode(pkt->data, pkt->size, false, pts_ms,
int64_t pts_us = pkt->pts == AV_NOPTS_VALUE ? 0
: av_rescale_q(pkt->pts, time_base, {1, 1000000});
dec.Decode(pkt->data, pkt->size, false, pts_us,
[&](MppFrame frm) {
PushFrameFromMpp(frm);
if (realtime_ && interval.count() > 0) {

View File

@ -130,7 +130,7 @@ private:
frame->planes[0] = {nullptr, width_, width_ * height_, 0};
frame->planes[1] = {nullptr, width_, width_ * height_ / 2, width_ * height_};
frame->frame_id = ++frame_id_;
frame->pts = duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
frame->pts = duration_cast<microseconds>(steady_clock::now().time_since_epoch()).count();
PushToDownstream(frame);
if (frame_id_ % 100 == 0) {
@ -275,7 +275,7 @@ private:
frame->frame_id = ++frame_id_;
auto best_pts = f.pts; // av_frame_get_best_effort_timestamp is deprecated
frame->pts = best_pts == AV_NOPTS_VALUE ? 0
: static_cast<uint64_t>(av_rescale_q(best_pts, time_base, {1, 1000}));
: static_cast<uint64_t>(av_rescale_q(best_pts, time_base, {1, 1000000}));
frame->data_owner = buffer;
if (fmt == PixelFormat::NV12 && plane_count == 2) {
@ -345,7 +345,7 @@ private:
}
}
bool Decode(const uint8_t* data, size_t size, bool eos, int64_t pts_ms,
bool Decode(const uint8_t* data, size_t size, bool eos, int64_t pts_us,
const std::function<void(MppFrame)>& on_frame) {
if (!ctx || !mpi || !data || size == 0) return false;
@ -357,7 +357,7 @@ private:
mpp_packet_set_size(packet, size);
mpp_packet_set_pos(packet, pkt_data);
mpp_packet_set_length(packet, size);
mpp_packet_set_pts(packet, pts_ms);
mpp_packet_set_pts(packet, pts_us);
if (eos) mpp_packet_set_eos(packet);
bool pkt_done = false;
@ -504,9 +504,9 @@ private:
av_packet_unref(pkt);
continue;
}
int64_t pts_ms = pkt->pts == AV_NOPTS_VALUE ? 0
: av_rescale_q(pkt->pts, time_base, {1, 1000});
dec.Decode(pkt->data, pkt->size, false, pts_ms,
int64_t pts_us = pkt->pts == AV_NOPTS_VALUE ? 0
: av_rescale_q(pkt->pts, time_base, {1, 1000000});
dec.Decode(pkt->data, pkt->size, false, pts_us,
[&](MppFrame frm) { PushFrameFromMpp(frm); });
av_packet_unref(pkt);
}

View File

@ -518,8 +518,8 @@ public:
}
}
int64_t mpp_pts = mpp_packet_get_pts(packet);
// Use encoder's pts if valid, otherwise compute from frame count
out.pts_ms = mpp_pts > 0 ? mpp_pts : static_cast<int64_t>(frame_count_ * 1000 / fps_);
// Frame::pts is microseconds; ZLMediaKit/FFmpeg muxer paths use millisecond timestamps.
out.pts_ms = mpp_pts > 0 ? (mpp_pts / 1000) : static_cast<int64_t>(frame_count_ * 1000 / fps_);
++frame_count_;
if (on_packet) on_packet(out);
mpp_packet_deinit(&packet);

View File

@ -2,7 +2,8 @@
#include <cstring>
#include <fstream>
#include <iostream>
#include "utils/logger.h"
namespace rk3588 {
@ -12,7 +13,7 @@ AiScheduler& AiScheduler::Instance() {
}
AiScheduler::AiScheduler() {
std::cout << "[AiScheduler] initialized\n";
LogInfo("[AiScheduler] initialized");
}
AiScheduler::~AiScheduler() {
@ -25,8 +26,8 @@ void AiScheduler::Shutdown() {
std::lock_guard<std::mutex> lock(models_mutex_);
models_.clear();
}
std::cout << "[AiScheduler] shutdown, total inferences: " << total_inferences_.load()
<< ", errors: " << total_errors_.load() << "\n";
LogInfo("[AiScheduler] shutdown, total inferences: " + std::to_string(total_inferences_.load()) +
", errors: " + std::to_string(total_errors_.load()));
#endif
}
@ -104,10 +105,11 @@ ModelHandle AiScheduler::LoadModel(const std::string& model_path, std::string& e
models_[handle] = ctx;
}
std::cout << "[AiScheduler] loaded model: " << model_path
<< " (handle=" << handle << ", input=" << ctx->input_w
<< "x" << ctx->input_h << "x" << ctx->input_c
<< ", outputs=" << ctx->n_output << ")\n";
LogInfo("[AiScheduler] loaded model: " + model_path +
" (handle=" + std::to_string(handle) +
", input=" + std::to_string(ctx->input_w) + "x" + std::to_string(ctx->input_h) +
"x" + std::to_string(ctx->input_c) +
", outputs=" + std::to_string(ctx->n_output) + ")");
return handle;
#else
@ -129,7 +131,7 @@ void AiScheduler::UnloadModel(ModelHandle handle) {
}
}
if (erased) {
std::cout << "[AiScheduler] unloaded model handle=" << handle << "\n";
LogInfo("[AiScheduler] unloaded model handle=" + std::to_string(handle));
}
#else
(void)handle;

View File

@ -806,6 +806,14 @@ bool GraphManager::Build(const SimpleJson& root_cfg, std::string& err) {
if (!plugin_path.empty()) {
loader_.SetPluginDir(plugin_path);
}
const std::string log_level = g->ValueOr<std::string>("log_level", "");
if (!log_level.empty()) {
LogLevel lvl;
if (ParseLogLevel(log_level, lvl)) {
Logger::Instance().SetLevel(lvl);
}
}
}
std::lock_guard<std::mutex> lock(graphs_mu_);
@ -928,6 +936,17 @@ bool GraphManager::ReloadFromFile(const std::string& path, std::string& err) {
return "";
}();
std::optional<LogLevel> new_log_level;
if (const SimpleJson* g = expanded.Find("global")) {
const std::string log_level = g->ValueOr<std::string>("log_level", "");
if (!log_level.empty()) {
LogLevel lvl;
if (ParseLogLevel(log_level, lvl)) {
new_log_level = lvl;
}
}
}
std::lock_guard<std::mutex> lock(graphs_mu_);
const SimpleJson prev_last_good = last_good_root_;
@ -1047,6 +1066,9 @@ bool GraphManager::ReloadFromFile(const std::string& path, std::string& err) {
last_good_root_ = expanded;
default_queue_size_ = new_default_queue_size;
default_strategy_ = new_default_strategy;
if (new_log_level) {
Logger::Instance().SetLevel(*new_log_level);
}
return true;
}
@ -1149,6 +1171,10 @@ bool GraphManager::ReloadFromFile(const std::string& path, std::string& err) {
default_queue_size_ = new_default_queue_size;
default_strategy_ = new_default_strategy;
if (new_log_level) {
Logger::Instance().SetLevel(*new_log_level);
}
if (!last_good_path_.empty()) {
std::string werr;
if (!WriteTextFileAtomic(last_good_path_, StringifySimpleJson(last_good_root_), werr)) {

View File

@ -396,14 +396,14 @@ void HttpServer::Stop() {
void HttpServer::ServerLoop() {
if (!InitSockets()) {
std::cerr << "[HttpServer] socket init failed\n";
LogError("[HttpServer] socket init failed");
running_.store(false);
return;
}
Sock srv = socket(AF_INET, SOCK_STREAM, 0);
if (srv == kInvalidSock) {
std::cerr << "[HttpServer] socket() failed\n";
LogError("[HttpServer] socket() failed");
CleanupSockets();
running_.store(false);
return;
@ -421,14 +421,14 @@ void HttpServer::ServerLoop() {
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(static_cast<uint16_t>(port_));
if (bind(srv, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0) {
std::cerr << "[HttpServer] bind failed on port " << port_ << "\n";
LogError("[HttpServer] bind failed on port " + std::to_string(port_));
CloseSock(srv);
CleanupSockets();
running_.store(false);
return;
}
if (listen(srv, 16) != 0) {
std::cerr << "[HttpServer] listen failed\n";
LogError("[HttpServer] listen failed");
CloseSock(srv);
CleanupSockets();
running_.store(false);
@ -436,7 +436,7 @@ void HttpServer::ServerLoop() {
}
(void)SetNonBlocking(srv);
listen_sock_.store(static_cast<int64_t>(srv));
std::cout << "[HttpServer] listening on 0.0.0.0:" << port_ << " (web_root=" << web_root_ << ")\n";
LogInfo("[HttpServer] listening on 0.0.0.0:" + std::to_string(port_) + " (web_root=" + web_root_ + ")");
while (running_.load()) {
fd_set rfds;
@ -527,6 +527,9 @@ void HttpServer::ServerLoop() {
}
oss << "]}";
resp.body = oss.str();
} else if (req.path == "/api/log/level") {
const LogLevel lvl = Logger::Instance().GetLevel();
resp.body = std::string("{\"level\":\"") + LogLevelToString(lvl) + "\"}";
} else if (req.path.rfind("/api/nodes/", 0) == 0) {
// /api/nodes/{id}/metrics
const std::string prefix = "/api/nodes/";
@ -577,6 +580,33 @@ void HttpServer::ServerLoop() {
} else {
resp.body = OkJson();
}
} else if (req.path == "/api/log/level") {
if (req.body.empty()) {
resp.status = 400;
resp.body = ErrorJson("empty body");
} else {
SimpleJson body;
std::string jerr;
if (!ParseSimpleJson(req.body, body, jerr)) {
resp.status = 400;
resp.body = ErrorJson(jerr);
} else {
const std::string level = body.ValueOr<std::string>("level", "");
if (level.empty()) {
resp.status = 400;
resp.body = ErrorJson("missing 'level'");
} else {
LogLevel lvl;
if (!ParseLogLevel(level, lvl)) {
resp.status = 400;
resp.body = ErrorJson("invalid level; use debug|info|warn|error");
} else {
Logger::Instance().SetLevel(lvl);
resp.body = OkJson();
}
}
}
}
} else if (req.path.rfind("/api/nodes/", 0) == 0) {
// /api/nodes/{id}/config
const std::string prefix = "/api/nodes/";

View File

@ -23,6 +23,7 @@ namespace fs = std::filesystem;
#include "graph_manager.h"
#include "http_server.h"
#include "utils/logger.h"
#include "utils/simple_json.h"
namespace rk3588 {
@ -53,7 +54,7 @@ std::string ResolvePluginDir() {
void HandleSignal(int) {
if (g_manager) {
std::cerr << "\n[MediaServerApp] Caught signal, stopping...\n";
LogWarn("[MediaServerApp] Caught signal, stopping...");
g_manager->RequestStop();
}
}
@ -124,9 +125,9 @@ void InotifyWatchLoop(const std::string& path, std::atomic<bool>& stop_flag, Gra
if (matched) {
std::string err;
if (!mgr.ReloadFromFile(path, err)) {
std::cerr << "[MediaServerApp] hot reload failed: " << err << "\n";
LogWarn("[MediaServerApp] hot reload failed: " + err);
} else {
std::cout << "[MediaServerApp] hot reload succeeded\n";
LogInfo("[MediaServerApp] hot reload succeeded");
}
// Debounce burst events.
std::this_thread::sleep_for(std::chrono::milliseconds(300));
@ -158,9 +159,9 @@ void PollWatchLoop(const std::string& path, std::atomic<bool>& stop_flag, GraphM
std::string err;
if (!mgr.ReloadFromFile(path, err)) {
std::cerr << "[MediaServerApp] hot reload failed: " << err << "\n";
LogWarn("[MediaServerApp] hot reload failed: " + err);
} else {
std::cout << "[MediaServerApp] hot reload succeeded\n";
LogInfo("[MediaServerApp] hot reload succeeded");
}
#else
(void)path;
@ -178,7 +179,7 @@ int MediaServerApp::Start() {
SimpleJson root_cfg;
std::string err;
if (!GraphManager::LoadConfigFile(config_path_, root_cfg, err)) {
std::cerr << "[MediaServerApp] Failed to load config: " << err << "\n";
LogError("[MediaServerApp] Failed to load config: " + err);
return 1;
}
@ -190,7 +191,7 @@ int MediaServerApp::Start() {
}
if (!graph_manager_.BuildFromFile(config_path_, err)) {
std::cerr << "[MediaServerApp] Failed to build graphs: " << err << "\n";
LogError("[MediaServerApp] Failed to build graphs: " + err);
return 1;
}
@ -199,14 +200,14 @@ int MediaServerApp::Start() {
signal(SIGTERM, HandleSignal);
if (!graph_manager_.StartAll()) {
std::cerr << "[MediaServerApp] Failed to start graphs\n";
LogError("[MediaServerApp] Failed to start graphs");
return 1;
}
HttpServer http(graph_manager_, metrics_port, web_root);
(void)http.Start();
std::cout << "[MediaServerApp] Running. Press Ctrl+C to stop.\n";
LogInfo("[MediaServerApp] Running. Press Ctrl+C to stop.");
std::atomic<bool> stop_watch{false};
std::thread watcher;
@ -220,7 +221,7 @@ int MediaServerApp::Start() {
#else
watcher = std::thread([&] { PollWatchLoop(config_path_, stop_watch, graph_manager_); });
#endif
std::cout << "[MediaServerApp] Hot reload enabled\n";
LogInfo("[MediaServerApp] Hot reload enabled");
}
graph_manager_.BlockUntilStop();
@ -229,7 +230,7 @@ int MediaServerApp::Start() {
stop_watch.store(true);
if (watcher.joinable()) watcher.join();
std::cout << "[MediaServerApp] Shutdown complete.\n";
LogInfo("[MediaServerApp] Shutdown complete.");
return 0;
}