Kiến trúc Plugin động
Mỗi thành phần là một DLL riêng biệt, được load tại runtime theo appsettings.json. Kết nối qua callbacks trong Pipeline::Wire() - không có phụ thuộc trực tiếp giữa các plugin.
// Luồng dữ liệu chính appsettings.json | v Env::Load() -> PluginManager::LoadPlugin() | v Pipeline::Wire() | +-- Per channel: | | IVideoSource | | | +--[FrameCallback]--> IMultiviewMonitor::SetChannelFrame() | | --> IOutput::SetInputFrame() | | --> IFilter::ProcessFrame() | | | +--[AlertCallback]--> IMultiviewMonitor::TriggerChannelAlert() | --> IOutput::TriggerAlert() | +-- Mỗi IFilter: +--[AlertCallback]--> IMultiviewMonitor::TriggerChannelAlert() | --> IOutput::TriggerAlert() +--[ClearCallback]--> IMultiviewMonitor::ClearChannelAlert() --> IOutput::ClearAlert()
Plugin DLL export
IPlugin* CreatePlugin()
void DestroyPlugin(IPlugin*)

Mỗi DLL phải export 2 hàm này với extern "C" linkage.

Plugin lifecycle
1. CreatePlugin()
2. SetLogger(logger)
3. Initialize(cfg) -> bool
4. [hoạt động]
5. Shutdown()
6. DestroyPlugin(instance)
Shutdown order
1. pipeline.StopAll() // join threads
2. pipeline.Unwire() // xóa callbacks
3. manager.UnloadAll() // FreeLibrary

Thứ tự này bắt buộc để tránh use-after-free.

Core Interfaces
Tất cả interfaces nằm trong namespace HD::QCMonitor, khai báo trong Core/HD.QCMonitor.Core/include/. Đây là "hợp đồng" giữa Core và các plugin.
IPlugin (base)
wstring GetName() const
wstring GetVersion() const
void SetLogger(shared_ptr<ILogger>)
bool Initialize(const PluginConfig&)
void Shutdown()
IVideoSource
void SetFrameCallback(FrameCallback)
void SetAlertCallback(AlertCallback)
bool StartCapture()
void StopCapture()
bool IsCapturing() const
IFilter
void ProcessFrame(const Frame&)
void SetAlertCallback(AlertCallback)
void SetClearAlertCallback(function<void()>)

ProcessFrame() gọi từ capture thread - phải thread-safe.

IOutput
void SetInputFrame(const Frame&)
void TriggerAlert(const AlertEvent&)
void ClearAlert()

Tất cả phương thức phải thread-safe (gọi từ capture thread).

