热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

C#实现通过ffmpeg拉取海康摄像头rtsp流转m3u8,并在前端播放的解决方案。

前言:最近拿到一个需求,客户希望在公司自研系统上能够播放他们自己安装的(海康)摄像头实时画面。刚开始跟老大一起技术选型定的是easydarwin流媒体服务器,去github查看

前言:

  最近拿到一个需求,客户希望在公司自研系统上能够播放他们自己安装的(海康)摄像头实时画面。刚开始跟老大一起技术选型定的是easydarwin 流媒体服务器,去github查看文档然后测试发现拉流非常不稳定经常容易断。随着深入了解发现easydarwin内部本身还是依托的ffmpeg,只不过做了一个前台页面对拉流进程进行了管理,然后提供了接口供调用,在很多情况下并不能满足当前 的需求。所以最终决定还是用自己熟悉的C#去实现这个需求。

  查阅资料 常见网络摄像机(摄像头)的端口及RTSP地址发现 海康摄像头视频信号是rtsp流的,C# 调用ffmpeg 拉取rtsp流生成m3u8文件,然后通过js播放m3u8文件。大概思路是这样,不过中间还是踩了很多坑,自己在这里总结一下。

一些下载地址资源:

  ffmpeg下载地址:https://pan.baidu.com/s/1n1t7rmal9LnQY8Ngp5YcNA 提取码:vncv

  海康客户端 IVMS-4200 V3,3,1,8下载 :   https://www.hikvision.com/cn/download_more_390.html#prettyPhoto

  VCL播放器 :自己去百度下一个 

开发前准备工作: 

1. 配置ffmpeg环境变量,将ffmpeg.exe的路径配入Path环境变量(为了减少篇幅,不懂的麻烦自行百度)

百度的时候, C#通过 Process 命令调用进程网上很多都说直接在命令行里面写exe全路径等等后面发现都无效,所以最省事的办法就是直接配到环境变量里面去。

2. 明确摄像头rtsp地址各个参数的含义.(上文中有链接,这里在强调一下)

例如:rtsp://admin:KTTHVE@192.168.137.239:554/h265/ch1/main/av_stream

1) admin / KTTHVE:摄像头的账号/密码

2)192.168.137.239:摄像头所连接的wifi ip地址, 554 默认端口号

3)h265编码方式

4)ch1 通道1,如果摄像头为热成像摄像头则一般会有两个通道。分别对应普通画面和热成像画面

5)main主码流,sub子码流

验证rtsp是否正确的途径之一就是用上面下载的VCL播放器播放,如果能播放那么就正确(这句话其实有个坑,后文关于热成像画面的时候我们会补充)

特别注意:

1) 如果你的VCL播放器的PC端 IP 和摄像头连得IP不是同一局域网那么默认是访问不到的(当时我就在这卡了很久,所以网络也是有必要好好学的)。其实最简单的方法是通过 cmd ping摄像头的ip 能否ping通,如果ping不通就只能找公司网络工程师解决了。

2) 关于如何查到摄像头的ip, 可以通过海康客户端,搜索可连接设备,然后就可以看到了。

点击设备管理->在线设备->双击记录可以看到摄像头的mac地址和ip

  

 

 

 

 

 

 

 

 

3. 新建windows服务 我这里用的是VS2015版本(第一版的时候只想到用控制台程序手动启动。后面第一次部署到客户服务器的时候客户那边的技术就建议做成服务。万一服务器宕机重启就不用手动启动exe,而且避免了人为误关控制台窗口造成程序中止的问题。所以第二版就改进了一下,确实说的有道理,没有考虑到这个问题,感谢对方的宝贵建议)

3.1 文件->新建项目->...经典桌面-> window服务

 

3.2 完成后将 HlsService1.cs 重命名为HlsService.cs

3.3 如图所示 点击HlsService 编辑代码

 

 3.4 添加windows安装程序

 

双击HlsService.cs 在右边空白处右键 -》添加安装程序

