流媒体服务器、海康威视 大华摄像头实现视频监控、直播解决方案

  随着互联网+物联网进程的加快,视频监控应用领域变得越来越广泛,其中海康威视 大华等品牌的摄像头频繁出现在视野中。由于去年也实现过智慧工地项目上的视频监控方案,加上当今直播趋势不减。现在总结一下:

缘由:是1对N 点对多的直播方式, 一般都是采用服务器转发,所以此处不考虑WebRTC这种端对端的方式,WebRTC将在下一篇文章中讲解下实现思路。

前提:需要海康威视或大华的摄像头,大华摄像头清晰度 品质较好,但相对于海康的摄像头较贵,所以海康威视的摄像头更受口袋欢迎。

一.自建流媒体服务器

  第一种方式就是自建流媒体服务器,然后自己实现采集推流 到服务器 拉流到客户端播放。先看一张图:

  1. 先客户端软件或设备采集视频流和语音流,或者是摄像头硬件采集的画面流等(如何采集就属于硬件相关的问题了,此处不讨论)
  2. 然后通过推流的方式推到流媒体服务器,推流协议可以使用RTMP RMSP,这2种都是基于tcp的 不会丢包。但是很容易造成高延迟(具体的看服务器 网络 是否做CDN来支撑)。
    1 //可指定h264或h265编码,可以把h265编码看成是h264编码的升级版,在码率 体积 清晰度 移动补偿上更友好些  2 //大体结构为:rtsp://摄像头用户名:密码@地址:端口 服务器上地址参数...  3 rtsp://admin:[email protected]:554/h264/ch1/main/av_stream  4 rtsp://admin:[email protected]:554/Streaming/Channels/101?transportmode=unicast

    以上方式只是实现了流推送到了服务器,并没有指定它播放地址以及播放的转码。因此我们可以考虑使用ffmpeg,这是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。也就是使用ffmpeg不光可以本地采集流还可以指定推送到那一台服务器上和它的播放地址等等;

    1 //ffmpeg -re -i表示使用的协议和协议的参数,具体的参数意义请百度  2 //接着是和上面一样的推流,这里使用的是rtsp,建议用rtmp,本帅在使用中感觉rtmp兼容性更好 web前端使用rtmp更方便。比如前端用Flash插件。或者Video标签等等。  3 //然后是基于tcp 转码 播放的地址,比如播放地址是:rtsp://117.250.250.250/Cameratest  4 ffmpeg -re -i rtsp://admin:[email protected]:554/h264/ch1/main/av_stream -rtsp_transport tcp -vcodec h264 -f rtsp rtsp://localhost/test  5 ffmpeg -i rtsp://admin:[email protected]:554/h264/ch1/main/av_stream -rtsp_transport tcp -vcodec h264 -f rtsp rtsp://117.250.250.250/Cameratest

     注意播放地址前指定播放协议,比如rtsp rtsp://117.250.250.250/Cameratest。如果是rtmp那么最后就应该是:rtmp rtmp://117……………………

  3. 流媒体服务器做一些编码转码处理等将流分发给各个客户端,进而进行拉流播放。那么问题来了  如何实现流媒体服务器呢?如何架设???
  4. 架设上我们可以使用nginx rtmp-module模块来架设,架设好后就可以使用rtmp推流给它。还可以用上面第2点中的ffmpeg命令写一个bat脚本来测试摄像头和架设的流媒体。
  5. PC端播放使用rtmp Flash来进行播放(H5中的Video标签了解一下),移动端播放使用HLS m3u8 rtmp来进行播放(具体播放方式视项目框架情况而定)。看网上有人还使用flv + http stream 进行播放的。
  6. 后期出现了并发 播放量增多的压力可以把nginx做分层(接入层+交换层),或者是转发一下做负载均衡,或者CDN来支撑。前期如果考虑到后期会使用CDN也可以直接跳过nginx 一开始用cdn的直播服务。