IMultiviewMonitor
void RegisterChannel(const PluginConfig&)
void SetChannelFrame(string, Frame)
void TriggerChannelAlert(string, AlertEvent)
void ClearChannelAlert(string)
Data types
Frame: pData, width, height, stride, format, timestamp_us, audio
AudioBuffer: pData (S16), channels, sampleRate, numSamples
AlertEvent: pluginName, message, severity, timestamp_us
PixelFormat: UYVY, NV12, BGRA
Thread Model
Hệ thống sử dụng nhiều thread độc lập. Mỗi plugin phải thread-safe khi nhận callbacks từ capture thread.
ThreadSở hữu bởiCông việcCơ chế đồng bộ
Main threadApp (main.cpp)Khởi động, tắt, vòng lặp Sleep()Không cần lock
Capture threadMỗi IVideoSourceĐọc frame, gọi FrameCallback + AlertCallbackstd::thread, std::atomic<bool>
Render threadPreviewMonitor (SFML)SFML render loop, đọc frame bufferPer-slot mutex
Audio threadAudioPreviewStream (SFML)SFML audio callback, lấy samplesm_audioMutex
Inference threadDOVERQualityScoreONNX inference (background)std::thread, queue mutex
// Thread safety trong Monitor plugin RegisterChannel() // Main thread - gọi trước StartAll(), không cần lock SetChannelFrame() // Capture thread - per-slot mutex cho frame buffer TriggerChannelAlert() // Capture thread - per-slot mutex cho alert state ClearChannelAlert() // Capture thread - per-slot mutex cho alert state // Render thread đọc dữ liệu với same per-slot mutex // Thread safety trong Filter plugins ProcessFrame() // Capture thread - state chỉ truy cập từ 1 thread SetAlertCallback() // Any thread - bảo vệ bởi std::mutex m_cbMutex SetClearAlertCallback() // Any thread - bảo vệ bởi std::mutex m_cbMutex
Cấu hình appsettings.json
File cấu hình duy nhất, nằm cùng thư mục với .exe. Được parse bởi Env::Load() khi khởi động. Cấu trúc: "monitor" (cửa sổ multiview) + "processes" (mảng các channel).
{ "monitor": { "type": "Monitor", "name": "Monitor", "enabled": true, "windowWidth": 1280, "windowHeight": 720, "vuWidth": 65, "alertBorder": 10 }, "processes": [ { "source": { "type": "DeckLink", "name": "SDI-1", "enabled": true, "deviceIndex": 0 }, "filters": [ { "type": "BlackVideoDetection", "name": "BlackDetect-SDI1", "enabled": true, "pixelBlackThreshold": 16, "maxNonBlackRatio": 0.001, "maxBlackSeconds": 3.0 }, { "type": "FreezeVideoDetection", "name": "FreezeDetect-SDI1", "enabled": true, "freezeThreshold": 4.0, "maxFreezeSeconds": 3.0 }, { "type": "AudioSilenceDetection", "name": "AudioSilence-SDI1", "enabled": true, "silenceThreshold": -60.0, "maxSilenceSeconds": 3.0 }, { "type": "AudioInversionDetection", "name": "AudioInversion-SDI1", "enabled": true, "inversionThreshold": -0.8, "maxInversionSeconds": 3.0, "minAudioLevelDb": -40.0 }, { "type": "AudioLimitDetector", "name": "AudioLimit-SDI1", "enabled": true, "limitThreshold": -3.0, "minDurationSeconds": 0.5 }, { "type": "DOVERQualityScore", "name": "DOVER-SDI1", "enabled": true, "modelPath": "models/dover.onnx", "minQualityScore": 50.0, "useCuda": false } ], "monitors": [ { "name": "CHANNEL 1", "enabled": true, "windowX": 0.0, "windowY": 0.0, "windowWidth": 0.5, "windowHeight": 0.5 } ], "outputs": [] } ] }
Tham sốKiểuMô tả
namestringPhải duy nhất trên toàn bộ file - dùng làm key trong PluginManager
typestringKhớp với stem cuối của tên DLL: "DeckLink" -> HD.QCMonitor.VideoSource.DeckLink.dll
enabledboolfalse -> bỏ qua, không load plugin này
dllstring (optional)Override đường dẫn DLL tường minh, bỏ qua auto-resolve
windowX/Ydouble [0..1]Vị trí cell trong cửa sổ multiview (tỉ lệ)
windowWidth/Heightdouble [0..1]Kích thước cell (tỉ lệ theo chiều rộng/cao cửa sổ)
Build và Deploy
Yêu cầu Visual Studio 2022, Windows SDK 10.0, Blackmagic Desktop Video SDK và FFmpeg 7.x dev package.
1
Chuẩn bị thư viện
Đặt Blackmagic DeckLink SDK vào ThirdParty/DeckLink/include/. Đặt FFmpeg 7.x dev package vào ThirdParty/FFmpeg/ (include/ + lib/ + bin/). Đặt ONNX Runtime vào ThirdParty/onnx/.
2
Mở solution
Mở HD.QCMonitor.sln trong Visual Studio 2022. Chọn cấu hình Release|x64 (hoặc Debug|x64).
3
Build solution
Build -> Build Solution (Ctrl+Shift+B). Output: bin/Release/HD.QCMonitor.App.exebin/Release/plugins/*.dll.
4
Chuẩn bị runtime
Sao chép appsettings.json vào cùng thư mục với .exe. Sao chép FFmpeg DLLs (avcodec-61.dll, avformat-61.dll, v.v.) vào thư mục exe. Sao chép dover.onnx vào models/.
5
Chạy ứng dụng
Chạy HD.QCMonitor.App.exe. Nhấn Ctrl+C để dừng. Log được ghi ra console và file xoay ngày trong thư mục logs/.
// Cấu trúc thư mục output bin/Release/ +-- HD.QCMonitor.App.exe +-- HD.QCMonitor.Core.dll +-- models/ | +-- dover.onnx +-- plugins/ +-- HD.QCMonitor.VideoSource.DeckLink.dll +-- HD.QCMonitor.VideoSource.FFmpeg.dll +-- HD.QCMonitor.Monitor.dll +-- HD.QCMonitor.Filter.BlackVideoDetection.dll +-- HD.QCMonitor.Filter.FreezeVideoDetection.dll +-- HD.QCMonitor.Filter.AudioSilenceDetection.dll +-- HD.QCMonitor.Filter.AudioInversionDetection.dll +-- HD.QCMonitor.Filter.AudioLimitDetector.dll +-- HD.QCMonitor.Filter.DOVERQualityScore.dll +-- HD.QCMonitor.Output.TelegramAlert.dll +-- HD.QCMonitor.Web.dll
Mở rộng - Thêm Plugin mới
Thêm loại phát hiện mới, nguồn tín hiệu mới hoặc output mới chỉ cần tạo DLL mới. Không cần sửa đổi Core hay các plugin hiện có.
// Ví dụ: Thêm Filter mới (phát hiện logo mất) // 1. Tạo project DLL mới trong Plugins/ // HD.QCMonitor.Filter.LogoDetection.vcxproj // 2. Implement IFilter class LogoDetectionFilter : public IFilter { public: wstring GetName() const override { return L"LogoDetection"; } bool Initialize(const PluginConfig& cfg) override { m_threshold = cfg.GetDouble("threshold", 0.8); return true; } void ProcessFrame(const Frame& frame) override { // Phân tích frame, gọi m_alertCb nếu phát hiện } // ... implement các phương thức khác }; // 3. Export CreatePlugin / DestroyPlugin extern "C" { IPlugin* CreatePlugin() { return new LogoDetectionFilter(); } void DestroyPlugin(IPlugin* p) { delete p; } } // 4. Thêm vào appsettings.json { "type": "LogoDetection", "name": "LogoDetect-SDI1", "enabled": true, "threshold": 0.8 }
InterfaceDùng choPhương thức chính
IVideoSourceNguồn tín hiệu mới (NDI, RTMP, v.v.)StartCapture(), StopCapture(), SetFrameCallback()
IFilterLoại phát hiện mớiProcessFrame(), SetAlertCallback(), SetClearAlertCallback()
IOutputĐầu ra mới (Email, SNMP, v.v.)TriggerAlert(), ClearAlert(), SetInputFrame()
IMultiviewMonitorCửa sổ hiển thị khácRegisterChannel(), SetChannelFrame(), TriggerChannelAlert()
Xuất mô hình DOVER sang ONNX
Hướng dẫn xuất mô hình DOVER từ PyTorch sang ONNX để sử dụng với plugin DOVERQualityScore.
1
Cài đặt DOVER
git clone https://github.com/VQAssessment/DOVER rồi pip install -r requirements.txt onnx onnxruntime.
2
Tải pre-trained weights
Full DOVER (~300MB): DOVER.pth. DOVER-Mobile (~30MB): DOVER-Mobile.pth (dùng với singleInput: true).
3
Chạy script export
Lưu script export_dover_onnx.py vào thư mục DOVER rồi chạy python export_dover_onnx.py. Output: dover.onnx.
4
Đặt file model
Sao chép dover.onnx vào models/ cùng thư mục với .exe, hoặc set đường dẫn tuyệt đối trong modelPath.
# export_dover_onnx.py import torch from dover.models import DOVER class DOVERExportWrapper(torch.nn.Module): def forward(self, aesthetic, technical): scores = self.model([aesthetic, technical], inference=True) return scores[0], scores[1] model = DOVER() model.load_state_dict(torch.load("DOVER.pth", map_location="cpu")) model.eval() wrapper = DOVERExportWrapper(model) T, H, W = 32, 224, 224 aesthetic_in = torch.randn(1, 3, T, H, W) technical_in = torch.randn(1, 3, T, H, W) torch.onnx.export( wrapper, (aesthetic_in, technical_in), "dover.onnx", input_names=["aesthetic", "technical"], output_names=["aesthetic_score", "technical_score"], dynamic_axes={"aesthetic": {2: "T"}, "technical": {2: "T"}}, opset_version=17 )
TensorShapeMô tả
aesthetic (input)[1, 3, T, H, W]Full-frame temporal clip, ImageNet-normalised, NCTHW
technical (input)[1, 3, T, H, W]7x7 fragment mosaic temporal clip, cùng shape với aesthetic
aesthetic_score (output)scalar / [1]Raw regression logit - aesthetic branch
technical_score (output)scalar / [1]Raw regression logit - technical branch

Lưu ý: Mô hình cũ export với batch=4 cho technical input [4, 3, T, H, W] KHÔNG tương thích. Phải export lại với batch=1 cho cả hai input. Mô hình sai sẽ cho điểm cố định ~50% (raw output ~0 -> sigmoid(0) = 0.5).