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.
| Thread | Sở hữu bởi | Công việc | Cơ chế đồng bộ |
|---|---|---|---|
| Main thread | App (main.cpp) | Khởi động, tắt, vòng lặp Sleep() | Không cần lock |
| Capture thread | Mỗi IVideoSource | Đọc frame, gọi FrameCallback + AlertCallback | std::thread, std::atomic<bool> |
| Render thread | PreviewMonitor (SFML) | SFML render loop, đọc frame buffer | Per-slot mutex |
| Audio thread | AudioPreviewStream (SFML) | SFML audio callback, lấy samples | m_audioMutex |
| Inference thread | DOVERQualityScore | ONNX 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ểu | Mô tả |
|---|---|---|
| name | string | Phải duy nhất trên toàn bộ file - dùng làm key trong PluginManager |
| type | string | Khớp với stem cuối của tên DLL: "DeckLink" -> HD.QCMonitor.VideoSource.DeckLink.dll |
| enabled | bool | false -> bỏ qua, không load plugin này |
| dll | string (optional) | Override đường dẫn DLL tường minh, bỏ qua auto-resolve |
| windowX/Y | double [0..1] | Vị trí cell trong cửa sổ multiview (tỉ lệ) |
| windowWidth/Height | double [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.exe và bin/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
}
| Interface | Dùng cho | Phương thức chính |
|---|---|---|
| IVideoSource | Nguồn tín hiệu mới (NDI, RTMP, v.v.) | StartCapture(), StopCapture(), SetFrameCallback() |
| IFilter | Loại phát hiện mới | ProcessFrame(), SetAlertCallback(), SetClearAlertCallback() |
| IOutput | Đầu ra mới (Email, SNMP, v.v.) | TriggerAlert(), ClearAlert(), SetInputFrame() |
| IMultiviewMonitor | Cửa sổ hiển thị khác | RegisterChannel(), 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
)
| Tensor | Shape | Mô 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).