然后双击ProjectInstaller.cs,分别点击serviceProcessInstaller1,serviceInstaller1

 

属性分别配置如下:

 

 3.5 接下来贴代码:HlsService代码

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.ServiceProcess;
using System.Threading;

namespace HlsService
{
    public partial class HlsService : ServiceBase
    {
        public class ReceiveInfo
        {
            public string OnlineRootAddress { get; set; }//外网ip+端口路径 

            public int IsStartCheck { get; set; }   //是否开启定时检查

            public int IsRecordLog { get; set; }    //重启拉流是否记录日志

            //WaitTimeBeforeCheck / LoopPeriodSeconds / OutdateSecends 需要根据服务器cpu等性能 适当调节,电脑配置高可以小点,配置低调大点

            //开启服务 多少秒后开启定时检查 摄像头比较多需要所有的都开启了拉流 才进行检查
            public int WaitTimeBeforeCheck { get; set; }
            //定时器循环周期 (秒) 摄像头拉流需要一定时间 否则时间太断 在进行重启检查时  在拉到流之前 文件永远都是过期
            public int LoopPeriodSeconds { get; set; }
       
       //判断过期的文件名 
public string JudgeFileName { get; set; } //判断文件过期时间 (秒) 切片3秒1个 30个切片 2分钟足够判断过期 public int OutdateSecends { get; set; }        
       //摄像头配置信息 
public List ConfigInfos { get; set; } } public class ConfigInfo { public int CameraId { get; set; } //摄像头id 从1开始递增 public string CameraName { get; set; } //摄像头名称 (对应视频监控添加 标题) public string OutDirName { get; set; } //摄像头推流生成的m3u8文件存放目录名 public string MacAddress { get; set; } //通过mac地址到时候可以方便摄像头所连接的wifi ip public string RtspPath { get; set; } //摄像头rtsp地址 public int ProcessId { get; set; } //摄像头对应的 ffmpeg推流进程ID public string PlayUrl { get; set; } //可播放的m3u8 http地址 (对应视频监控添加 url) } public static string OnlineRootAddress= "";//"test.kingnen.com:12400/";外网ip端口 public static string COnfigFileName= "config.json";//摄像头配置文件名称 public static string BasePath = AppDomain.CurrentDomain.BaseDirectory;//HLSTransfer所在文件夹路径 public static string M3u8FileBaseDir = "FileDir"; //m3u8文件存放 根目录 public static string M3u8FileName = "play.m3u8"; //名称统一为play.m3u8 public static ReceiveInfo receiveInfo = null; //配置文件接收类 public static System.Threading.Timer timer; //定时器 public HlsService() { InitializeComponent(); base.ServiceName = "HlsService"; } protected override void OnStart(string[] args) { MainStart(); } protected override void OnStop() { timer?.Dispose(); StopAllProcess(); } /// /// 主方法 /// /// static void MainStart() { if (!ReadConfigFile(BasePath + ConfigFileName)) return; StopAllProcess(); //首先关闭之前所有的拉流 OnlineRootAddress = receiveInfo.OnlineRootAddress;//设置外网ip 端口 foreach (var item in receiveInfo.ConfigInfos) { item.ProcessId = 0; item.PlayUrl = ""; item.ProcessId = StartPull(item); //重启推流 item.PlayUrl = item.ProcessId > 0 ? (OnlineRootAddress + M3u8FileBaseDir + "/" + item.OutDirName + "/" + M3u8FileName) : ""; WriteLog("start cameraId:" + item.CameraId + " ok!, processId:" + item.ProcessId); } //写回配置文件 JsonWriteBack(); if (receiveInfo.IsStartCheck == 1) {
          //先保证启动第一次拉流成功,拉流需要时间,所以需要等待一会然后再开启定时任务 Thread.Sleep(receiveInfo.WaitTimeBeforeCheck
* 1000); timer = new Timer(p => TaskCheck(), null, 0, receiveInfo.LoopPeriodSeconds*1000);//启动定时任务每 LoopPeriodSeconds启动一次 } } /// /// 读取配置文件 /// /// /// public static bool ReadConfigFile(string path) { using (StreamReader fileRead = new StreamReader(path)) { string strRead = fileRead.ReadToEnd(); if (string.IsNullOrEmpty(strRead)) { WriteLog("read config.json error!"); return false; } try { receiveInfo = JsonConvert.DeserializeObject(strRead); if (receiveInfo == null || receiveInfo.COnfigInfos== null || receiveInfo.ConfigInfos.Count <= 0) { WriteLog("read config.json error!"); return false; } } catch (Exception) { WriteLog("read config.json error!"); return false; } } return true; } /// /// 拉流后 将配置重新写回配置文件 /// /// public static void JsonWriteBack() { if (receiveInfo == null || receiveInfo.COnfigInfos== null || receiveInfo.ConfigInfos.Count <= 0) { WriteLog("JsonWriteBack() receiveInfo == null ..."); return; } try { //将配置文件重新写回 using (StreamWriter fileWriter = new StreamWriter(BasePath + ConfigFileName, false)) { //fileWriter.WriteAsync(JsonConvert.SerializeObject(receiveInfo)); fileWriter.Write(JsonConvert.SerializeObject(receiveInfo)); } } catch (Exception e) { WriteLog("JsonWriteBack() error:" + e.Message); } } /// /// 定时检查拉流进程 /// public static void TaskCheck() { //重新读取配置文件 if (!ReadConfigFile(BasePath + ConfigFileName)) return; //重新设置外网ip 端口 OnlineRootAddress= receiveInfo.OnlineRootAddress; bool isRestart = true; int changeCount = 0; Process[] processArr = Process.GetProcessesByName("ffmpeg"); List<int> pIdList = (processArr != null && processArr.Length > 0) ? processArr.Select(m => m.Id).ToList() : new List<int>(); foreach (var item in receiveInfo.ConfigInfos) { isRestart = true; //该摄像头对应进程是否需要重启 //进程id存在且m3u8文件未过期 if (pIdList.Contains(item.ProcessId) && item.ProcessId > 0) { string tsFilePath = BasePath+ M3u8FileBaseDir + "/" + item.OutDirName + "/" + receiveInfo.JudgeFileName; if (File.Exists(tsFilePath)) { FileInfo fi = new FileInfo(tsFilePath); if ((DateTime.Now - fi.LastWriteTime).TotalSeconds //文件未过期 一直在拉流 { isRestart = false; } } else { //覆盖文件时,会存在 JudgeFileName 刚好不存在的情况(ffmpeg会先删除文件然后再生成,所以必须要保证第一次开启所有的摄像头都能生成m3u8文件) isRestart = false; } } //重启进程 if (isRestart) { string str = "Restart CameraId:" + item.CameraId + ", ProcessId:" + item.ProcessId + " -> "; if (pIdList.Contains(item.ProcessId)) { processArr.FirstOrDefault(p => p.Id == item.ProcessId)?.Kill(); } item.ProcessId = 0; item.PlayUrl = ""; item.ProcessId = StartPull(item); //重启推流 if (item.ProcessId > 0) { changeCount++; item.PlayUrl = OnlineRootAddress + M3u8FileBaseDir + "/" + item.OutDirName + "/" + M3u8FileName; } //是否记录日志 if (receiveInfo.IsRecordLog == 1) { WriteLog(str + item.ProcessId); } } } //写回配置文件 if (changeCount > 0) { JsonWriteBack(); } } /// /// 开启拉流 /// /// /// public static int StartPull(ConfigInfo item) { if (item == null) return 0; if (!Directory.Exists(BasePath + M3u8FileBaseDir + "\\" + item.OutDirName)) { Directory.CreateDirectory(BasePath + M3u8FileBaseDir + "\\" + item.OutDirName); } Process p = null; try { var startInfo = new ProcessStartInfo(); startInfo.FileName = "ffmpeg.exe"; //需提前配置环境变量 startInfo.Arguments = " -rtsp_transport tcp -i " + item.RtspPath + " -s 640x480 -force_key_frames \"expr: gte(t, n_forced * 3)\" "; startInfo.Arguments += " -c:v libx264 -hls_time 3 -hls_list_size 30 -hls_wrap 30 -f hls "; startInfo.Arguments += (BasePath + M3u8FileBaseDir + "\\" + item.OutDirName + "\\" + M3u8FileName); startInfo.CreateNoWindow = true; startInfo.UseShellExecute = false; startInfo.Verb = "RunAs";//以管理员身份运行 p = Process.Start(startInfo); return p != null ? p.Id : 0; } catch (Exception ex) { WriteLog("restart cameraId:" + item.CameraId + " error,"+ ex.Message); p?.Close(); return 0; } } /// /// 结束掉所有的推流进程 /// public static void StopAllProcess() { WriteLog("StopAllProcess() start.."); //结束掉所有的进程 ffmpeg进程 List processList = Process.GetProcessesByName("ffmpeg").ToList(); if (processList != null && processList.Count > 0) { processList.ForEach(p => { WriteLog("processId:" + p.Id + " be killed;"); p.Kill(); }); } //将ProcessId,PlayUrl 清空 receiveInfo.ConfigInfos.ForEach(p => { p.ProcessId = 0; p.PlayUrl = ""; }); JsonWriteBack(); processList = Process.GetProcessesByName("CrashServerDamon").ToList(); if (processList != null && processList.Count > 0) processList.ForEach(p => { p.Kill(); }); } /// /// 写日志 /// /// public static void WriteLog(string msg) { string path = BasePath + "Log" + "\\" + DateTime.Now.ToString("yyyyMMdd") + ".txt"; if (!File.Exists(path)) { using (File.Create(path))//释放文件流 { } } using (StreamWriter fileWriter = new StreamWriter(path, true)) { fileWriter.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ") + "-" + msg); } } } }

Program.cs代码

namespace HlsService
{
    static class Program
    {
        /// 
        /// 应用程序的主入口点。
        /// 
        static void Main()
        {
            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[]
            {
                new HlsService()
            };
            ServiceBase.Run(ServicesToRun);
        }
    }
} 

3.6 将工程编译编译后,自己创建新的文件夹结构如下:

FileDir,Log文件夹需要自己手动创建。config.json按照HlsService类中的ReceiveInfo类去对应创建。

 

HlsService.exe, Newtonsoft.Json.dll从工程目录的bin/Debug 或 bin/Release文件夹复制过来

 

InstallService.bat:   安装服务脚本  exe写绝对路径,内容如下:

installutil E:\HlsService\Service.exe  

pause

Uninstall.bat:卸载服务脚本 exe应写绝对路径 内容如下:

installutil E:\HlsService\HlsService.exe /u

pause

注意事项:

1-运行脚本的时候以管理员身份运行

2- 运行时可能会报错 说installutil不存在

将C:\Windows\Microsoft.NET\Framework64\v4.0.30319 配入环境变量

这里只是一个例子,可能Framework64文件夹下有很多.framework版本,然后你选最新版本的文件夹打开,确定里面有installutil.exe 就行了

首先管理员身份启动InstallService.bat, win+R service.msc 看服务是否注册成功,

然后启动确保程序正确运行后,将服务设置为自动启动。

这里有一个小坑:可能ffmpeg无法进行拉流,会报错当前操作系统缺少 mfplat.dll文件

 

就把下面的地址下载解压后将对应文件夹内的 dll copy到左边文件夹中。

mfplat.dll 下载地址:https://pan.baidu.com/s/195QiIP0f42jXoWGZjCxWGw     提取码:imtf

 

3.7   本地部署IIS站点 这一步是为了让m3u8文件对应到tcp端口。外网ip+端口 映射到内网ip+端口

按win 键,键盘右下角介于 fn和alt的那个键。输入iis确定,进入到iis管理器。右键网站,选择添加网站。

 