二.接入第三方平台

  在之前的项目中是购买了海康威视的摄像头,所以为了方便快捷的开发,是接入了第三方平台,由第三方平台进行管理和转发。大体流程是往第三方平台注册摄像头信息(序列号 验证码),然后流直接走第三方平台,自己服务端只需要获取三方平台的API接口便能得知播放地址 直接客户端播放。使用的是萤石云

 

 

 

   我们可以在自己的项目中往萤石云注册摄像头信息(也就是调用萤石云接口 往萤石云写一条数据),然后在需要用的地方获取萤石云API接口播放地址。完全不用管流的处理(得充值)。

   提供一份对萤石云请求封装的类(C#代码):  

  1 using Newtonsoft.Json;    2 using System;    3 using System.Collections.Generic;    4 using System.Linq;    5 using System.Net.Http;    6 using System.Net.Http.Headers;    7 using System.Text;    8 using System.Threading.Tasks;    9 using YJT.Common;   10   11 /*20190819 by suzong */   12 namespace YJT.Wisdom.Api.lib   13 {   14     /// <summary>   15     /// 萤石云请求封装   16     /// </summary>   17     public class YsClient   18     {   19         private static readonly string requestUrl = "https://open.ys7.com/";   20         private static readonly string appKey = "";//官网注册获得   21         private static readonly string appSecret = "";//官网注册获得   22   23         /// <summary>   24         /// 获得token   25         /// </summary>   26         /// <returns>{code:200,data:{accessToken:"",expireTime:精确到毫秒}}</returns>   27         public static async Task<string> GetToken()   28         {   29             string key = ConfigHelper.GetSetting("CacheKey:YsToken") ?? "YsAccessToken";   30             string tokenStr = MemoryCacheHelper.Get(key)?.ToString();   31             if (string.IsNullOrEmpty(tokenStr))   32             {   33                 string str = await HttpHelper.HttpPostAsync($"{requestUrl}api/lapp/token/get?appKey={appKey}&appSecret={appSecret}");   34                 YsResult result = JsonConvert.DeserializeObject<YsResult>(str);   35                 //缓存token 缓存时间为5天   36                 tokenStr = result?.data?.accessToken;   37                 MemoryCacheHelper.Set(key, tokenStr, TimeSpan.FromDays(5));   38             }   39             return tokenStr;   40         }   41   42         /// <summary>   43         /// 添加设备   44         /// </summary>   45         /// <param name="deviceSerial">设备序列号</param>   46         /// <param name="validateCode">设备验证码</param>   47         /// <returns></returns>   48         public static async Task<YsResult> SaveDevice(string deviceSerial, string validateCode)   49         {   50             if (string.IsNullOrEmpty(deviceSerial) || string.IsNullOrEmpty(validateCode))   51                 return new YsResult() { code = "-1", msg = "缺少验证码或序列号" };   52   53             string str = await HttpHelper.HttpPostAsync($"{requestUrl}api/lapp/device/add?accessToken={GetToken().Result}&deviceSerial={deviceSerial.ToUpper()}&validateCode={validateCode.ToUpper()}");   54             return JsonConvert.DeserializeObject<YsResult>(str);   55         }   56   57         /// <summary>   58         /// 关闭视频加密   59         /// </summary>   60         /// <param name="deviceSerial">设备序列号</param>   61         /// <param name="validateCode">设备验证码</param>   62         /// <returns>{code:200}</returns>   63         public static async Task<YsResult> OffEncryption(string deviceSerial, string validateCode)   64         {   65             if (string.IsNullOrEmpty(deviceSerial) || string.IsNullOrEmpty(validateCode))   66                 return new YsResult() { code = "-1", msg = "缺少验证码或序列号" };   67             string str = await HttpHelper.HttpPostAsync($"{requestUrl}api/lapp/device/encrypt/off?accessToken={GetToken().Result}&deviceSerial={deviceSerial.ToUpper()}&validateCode={validateCode.ToUpper()}");   68             return JsonConvert.DeserializeObject<YsResult>(str);   69         }   70   71         /// <summary>   72         /// 删除设备   73         /// </summary>   74         /// <param name="token"></param>   75         /// <param name="deviceSerial">设备序列号</param>   76         /// <returns>{code:200}</returns>   77         public static async Task<YsResult> DeleteDevice(string deviceSerial)   78         {   79             if (string.IsNullOrEmpty(deviceSerial))   80                 return new YsResult() { code = "-1", msg = "缺少序列号" };   81   82             string str = await HttpHelper.HttpPostAsync($"{requestUrl}api/lapp/device/delete?accessToken={GetToken().Result}&deviceSerial={deviceSerial.ToUpper()}");   83             return JsonConvert.DeserializeObject<YsResult>(str);   84         }   85   86         /// <summary>   87         /// 获取直播地址 WSS地址 #get请求 每次获取   88         /// </summary>   89         /// <param name="token"></param>   90         /// <param name="deviceSerial">设备序列号</param>   91         /// <param name="validateCode">设备验证码</param>   92         /// <returns></returns>   93         public static async Task<string> GetPlayWss(string deviceSerial, string validateCode)   94         {   95             if (string.IsNullOrEmpty(deviceSerial) || string.IsNullOrEmpty(validateCode))   96                 return null;   97             //{"retcode":0,"msg":"成功","data":{"tokens":["ot.cadfwa3t0dkdn62x5qf257es7dbq1cie-1vwkltfwtz-1w1jc79-9eabx2bbz"],"params":"&auth=1&biz=4&cln=100"}}   98             string str = await HttpHelper.HttpGetAsync($"{requestUrl}jssdk/ezopen/getStreamToken?accessToken={GetToken().Result}&num=1&type=live");   99             YsResult result = JsonConvert.DeserializeObject<YsResult>(str);  100             if (result.retcode == 0)  101             {  102                 string tokensStr = result?.data?.tokens[0];  103                 string paramStr = result?.data["params"];  104                 //wss://jsdecoder.ys7.com:20006/live?dev=设备序列号&chn=1&stream=2&ssn=刚才获取的tokens[0]+刚才获取的params的字符串。作为wssUrl,此地址可以加上checkCode=验证码作为视频加密传输。  105                 return $"wss://jsdecoder.ys7.com:20006/live?dev={deviceSerial}&chn=1&stream=2&ssn={tokensStr}{paramStr}&checkCode={validateCode}";  106             }  107             return null;  108         }  109  110         /// <summary>  111         /// 获取直播地址 #返回RTMP地址  112         /// </summary>  113         /// <param name="token"></param>  114         /// <param name="deviceSerial">设备序列号</param>  115         /// <returns>返回rtmp</returns>  116         public static async Task<string> GetPlayRtmp(string deviceSerial)  117         {  118             if (string.IsNullOrEmpty(deviceSerial))  119                 return null;  120             string str = await HttpHelper.HttpPostAsync($"{requestUrl}api/lapp/live/address/get?accessToken={GetToken().Result}&source={deviceSerial}:1");  121             YsResult result = JsonConvert.DeserializeObject<YsResult>(str);  122             if (result.code.Equals("200"))  123                 return result?.data[0]?.rtmp;  124             return null;  125         }  126  127         /// <summary>  128         /// 获取设备可有的权限  129         /// </summary>  130         /// <param name="token"></param>  131         /// <param name="deviceSerial">设备序列号</param>  132         /// <returns>data:  133         ///{  134         ///    supprot_encrypt 是否支持视频图像加密 0 - 不支持, 1 - 支持  135         ///    support_modify_pwd 是否支持修改设备加密密码: 0 - 不支持, 1 - 支持  136         ///    ptz_top_bottom 是否支持云台上下转动 0 - 不支持, 1 - 支持  137         ///    ptz_left_right 是否支持云台左右转动 0 - 不支持, 1 - 支持  138         ///    ptz_45 是否支持云台45度方向转动 0 - 不支持, 1 - 支持  139         ///    ptz_zoom 是否支持云台缩放控制 0 - 不支持, 1 - 支持  140         ///    ptz_focus 是否支持焦距模式 0 - 不支持, 1 - 支持  141         ///}code: 200  142         /// </returns>  143         public static async Task<YsResult<YsRoles>> GetDeviceRole(string deviceSerial)  144         {  145             if (string.IsNullOrEmpty(deviceSerial))  146                 return new YsResult<YsRoles>() { code = "-1", msg = "缺少序列号" };  147  148             string str = await HttpHelper.HttpPostAsync($"{requestUrl}api/lapp/device/capacity?accessToken={GetToken().Result}&deviceSerial={deviceSerial.ToUpper()}");  149             return JsonConvert.DeserializeObject<YsResult<YsRoles>>(str);  150         }  151  152         /// <summary>  153         /// 云台控制开始  154         /// </summary>  155         /// <param name="token"></param>  156         /// <param name="deviceSerial">设备序列号</param>  157         /// <param name="direction">方向 (操作命令:0 - 上,1 - 下,2 - 左,3 - 右,4 - 左上,5 - 左下,6 - 右上,7 - 右下,8 - 放大,9 - 缩小,10 - 近焦距,11 - 远焦距)</param>  158         /// <param name="speed">速度 (云台速度:0 - 慢,1 - 适中,2 - 快)</param>  159         /// <returns>{code:200}</returns>  160         public static async Task<YsResult> CradleControlStarts(string token, string deviceSerial, int direction, int speed)  161         {  162             if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(deviceSerial))  163                 return null;  164             string str = await HttpHelper.HttpPostAsync($"{requestUrl}api/lapp/device/ptz/start?accessToken={token}&deviceSerial={deviceSerial}&channelNo=1&direction={direction}&speed={speed}");  165             return JsonConvert.DeserializeObject<YsResult>(str);  166         }  167  168         /// <summary>  169         /// 云台控制结束  170         /// </summary>  171         /// <param name="token"></param>  172         /// <param name="deviceSerial">设备序列号</param>  173         /// <param name="direction">方向 (操作命令:0-上,1-下,2-左,3-右,4-左上,5-左下,6-右上,7-右下,8-放大,9-缩小,10-近焦距,11-远焦距)</param>  174         /// <returns>{code:200}</returns>  175         public static async Task<YsResult> CradleControlEnd(string token, string deviceSerial, int direction)  176         {  177             if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(deviceSerial))  178                 return null;  179             string str = await HttpHelper.HttpPostAsync($"{requestUrl}api/lapp/device/ptz/stop?accessToken={token}&deviceSerial={deviceSerial}&channelNo=1&direction={direction}");  180             return JsonConvert.DeserializeObject<YsResult>(str);  181         }  182  183         /// <summary>  184         /// 获取单个设备信息  185         /// </summary>  186         /// <param name="token"></param>  187         /// <param name="deviceSerial">设备序列号</param>  188         /// <returns></returns>  189         public static async Task<YsResult> GetDeviceInfo(string deviceSerial)  190         {  191             if (string.IsNullOrEmpty(deviceSerial))  192                 return null;  193             string str = await HttpHelper.HttpPostAsync($"{requestUrl}api/lapp/device/info?accessToken={GetToken().Result}&deviceSerial={deviceSerial}");  194             YsResult result = JsonConvert.DeserializeObject<YsResult>(str);  195             if (result.code.Equals("200"))  196                 return result;  197             return null;  198         }  199  200         /// <summary>  201         /// 开通直播功能  202         /// </summary>  203         /// <param name="deviceSerial">设备序列号</param>  204         /// <returns></returns>  205         public static async Task<YsResult> LiveOpen(string deviceSerial)  206         {  207             if (string.IsNullOrEmpty(deviceSerial))  208                 return null;  209             string str = await HttpHelper.HttpPostAsync($"{requestUrl}api/lapp/live/video/open?accessToken={GetToken().Result}&source={deviceSerial}:1");  210             return JsonConvert.DeserializeObject<YsResult>(str);  211         }  212  213  214     }  215  216     /// <summary>  217     /// 萤石云返回对象  218     /// </summary>  219     public class YsResult<T>  220     {  221         public string code { get; set; }  222         public T data { get; set; }  223         public string msg { get; set; }  224         public int retcode { get; set; }  225     }  226     public class YsResult : YsResult<dynamic>  227     {  228     }  229  230     /// <summary>  231     /// 萤石云设备能力集  232     /// </summary>  233     public class YsRoles  234     {  235         /// <summary>  236         /// 是否支持视频图像加密 0 - 不支持, 1 - 支持  237         /// </summary>  238         public int supprot_encrypt { get; set; } = 0;  239         /// <summary>  240         /// 是否支持修改设备加密密码: 0 - 不支持, 1 - 支持  241         /// </summary>  242         public int support_modify_pwd { get; set; } = 0;  243         /// <summary>  244         /// 是否支持云台上下转动 0 - 不支持, 1 - 支持  245         /// </summary>  246         public int ptz_top_bottom { get; set; } = 0;  247         /// <summary>  248         /// 是否支持云台左右转动 0 - 不支持, 1 - 支持  249         /// </summary>  250         public int ptz_left_right { get; set; } = 0;  251         /// <summary>  252         /// 是否支持云台45度方向转动 0 - 不支持, 1 - 支持  253         /// </summary>  254         public int ptz_45 { get; set; } = 0;  255         /// <summary>  256         /// 是否支持云台缩放控制 0 - 不支持, 1 - 支持  257         /// </summary>  258         public int ptz_zoom { get; set; } = 0;  259         /// <summary>  260         /// 是否支持焦距模式 0 - 不支持, 1 - 支持  261         /// </summary>  262         public int ptz_focus { get; set; } = 0;  263     }  264  265 }

萤石云请求封装

 

三.使用开源流媒体框架

   开源流媒体框架就很多了,常见的SRS国产的。安装 推流 拉流。可用于直播/录播/视频客服等多种场景,其定位是运营级的互联网直播服务器集群。传送门:http://www.ossrs.net/srs.release/releases/ 喜欢的可以自己去了解了解。


提醒:你所购买的摄像头硬件上都会有摄像头的名称 序列号 验证码信息,摄像头厂商比如海康会有搜索局域网内摄像头的一个工具(官网去找)。一个Web界面的后台用于设置摄像头通道 配置信息等,在局域网内连接上摄像头 浏览器地址栏输入对应的地址就可以登录当前摄像头后台。

附赠几个rtsp rtmp免费测试地址(可以先让前端用这些地址先实现播放功能):

1 rtsp://184.72.239.149/vod/mp4:BigBuckBunny_175k.mov  2 rtsp://195.200.199.8/mpeg4/media.amp  3 rtmp://media3.sinovision.net:1935/live/livestream

 End…