視頻線上率統計——基於驅動匯流排設備的領域驅動設計方法落地 [toc] 1.應用背景 本司智能信息箱產品是管控攝像頭電源,監控攝像頭視頻線上率的一個有效運維工具。因為統計視頻線上率是業主十分關心的問題,所以如何有效地統計視頻線上率是工程師需要努力解決的問題。 2.各視頻線上率統計方法比較 |方案|是否 ...
目錄
- 視頻線上率統計——基於驅動匯流排設備的領域驅動設計方法落地
- 1.應用背景
- 2.各視頻線上率統計方法比較
- 3.本文側重點
- 4.基於領域驅動來設計攝像頭網路狀態這一領域
- 5.依賴註入CameraPingBus,窗體程式析構法調用
- 6.匯流排,驅動,設備
- 7.多線程併發ping攝像頭效果圖
- 8. 小結
- 9.最終
視頻線上率統計——基於驅動匯流排設備的領域驅動設計方法落地
1.應用背景
本司智能信息箱產品是管控攝像頭電源,監控攝像頭視頻線上率的一個有效運維工具。因為統計視頻線上率是業主十分關心的問題,所以如何有效地統計視頻線上率是工程師需要努力解決的問題。
2.各視頻線上率統計方法比較
方案 | 是否需要攝像頭密碼 | 是否能與攝像頭交互信息 | 是否能知道攝像頭的網路狀態 |
---|---|---|---|
ping | 否 | 否 | 是 |
onvif | 是 | 是 | 是 |
ffmpeg | 是 | 是 | 是 |
ping,onvif,ffmpeg三種協議應用場合不同,各有優劣。onvif會多出用戶名,密碼欄位,方法上會多StartStreaming,StopStreaming,及識別視頻的編碼解析度等信息,從而從媒體信息地址URI獲取視頻流;ffmpeg則更進一步,可以直接調用方法分析視頻質量等等。
3.本文側重點
這裡需要聲明本文側重點有兩方面:
- 面向領域編程,不面向資料庫,下文會做詳解;
- 第2點的三種協議都可以借鑒linux的設備,匯流排,驅動與攝像頭之間的交互協議設計思想,只是創建子領域對象時onvif會多用戶名,密碼欄位,方法上會多StartStreaming,StopStreaming。ffmpeg則可以直接將需要的方法包裝到子領域進行調用。因此本文側重講解設備,匯流排,驅動來開發與硬體設備交互的思想。
4.基於領域驅動來設計攝像頭網路狀態這一領域
4.1 值對象driverContext
driverContext是用來配置ping驅動軟體(new System.Net.Ping())介面正常工作的上下文配置。這裡主要是interval,timeout,minSuccess三個欄位,其中timeout是驅動內配置,interval,minSuccess為驅動外配置,下文6.1中會有詳細舉例,按下不表。
3個欄位含義詳見註釋。
public class PingDriverContext
{
///可以增加name欄位,表示驅動名稱。
/// <summary>
/// ping間隔
/// </summary>
public int interval { get; set; }
/// <summary>
/// ping超時時間
/// </summary>
public int timeout { get; set; }
/// <summary>
/// ping成功最小次數
/// </summary>
public int minSuccess { get; set; }
}
4.2 子領域CameraPingDM
Camera表示攝像頭,ping表示檢查攝像頭網路狀態的驅動,DM表示Domain Model即領域模型。
4.2.1 枚舉類型攝像頭網路狀態CameraState
public enum CameraState
{
Online = 0,
Offline = 1,
Unknown = 2
}
4.2.2 屬性
private PingDriverContext _driverContext;
private Timer _timer;
private readonly object _lock = new object();
private readonly ReadOnlyMemory<byte> _buffer;
public int CurrentSuccess { get; private set; }
/// <summary>
/// IP地址
/// </summary>
public string Ip { get; set; }
///// <summary>
///// 狀態
///// </summary>
private CameraState cameraState;
public CameraState CameraState
{
get { return cameraState; }
set { cameraState = value; }
}
/// <summary>
/// 攝像頭狀態更新時間
/// </summary>
public DateTime UpdateTime { get; set; }
- _driverContext
值對象,每個攝像頭的timeout,interval,minSuccess都可以配置不同,這裡在匯流排類文件里寫死成
interval = 3,//每個攝像頭間隔3秒ping一次
timeout = 200,//單次ping等待返回結果200ms超時
minSuccess = 0,//一次ping成功即可認為online
- _lock
多線程併發互斥鎖 - _buffer
ping發送的數據包。 - CurrentSuccess
當前ping成功次數,根據minSuccess進行適當的計數清零。 - cameraState
網路狀態,詳見4.2.1 - UpdateTime
攝像頭狀態更新時間。
4.2.3 子領域的劃分
4.2.3.1析構體
在析構體中,主要傳入創建子領域對象所必須的參數。
#region Constructors
/// <summary>
/// 根據IP,以及driverContext創建攝像頭領域模型
/// </summary>
/// <param name="driverContext">聚合CameraPingBus中的驅動上下文driverContext</param>
/// <param name="iP">資料庫中的攝像頭的IP地址</param>
public CameraPingDM(PingDriverContext driverContext, string iP)
{
string data = "ping-pong";
_buffer = Encoding.ASCII.GetBytes(data);
Ip = iP;
_driverContext = driverContext;
LoggerHelper.Debug($"Ping camera IPList every {_driverContext.interval}s");
}
//Dapper數據模型需要
public CameraPingDM(){ }
#endregion
4.2.3.2 創建子領域對象
這裡就是傳入所需要的參數直接new子領域對象。一般是直接調用,所以它是靜態方法。
=>這裡是lambda表達式的語法糖,表示返回一個創建好的CameraPingDM子領域對象。
#region Creations
public static CameraPingDM Create(PingDriverContext driverContext, string iP) => new CameraPingDM(driverContext, iP);
#endregion
4.2.3.3 子領域對象內的修改屬性行為
主要表現為修改屬性值等。這裡CameraStateUpdate方法更新攝像頭網路狀態,同時保存更新時間。
#region Behaviors
/// <summary>
/// 更新攝像頭網路狀態,同時保存更新時間
/// </summary>
/// <param name="_cameraState"></param>
public void CameraStateUpdate(CameraState _cameraState)
{
cameraState = _cameraState;
UpdateTime = DateTime.Now;
}
#endregion
4.2.3.4 攝像頭的網路驅動——ping驅動相關的行為
#region Behaviors with Ping
/// <summary>
/// 表示為ping單個攝像頭,檢查其網路狀態。
/// </summary>
/// <returns></returns>
public async Task<bool> Start()
{
if (_driverContext.interval >= 0)
{
var interval = Convert.ToInt32(TimeSpan.FromSeconds(_driverContext.interval).TotalMilliseconds);
_timer = new Timer(state =>
{
lock (_lock)
{
DoPing();
}
}, null, interval, interval);
}
return true;
}
/// <summary>
/// 根據ping對應IP返回的結果來對當前ping成功次數計數,滿足要求為online,否則為offline。
/// </summary>
private void DoPing()
{
var pingSender = new Ping();
var options = new PingOptions
{
//不分包
DontFragment = true
};
try
{
PingReply reply = pingSender.Send(IPAddress.Parse(Ip), _driverContext.timeout, _buffer.ToArray(), options);
LoggerHelper.Debug($"Ping reply for {Ip} is {reply.Status}");
if (reply?.Status == IPStatus.Success)
{
Increment();
}
else
{
Decrement();
}
}
catch (Exception)
{
LoggerHelper.Debug($"Ping reply for {Ip} failed");
Decrement();
}
}
/// <summary>
/// 當前ping成功次數CurrentSuccess減1,CurrentSuccess為非負數
/// </summary>
private void Decrement()
{
if (CurrentSuccess <= 0)
{
CurrentSuccess = 0;
CameraStateUpdate(CameraState.Offline);
}
else
{
CurrentSuccess--;
}
}
/// <summary>
/// 當前ping成功次數CurrentSuccess+1,如果大於等於設置的最小ping成功次數,則更新攝像頭的網路狀態
/// </summary>
private void Increment()
{
if (CurrentSuccess >= _driverContext.minSuccess)
{
CameraStateUpdate(CameraState.Online);
}
else
{
CurrentSuccess++;
}
}
/// <summary>
/// 定時ping定時器關閉
/// </summary>
/// <returns></returns>
public async Task<bool> Stop()
{
_timer?.Dispose();
return true;
}
#endregion
所有方法的作用詳見註釋,不明白的可以在評論區評論,我會耐心解答,有更好建議的懇請提出。
這裡上層聚合CameraPingBus主要調用的就是Start()表示開始ping對應ip的攝像頭,根據ping結果刷新攝像頭網路狀態更新時間;Stop()方法停止ping。這裡Timer定時器會在4.3.5中詳細介紹,按下不表。
4.3 聚合CameraPingBus
也可稱之為CameraPingBus領域,也就是需要我們去解決與攝像頭協議交互查看攝像頭是否線上的問題域。領域是從需要解決的問題域命名,聚合是從功能角度命名,該類是聚合了許多子領域CameraPingDM,它是去ping 攝像頭Camera的行為,返回的是online/offline網路狀態值,通過子領域聚合而解決了一整個問題域。
4.3.1 屬性
private Timer _dbTimer;
ICamera_Services _camera_Services;
public IList<CameraPingDM> CameraPingDMList = new List<CameraPingDM>();
///可以增加name欄位,表示驅動名稱。
///寫一個IP地址 對應狀態變化的方法,將有變化的ADD進差異集合。 如果差異集合不為空,再保存進資料庫。
///通過winform修改pingDriverContext 3個參數
//預設參數5/100/0
static PingDriverContext pingDriverContext = new PingDriverContext()
{
interval = 3,
timeout = 200,
minSuccess = 0,
};
- _dbTimer
定時器,資料庫定時4秒保存一下攝像頭的網路狀態。 - _camera_Services
攝像頭的資料庫數據模型讀寫服務,依賴註入,析構體調用,簡單,如果對依賴註入有疑問可以參照筆者的在net Core3.1上基於winform實現依賴註入實例,這裡不做贅述。 - CameraPingDMList
ping攝像頭子領域的集合,也就是將所有的CameraPingDM子領域掛載到了Bus匯流排上。可通過該集合調用CameraPingDM子領域的ping Start, Stop方法。 pingDriverContext
值對象,這裡將所有攝像頭的ping驅動配置為同樣參數去創建CameraPingDM子領域對象。4.3.2 析構函數
析構函數調用_camera_Services
public CameraPingBus(ICamera_Services camera_Services)
{
_camera_Services = camera_Services;
}
4.3.3 與CameraPingDM所有子領域相關的行為
#region Behaviors with all the CameraPingDMList
/// <summary>
/// 從資料庫的數據模型獲取所有攝像頭的IP地址載入到CameraPingDM對象集合,由Dapper完成數據模
/// 型的IP到CameraPingDM領域模型IP賦值的轉換工作,啟動所有CameraPingDM對象集合的ping方法
/// </summary>
/// <returns></returns>
public async Task<bool> CreateAndStartAllCameraPing()
{
var CameraIpList = await GetCameraIpList();
try
{
foreach (var item in CameraIpList)
{
var cameraPingDM = CameraPingDM.Create(pingDriverContext, item.Ip);
await cameraPingDM.Start();
CameraPingDMList.Add(cameraPingDM);
}
}
catch { return false; }
return true;
}
/// <summary>
/// 停止所有CameraPingDM對象集合的ping方法
/// </summary>
/// <returns></returns>
public async Task<bool> StopPing()
{
try
{
foreach (var item in CameraPingDMList)
{
await item.Stop();
}
}
catch { return false; }
return true;
}
/// <summary>
/// 非同步獲取CameraPingDM對象集合元素數量
/// </summary>
/// <returns></returns>
public async Task<int> CameraIpCount()
{
var CameraIpList = await _camera_Services.GetAllCameraIPAsync();
return CameraIpList.Count();
}
#endregion
所用方法的作用我都做了詳細的註釋,詳見註釋。有問題可在評論區提出,我會耐心解答。
4.3.4 領域模型欄位在資料庫中的讀寫行為
#region Behaviors with DataBase
/// <summary>
/// 從資料庫中載入所有攝像頭IP地址到CameraPingDM的IP欄位
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<CameraPingDM>> GetCameraIpList()
{
return await _camera_Services.GetAllCameraIPAsync();
}
/// <summary>
/// 將所有Cameara的線上狀態根據IP地址匹配定時5秒更新到資料庫
/// </summary>
/// <returns></returns>
public async Task<bool> Save2DbTimerStart()
{
_dbTimer = new Timer(state =>
{
_camera_Services.UpdateList(CameraPingDMList);
}, null, 4000, 4000);
return true;
}
/// <summary>
/// 關閉資料庫定時保存定時器
/// </summary>
/// <returns></returns>
public async Task<bool> Save2DbTimerStop()
{
_dbTimer?.Dispose();
return true;
}
#endregion
所用方法的作用我都做了詳細的註釋,詳見註釋。有問題可在評論區提出,我會耐心解答。
這裡需要註意的是由Dapper完成數據模型的IP到CameraPingDM領域模型IP賦值的轉換工作,保存也是由Dapper進行了從領域模型的IP,CameraState到數據模型的無縫對接,礙於篇幅過長,時間也很晚了,感興趣的請在評論區留言。筆者將根據讀者反饋情況看是否有必要另起一篇,寫一下基於Dapper進行數據模型與領域模型之間的互相轉換。
4.3.5 定時器timer介紹
4.3.5.1 定義一個定時器的引用類
用來指向下麵的定時器實例。
using System.Threading;
private Timer _dbTimer;
4.3.5.2 定時器使用
定時器的引用類型指向new Timer()實例,目的是為了去寫定時器的關閉方法。
_dbTimer = new Timer(state =>
{
_camera_Services.UpdateList(CameraPingDMList);
}, null, 4000, 4000);
這裡定時器有4個參數,F12可得如下
// callback:
// A System.Threading.TimerCallback delegate representing a method to be executed.
//
// state:
// An object containing information to be used by the callback method, or null.
//
// dueTime:
// The amount of time to delay before callback is invoked, in milliseconds. Specify
// System.Threading.Timeout.Infinite to prevent the timer from starting. Specify
// zero (0) to start the timer immediately.
//
// period:
// The time interval between invocations of callback, in milliseconds. Specify System.Threading.Timeout.Infinite
// to disable periodic signaling.
- 第一個參數callback即回調函數,也就是定時執行的方法;
- 第二個參數state回調函數的包含信息,這裡為null即可;
- 第三個參數dueTime,定時器啟動之後延遲調用回調函數的毫秒數;
第四個參數period定時周期。
4.3.5.3 定時器的關閉
_dbTimer?.Dispose();
4.3.5.4 與Java的調度器ScheduledExecutorService相比
熟悉Java的道友有沒有發現,C#里的Timer與Java的ScheduledExecutorService很相似,也不知道是誰抄誰,或者是異曲同工之妙吧。
import java.util.concurrent.ScheduledExecutorService;
private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(DeviceSetting.MAX_GROUP_ID);
executorService.scheduleWithFixedDelay(() -> {
try {
BaseMsg baseMsg = deque.take();
Thread.sleep(AWAKE_TO_PROCESS_INTERVAL);
Channel channel = touchChannel(channelId);
if (channel == null || !channel.isActive()) {
logger.warn("「Channel」" + " Channel「" + channelId + "」無效,無法下發該幀");
removeChannelCompleted(channel);
deque.clear();
return;
}
}, channelId, CommSetting.FRAME_SEND_INTERVAL, TimeUnit.MILLISECONDS);
4.3.5.5 Timers調用庫的問題
註意:這裡必須要調用System.Threading庫里的定時器可以多線程併發執行回調方法,否則的話,將沒有此功能,System.Timers定時器使用較為複雜,且無法多線程併發,需要自己寫多線程併發的方法,System.Timers定時器只能提供定時功能。
5.依賴註入CameraPingBus,窗體程式析構法調用
5.1 CameraPingBus匯流排依賴註入
//Domain
services.AddScoped(typeof(CameraPingBus));
5.2 窗體程式析構法調用
public PingSetting(CameraPingBus cameraPingBus)
{
_cameraPingBus = cameraPingBus;
InitializeComponent();
LoggerHelper.Debug("視頻線上率配置工具啟動");
}
5.3 CameraPingBus使用實例
/// <summary>
/// 按下啟動按鈕執行操作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void button1_Click(object sender, EventArgs e)
{
//IP地址從資料庫數據模型賦值到領域模型的IP欄位,並且每隔3秒開始ping攝像頭,保存其網路狀態
await _cameraPingBus.CreateAndStartAllCameraPing();
//每隔4s將攝像頭網路狀態更新到IP地址相等的資料庫數據模型中去
await _cameraPingBus.Save2DbTimerStart();
}
/// <summary>
/// 按下停止按鈕執行操作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void button2_Click(object sender, EventArgs e)
{
//停止攝像頭定時ping行為
await _cameraPingBus.StopPing();
//停止保存攝像頭網路狀態到資料庫
await _cameraPingBus.Save2DbTimerStop();
}
各調用方法含義詳見註釋。
5.4 基於net Core3.1的winform工具效果圖
工具圖示如下:
6.匯流排,驅動,設備
6.1.驅動
驅動即軟體介面,ping是驅動,modbus協議庫也是驅動,驅動配置(driverContext)分為驅動內配置與驅動外配置。值對象驅動上下文driverContext就是包含了驅動內配置與驅動外配置。
6.1.1驅動內配置舉例
var pingSender = new Ping();
PingReply reply = pingSender.Send(IPAddress.Parse(Ip), _driverContext.timeout, _buffer.ToArray(), options);
_driverContext.timeout即為ping驅動內配置。
6.1.2驅動外配置舉例
if (CurrentSuccess >= _driverContext.minSuccess)
{
CameraStateUpdate(CameraState.Online);
}
_driverContext.minSuccess即為驅動外配置。
if (_driverContext.interval >= 0)
{
var interval = Convert.ToInt32(TimeSpan.FromSeconds(_driverContext.interval).TotalMilliseconds);
_timer = new Timer(state =>
{
lock (_lock)
{
DoPing();
}
}, null, interval, interval);
}
_driverContext.interval也為驅動外配置。
6.2 設備
攝像頭即設備,這裡驅動跟設備是1對1的關係,驅動是設備的一個被動行為,SC平臺通過載入驅動的所需配置(driverContext)來獲取對應設備的數據(信號或者說狀態)。
6.3 匯流排
匯流排就像高速公路,他需要有名稱,是否關閉,起點,終點,限速(介面參數)。所以這裡的IP地址就好比是終點地址,故這裡的攝像頭IP是屬於匯流排的概念範疇。
具體驅動協議的上一層,一根匯流排可以對應多個驅動,也可以對應多個設備。
6.4 類比
設備上的信號值(網路狀態值)相當於是要寄的快遞,驅動相當於是運快遞的車,保持車間距,按時到達終點,而匯流排相當於是車開著的高速路。
7.多線程併發ping攝像頭效果圖
7.1分析日誌記錄內容1時刻值
得到結果ping行為為併發
7.2分析日誌記錄內容2線程號T
得到結果為多線程ping
7.3分析日誌記錄內容3消息
ping成功與超時與實際線上離線IP地址結果相符。
8. 小結
- 如果設備端是Modbus協議類比可得:Modbus驅動需要載入它的配置信息(所屬匯流排ID,使能,驅動名稱,協議,設備地址,發送間隔,接收超時時間,接收超時告警次數,對應設備的寄存器地址等),也即driverContext,載入到Modbus的驅動,驅動配置會包含設備地址,每台設備都會有他自己的驅動配置,將Modbus驅動(也就是new ping())封裝到設備的方法裡面去(可以封裝成AI,DI,AO,DO),將這些配置信息裝載到設備的驅動方法里,即可從設備返回值,而新建信號時,就會對該值定義(也或者可以模板形式解析值的顯示含義),而這就是最頂層的用戶工作組,也就是最大的聚合,也可建立匯流排,將各類驅動進行分類。以後有時間也會分享相關的信息,不過會稍微複雜一些,但是道理思想類似。
一個項目中不一定只有一個聚合像我現在做的智能箱它就需要兩個聚合,就有兩個問題域
一個是智能設備箱(也就是點位),所有的AI,DI,DO,AO(包含歷史數據),攝像頭,A介面配置數據,用戶,角色,升級,運維公司,運維人員,區域,設備箱告警,協議模板,歷史告警都是它的子領域。
攝像頭也是一個聚合,攝像頭的告警(離線,停電告警),歷史告警,攝像頭的型號,攝像頭的廠商,區域,設備箱,運維公司,運維人員,攝像頭驅動,攝像頭匯流排都是攝像頭的子領域。
- driver與driverContext
driver就是Ping驅動。
var pingSender = new Ping();
驅動上下文driverContext的欄位(配置信息)會載入到驅動pingSender上去,去獲取所需要的值,即為軟體介面
PingReply reply = pingSender.Send(IPAddress.Parse(Ip), _timeout, _buffer.ToArray(), options);
9.最終
自己對於領域驅動設計的理解並不深刻,但是憑著對設備域,以及協議,匯流排,驅動的甚微瞭解,以及看了不少開源項目,不斷地學習同行的資料庫,硬生生地拼湊成了此文,可能有些概念上或者實現上會有不合適的地方,請路過的高手們不吝賜教。當然如果你有不明白的地方也請提出,我也會耐心解答。
本作品採用知識共用署名-非商業性使用-相同方式共用 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發佈,但務必保留文章署名JerryMouseLi(包含鏈接:https://www.cnblogs.com/JerryMouseLi/),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。如有任何疑問,請與我聯繫。
————————————————
版權聲明:本文為博客園博主「JerryMouseLi」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://www.cnblogs.com/JerryMouseLi/p/12381098.html