Tính năng 1
Nguồn tín hiệu đa dạng
Hỗ trợ Blackmagic DeckLink SDI và FFmpeg (file / stream mạng). Mỗi nguồn chạy trên capture thread riêng, gọi FrameCallback và AlertCallback thread-safe.
DeckLink SDI: Blackmagic Desktop Video SDK (COM API). Tự động nhận diện format SDI khi tín hiệu thay đổi (VideoInputFormatChanged). Hỗ trợ UYVY 8-bit và NV12 10-bit. Audio PCM S16 48kHz stereo.
FFmpegSource: Hỗ trợ file cục bộ, UDP/Multicast MPEG-TS, RTSP, RTMP, HTTP/HLS. Pixel format đầu ra luôn là BGRA. Audio đầu ra luôn là PCM S16 48kHz stereo.
Auto-reconnect: FFmpegSource tự động retry khi mất tín hiệu. Fire AlertEvent "No signal" mỗi lần thất bại, log "signal restored" khi kết nối lại. Tham số retryDelaySeconds cấu hình được (default: 5.0s).
Pacing thời gian thực: FFmpegSource tính toán targetWall dựa trên PTS của frame để phát đúng tốc độ thực tế, tránh phát quá nhanh hoặc quá chậm.
Live stream options: Tự động set timeout=3s, reuse_socket=1 (UDP multicast), buffer_size=1MB cho live streams để tránh blocking vô hạn.
DeckLink
COM callback thread (DeckLink SDK) -> VideoInputFrameArrived() -> FrameCallback(frame). Capture thread chủ yếu cho tín hiệu từ SDK.
FFmpegSource
CaptureThread (std::thread) -> outer retry loop -> OpenStream() -> inner decode loop -> FrameCallback. m_capturing là std::atomic<bool>.
Tính năng 2
Phát hiện Video Đen (Black Video Detection)
Phân tích tỉ lệ pixel sáng trong frame GRAY8 64x36. Phát cảnh báo khi video đen liên tục quá ngưỡng thời gian. Tránh false positive cho frame có nền đen nhưng có chữ trắng.
Convert frame về GRAY8 64x36 bằng sws_scale(). Kích thước nhỏ giúp xử lý nhanh, đủ độ chính xác để phát hiện.
Đếm số pixel có luma > pixelBlackThreshold (default: 16). Tính nonBlackRatio = nonBlack / (64 x 36). Frame là "đen" khi nonBlackRatio <= maxNonBlackRatio (default: 0.001 = 0.1%).
Với maxNonBlackRatio = 0.001: chỉ cần ~3 pixel sáng (luma > 16) trên tổng 2304 pixel là đủ để KHÔNG coi là đen. Frame có nền đen nhưng có 1 dòng chữ trắng (~0.015 ratio) sẽ KHÔNG bị cảnh báo.
State machine: không đen -> đen: lưu m_blackStart. Đang đen: fire AlertCallback sau maxBlackSeconds. Đen -> sáng: gọi ClearAlertCallback, reset state.
ProcessFrame(frame): // 1. Convert về GRAY8 64x36 sws_scale(frame) -> gray64x36 // 2. Đếm pixel sáng nonBlack = count(pixel.luma > pixelBlackThreshold) nonBlackRatio = nonBlack / 2304 // 3. Kiểm tra ngưỡng isBlack = (nonBlackRatio <= maxNonBlackRatio) // 4. State machine if isBlack and !m_inBlack: m_blackStart = now if isBlack and (now - m_blackStart) >= maxBlackSeconds: AlertCallback() if !isBlack and m_inBlack: ClearAlertCallback(), reset
Phân tích GRAY8 64x36 Tránh false positive chữ trắng Severity: 0.9
Tính năng 3
Phát hiện Hình Đứng (Freeze Video Detection)
Kết hợp MAD (Mean Absolute Difference) và motionPixelRatio để phát hiện hình đứng chính xác. Tránh false positive cho cảnh có người nói chuyện (đầu/tay cử động ~5-20% frame).
Convert frame về GRAY8 64x36. So sánh với frame trước. Frame bị "đóng băng" chỉ khi cả hai điều kiện thỏa: MAD < freezeThresholdmotionPixelRatio < motionPixelMinRatio.
MAD = mean(|current[i] - prev[i]|). Ngưỡng default: 4.0. Phản ánh mức độ thay đổi trung bình toàn khung hình.
motionPixelRatio = count(|diff[i]| > pixelMotionThreshold) / totalPixels. Default: pixelMotionThreshold=8, motionPixelMinRatio=0.005 (0.5% = ~11 pixel). Chỉ cần ~12 pixel chuyển động là đủ để KHÔNG coi là freeze.
Khi format hoặc độ phân giải thay đổi, SwsContext được tạo lại và m_hasPrevFrame reset về false - tránh so sánh sai giữa các format khác nhau.
Ví dụ thực tế
Hoàn toàn đóng băng: MAD=0.0, motionRatio=0.000 -> FREEZE
Test card / slate: MAD=0.1, motionRatio=0.001 -> FREEZE
Nền tĩnh + người nói (đầu ~10%): MAD=1.8, motionRatio=0.025 -> OK
Chuyển cảnh hoàn toàn: MAD=40.0, motionRatio=0.850 -> OK
Điều chỉnh độ nhạy
Nhạy hơn: giảm motionPixelMinRatio (ví dụ: 0.002)
Ít nhạy hơn: tăng motionPixelMinRatio
Frame đen liên tục: bị phát hiện bởi cả BlackVideo và FreezeVideo
Tính năng 4
Phát hiện Audio Im Lặng (Silence Detection)
Tính RMS của tín hiệu PCM S16 chuyển về dBFS. Không phụ thuộc FFmpeg - tính toán thuần C++ trên PCM S16. Phát cảnh báo khi âm lượng dưới ngưỡng liên tục.
Tính RMS trên toàn bộ samples (tất cả channels): rms = sqrt(sum(sample^2) / totalSamples). Chuyển về dBFS: dB = 20 * log10(rms / 32767). Nếu rms=0: dB=-144.0.
Frame không có audio (numSamples == 0) được coi là im lặng - phát hiện nguồn chưa kết nối audio.
Dùng silenceThreshold = -72.0 nếu muốn phân biệt silence thực sự với noise floor thấp. Default -60.0 dBFS phù hợp cho phần lớn trường hợp broadcast.
Thang đo tham chiếu
0 dBFS: Tối đa digital (full scale)
-9 dBFS: Mức peak cho broadcast
-18 dBFS: Mức nominal cho broadcast
-60 dBFS: Ngưỡng silence mặc định
-144 dBFS: Tín hiệu zero (all samples = 0)
AlertEvent
pluginName: L"AudioSilence-SDI1"
message: L"Audio silence detected"
severity: 0.7
timestamp_us: frame.timestamp_us
Tính năng 5
Phát hiện Audio Ngược Pha (Inversion Detection)
Tính hệ số tương quan pha (cross-correlation) giữa kênh L và R. Phát hiện khi một kênh bị đảo dây kết nối vật lý, gây triệt tiêu tín hiệu khi phát mono.
Công thức: corr = sum(L[i] * R[i]) / sqrt(sum(L[i]^2) * sum(R[i]^2)). Kết quả [-1, +1]. +1: cùng pha, 0: không tương quan, -1: ngược pha hoàn toàn.
Bỏ qua frame nếu dBFS < minAudioLevelDb (default: -40.0) để tránh false positive khi âm lượng quá nhỏ (noise floor).
Bỏ qua audio mono (channels < 2) - không phân tích. Frame không có audio (numSamples == 0) bị bỏ qua - không tính là ngược pha.
Một số tín hiệu stereo có corr âm tự nhiên (Wide stereo, MS-encoded). Điều chỉnh inversionThreshold = -0.95 nếu có nhiều false positive.
Giá trị corr điển hình
Stereo bình thường: -0.3 .. +0.3
Một kênh mất (silence): ~0.0
Một kênh bị đảo pha: -0.9 .. -1.0 (PHÁT HIỆN)
Test tone in-phase (L=R): ~+1.0
Test tone out-of-phase (L=-R): ~-1.0 (PHÁT HIỆN)
AlertEvent
message: "Audio phase inversion on [name] (>3s, corr=-0.94)"
severity: 0.9
Hoạt động song song với AudioSilenceDetection
Tính năng 6
Phát hiện Audio Vượt Ngưỡng (Limit Detector)
Tính peak dBFS - mẫu có biên độ tuyệt đối lớn nhất trong frame. Dùng peak thay vì RMS để bắt được transient ngắn nhất có thể gây clipping. Không phụ thuộc FFmpeg.
Tính peak = max(|sample[i]|) trên toàn bộ samples. Chuyển về dBFS: dB = 20 * log10(peak / 32767). isOver = (dB >= limitThreshold).
Dùng limitThreshold = 0.0 để chỉ cảnh báo khi clip thực sự (full scale). Dùng -6.0 cho headroom thoải mái hơn. Default -3.0 phù hợp cho broadcast.
minDurationSeconds = 0.0 sẽ fire alert ngay từ frame đầu tiên vượt ngưỡng (không khuyến nghị, dễ false positive). Default 0.5s là cân bằng tốt.
Peak dBFS (không phải RMS) minDurationSeconds: 0.5s Severity: 0.9
Tính năng 7
Chấm Điểm Chất Lượng AI - DOVER Quality Score
Sử dụng mô hình DOVER qua ONNX Runtime. Phân tích cả hai nhánh aesthetic (toàn cảnh) và technical (chi tiết texture). Background inference - không bao giờ block pipeline video.
Lấy mẫu 1 frame mỗi sampleEveryN frame đến. Pre-process thành 2 views: Aesthetic (full frame resize 224x224, ImageNet-normalised) và Technical (7x7 fragment mosaic 32x32 patches -> 224x224).
Buffer framesPerClip (32) frames = một temporal clip. Khi đầy, gửi vào background inference thread. ProcessFrame() không bao giờ block - pipeline video tiếp tục chạy bình thường.
ONNX inference stack mỗi view thành [1, 3, T, H, W] (NCTHW). Tính điểm theo công thức DOVER fusion: x = (rawTech - techMean) / techStd * fusionWeightTech + (rawAes - aesMean) / aesStd * fusionWeightAes; score = sigmoid(x) * 100.
Z-score normalization bắt buộc: raw logits chỉ span ~+-0.2, không có scaling mới điểm đều cluster ở ~50%. Constants từ DOVER validation set: techMean=0.1107, techStd=0.07355, aesMean=-0.08285, aesStd=0.03774.
Hỗ trợ CUDA execution provider cho GPU acceleration. Inference CPU: 300-600ms. Inference GPU: 30-80ms. Set useCuda: true cần onnxruntime_providers_cuda.dll.
Bảng điểm tham chiếu
80-100 (Excellent): Broadcast quality, không có lỗi
60-80 (Good): Lỗi nhỏ, không gây khó chịu
40-60 (Fair): Nhiễu / mờ / nén rõ
20-40 (Poor): Suy giảm thường xuyên
0-20 (Bad): Mất chất lượng nghiêm trọng
Nguồn điển hình
Uncompressed / lossless SDI: 85-95
H.264 high-bitrate (≥15 Mbps): 70-85
H.264 mid-bitrate (4-15 Mbps): 55-75
H.264 low-bitrate (<4 Mbps): 35-60
Heavy re-encode: 25-50
Fragment Mosaic (Technical)
Grid 7x7 = 49 patches, mỗi patch 32x32px.
Vị trí: hgrids[i] = round(i * (fragH-32) / 6)
Bắt được texture detail từ nhiều vùng khác nhau của frame - nhạy với compression artifacts, noise, sharpness.
AlertEvent
message: "Low video quality on [DOVER-SDI1] (score=38/100, min=50)"
severity = 1.0 - (score / 100.0)
Re-fire mỗi 1 giây khi vẫn dưới ngưỡng
DOVER ONNX Runtime CPU: 300-600ms GPU CUDA: 30-80ms Background thread
Tính năng 8
Hiển Thị Multiview (Monitor Plugin)
Cửa sổ SFML hiển thị nhiều kênh video đồng thời. Mỗi cell có video preview, VU meter audio, viền đỏ khi cảnh báo. Click để nghe audio preview. Nút Mute toàn bộ.
Mỗi cell hiển thị: Video area (scaled fit, giữ aspect ratio), Channel name (label phía trên), VU meter (cột phải, -60 đến 0 dBFS), Alert border (viền đỏ toàn cell), Alert text (tên plugin + thông điệp).
VU meter: Xanh lá (-60 đến -18 dBFS), Vàng (-18 đến -9 dBFS), Đỏ (-9 đến 0 dBFS). Hiển thị mức âm lượng thực tế theo thời gian thực.
Audio preview: Click vào cell để chọn kênh nghe. AudioPreviewStream (extends sf::SoundStream) gọi onGetData() từ audio thread. Tự động resample nếu sample rate khác 48kHz.
Pixel format conversion: Mỗi cell có SwsContext riêng (per-slot, không lock) để convert UYVY / NV12 / BGRA -> BGRA (upload lên sf::Texture).
Thread safety: RegisterChannel() trên main thread (trước StartAll). SetChannelFrame() / TriggerChannelAlert() / ClearChannelAlert() từ capture thread - bảo vệ bởi per-slot mutex. Render thread đọc với same mutex.
Cell layout
windowX, windowY: Tọa độ góc trái/trên [0..1]
windowWidth, windowHeight: Kích thước [0..1]
Ví dụ 4 kênh 2x2: mỗi cell 0.5 x 0.5
Dependencies
SFML 2.5.1 (NuGet)
sfml-graphics-2.dll - render window
sfml-audio-2.dll - audio playback
openal32.dll - OpenAL backend
Tính năng 9
Web Dashboard, Alert History & Cảnh Báo Tự Động
Web Dashboard xem preview tín hiệu, giám sát real-time và Alert History tra cứu lịch sử. Telegram Alert gửi cảnh báo tức thì. Dễ dàng thêm output mới bằng cách tạo DLL plugin.
Web Dashboard: Xem preview tín hiệu từ các kênh đang giám sát qua trình duyệt. Hiển thị trạng thái real-time tất cả kênh, plugin đang hoạt động và cảnh báo hiện tại. Truy cập từ bất kỳ máy nào trong mạng nội bộ.
Alert History: Lưu trữ toàn bộ lịch sử cảnh báo vào SQLite. Xem lại và tra cứu qua web với bộ lọc theo kênh, loại lỗi (BlackVideo, Freeze, AudioSilence...) và khoảng thời gian.
Dashboard thống kê lỗi: Biểu đồ tần suất lỗi theo kênh và loại, kênh có nhiều sự cố nhất, xu hướng chất lượng DOVER Score theo thời gian.
IOutput interface: SetInputFrame() nhận frame từ capture thread, TriggerAlert() nhận AlertEvent, ClearAlert() khi tín hiệu trở lại bình thường. Tất cả phương thức phải thread-safe.
TelegramAlert: Gửi tin nhắn Telegram Bot API khi có AlertEvent. Tin nhắn kèm: tên plugin phát hiện, thông điệp mô tả, mức độ nghiêm trọng (severity 0.0-1.0), timestamp.
Mở rộng: Thêm output mới chỉ cần tạo DLL implement IOutput, export CreatePlugin() / DestroyPlugin(), thêm vào mảng "outputs" trong appsettings.json. Không cần sửa đổi Core hay các plugin khác.
Web Dashboard
Preview tín hiệu qua trình duyệt
Giám sát real-time tất cả kênh
Alert History - tra cứu lịch sử
Dashboard thống kê lỗi
TelegramAlert
Interface: IOutput
Trigger: TriggerAlert(AlertEvent)
Clear: ClearAlert()
Thread-safe: Yes (mutex)
V2 - Kế hoạch
Email SMTP alert
SNMP Trap integration
Webhook HTTP generic
Report PDF tự động
Tính năng 10
Ghi Bằng Chứng Phát Sóng - HlsRecorder
Ghi toàn bộ output render của Multiview Monitor thành HLS (H.264 + .ts segments + .m3u8 playlist). Encode chạy trên dedicated thread - không bao giờ block render thread. Segment đặt tên theo timestamp, tự tạo thư mục theo tháng.
HLS output: Muxer HLS của FFmpeg tạo file stream.m3u8 (playlist) và các segment .ts đặt tên theo timestamp YYYYMMDD_HHMMSS.ts. Playlist giữ 5 segment gần nhất (hls_list_size=5).
H.264 encoder: Encode RGBA frames từ render output sang YUV420P qua swscale, rồi encode H.264. Hỗ trợ CRF mode (chất lượng cố định, default CRF=23) hoặc bitrate mode (crf=-1). Preset mặc định veryfast để giảm tải CPU.
Dedicated encode thread: PushFrame() chỉ đẩy frame vào queue (tối đa 8 frame) rồi trả về ngay - không block render thread. Encode thread xử lý bất đồng bộ. Frame bị drop khi queue đầy thay vì block.
Tổ chức thư mục theo tháng: Segment lưu vào outputDir/YYYY_MM/YYYYMMDD_HHMMSS.ts. Tự động tạo thư mục tháng hiện tại và tháng tiếp theo trước khi cần, kiểm tra mỗi 5 phút.
Keyframe mỗi 2 giây: gop_size = frameRate × 2, không có B-frame (max_b_frames=0) để đảm bảo seeking chính xác và độ trễ thấp khi phát lại bằng chứng.
Flush an toàn khi dừng: Stop() flush encoder (gửi nullptr frame), ghi HLS trailer, đóng tất cả FFmpeg context theo đúng thứ tự - không mất dữ liệu segment cuối.
Cấu trúc thư mục output
recordings/
  stream.m3u8 (playlist)
  2026_04/
    20260418_190000.ts
    20260418_190100.ts
  2026_05/ (tạo trước)
Tham số cấu hình
outputDir: thư mục gốc
segmentSeconds: 60s/segment
width × height: 1280×720
crf: 23 (0=lossless, 51=worst)
preset: veryfast/fast/medium
frameRate: 25 fps
Thread model
Render thread: PushFrame() - non-blocking
Encode thread: RGBA→YUV→H.264→HLS
Queue: tối đa 8 frame
Drop policy: drop khi queue đầy
Stop: flush + write trailer
HLS + H.264 Segment 60s mặc định Non-blocking encode thread Tổ chức theo tháng