視頻在線率統計——基於驅動總線設備的領域驅動設計方法落地

視頻在線率統計——基於驅動總線設備的領域驅動設計方法落地

1.應用背景

本司智能信息箱產品是管控攝像頭電源,監控攝像頭視頻在線率的一個有效運維工具。由於統計視頻在線率是業主十分關心的問題,因此如何有效地統計視頻在線率是工程師須要努力解決的問題。java

2.各視頻在線率統計方法比較

方案 是否須要攝像頭密碼 是否能與攝像頭交互信息 是否能知道攝像頭的網絡狀態
ping
onvif
ffmpeg

ping,onvif,ffmpeg三種協議應用場合不一樣,各有優劣。onvif會多出用戶名,密碼字段,方法上會多StartStreaming,StopStreaming,及識別視頻的編碼分辨率等信息,從而從媒體信息地址URI獲取視頻流;ffmpeg則更進一步,能夠直接調用方法分析視頻質量等等。linux

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個字段含義詳見註釋。api

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子領域對象。app

#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 版權協議,轉載請附上原文出處連接及本聲明。
原文連接: http://www.javashuo.com/article/p-vasbljds-gu.html
相關文章
相關標籤/搜索