 2 中的目录就是 3.6步骤中 所有文件的父目录,3 端口可以自己定只要不端口冲突就行。

右键Hls(就是你刚刚新建的那个网站),添加虚拟目录。注意名称别名固定FileDir,然后物理路径固定到  3.6步骤中那个 FileDir文件夹。

 

 

左键点击Hls,右边双击Http响应表头

 

 

 

 

双击FileDir,双击右边MIME类型。

 

 

 

 

   

寻找 .m3u8, .ts这两项,如果原来已经有的点击编辑把类型替换没有项点击添加。

文件扩展名              MIME类型

.m3u8                       application/x-mpegURL

.ts                             video/MP2T

 

比如我们当前站点端口是12400,然后我们拉了一个摄像头的流,文件生成名为One, 这时候内网地址就是 localhost:12400/FileDir/One/play.m3u8

这个地址就可以拿到下面的demo本地播放了,至于映射到外网的话 ,就得需要网络工程师去弄这个东西了或者一些内网穿透软件。

 

3.8 前端播放m3u8

Vjs.rar为前端播放Demo,如果是vue,react,angular对应着各自的语法进行改造。



    
    
    
    


    
    

  前端Demo下载地址: 链接:https://pan.baidu.com/s/1aVIkKwqzCyPoGDZjk5p6ww  提取码:2mx1 

大概就是这样。如果哪里有错误,或者需要本人提供帮助,评论留言或者邮箱 16620834081@163.com


推荐阅读
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 电脑公司win7剪切板位置及使用方法
    本文介绍了电脑公司win7剪切板的位置和使用方法。剪切板一般位于c:\windows\system32目录,程序名为clipbrd.exe。通过在搜索栏中输入cmd打开命令提示符窗口,并输入clip /?即可调用剪贴板查看器。赶紧来试试看吧!更多精彩文章请关注本站。 ... [详细]
  • 解决github访问慢的问题的方法集锦
    本文总结了国内用户在访问github网站时可能遇到的加载慢的问题,并提供了解决方法,其中包括修改hosts文件来加速访问。 ... [详细]
  • 本文介绍了Composer依赖管理的重要性及使用方法。对于现代语言而言,包管理器是标配,而Composer作为PHP的包管理器,解决了PEAR的问题,并且使用简单,方便提交自己的包。文章还提到了使用Composer能够避免各种include的问题,避免命名空间冲突,并且能够方便地安装升级扩展包。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 本文介绍了在Windows环境下如何配置php+apache环境,包括下载php7和apache2.4、安装vc2015运行时环境、启动php7和apache2.4等步骤。希望对需要搭建php7环境的读者有一定的参考价值。摘要长度为169字。 ... [详细]
  • phpcomposer 那个中文镜像是不是凉了 ... [详细]
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
  • 本文介绍了win7系统休眠功能无法启动和关闭的解决方法,包括在控制面板中启用休眠功能、设置系统休眠的时间、通过命令行定时休眠、手动进入休眠状态等方法。 ... [详细]
  • 本文介绍了使用Python解析C语言结构体的方法,包括定义基本类型和结构体类型的字典,并提供了一个示例代码,展示了如何解析C语言结构体。 ... [详细]
  • 本文介绍了在Python中使用zlib模块进行字符串的压缩与解压缩的方法,并探讨了其在内存优化方面的应用。通过压缩存储URL等长字符串,可以大大降低内存消耗,虽然处理时间会增加,但是整体效果显著。同时,给出了参考链接,供进一步学习和应用。 ... [详细]
  • 本文主要复习了数据库的一些知识点,包括环境变量设置、表之间的引用关系等。同时介绍了一些常用的数据库命令及其使用方法,如创建数据库、查看已存在的数据库、切换数据库、创建表等操作。通过本文的学习,可以加深对数据库的理解和应用能力。 ... [详细]
author-avatar
ywf158
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有