C# rtsp流转m3u8 解决方案

Posted 三天不学习

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C# rtsp流转m3u8 解决方案相关的知识,希望对你有一定的参考价值。

1、相关资源下载与讲解

1.1 常见的网络摄像机的端口及RTSP地址 

1.2 ffmpeg 和 VLC media player 资源包 和  ffmpeg安装教程

1.3 前端播放m3u8 demo 

1.4 C#源码

2、源码解析

2.1 使用VS 创建windows 服务

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

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

  3.4 添加windows安装程序

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

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

 

 属性分别配置如下:

 接下来贴代码: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<ConfigInfo> 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();
        

        /// <summary>
        /// 主方法
        /// </summary>
        /// <param name="args"></param>
        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启动一次
            
        

        /// <summary>
        /// 读取配置文件
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        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<ReceiveInfo>(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;
        

        /// <summary>
        /// 拉流后 将配置重新写回配置文件
        /// </summary>
        /// <param name="receiveInfo"></param>
        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);
            
        

        /// <summary>
        /// 定时检查拉流进程
        /// </summary>
        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 < receiveInfo.OutdateSecends)//文件未过期 一直在拉流
                        
                            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();
            
        

        /// <summary>
        /// 开启拉流 
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        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;
            
        

        /// <summary>
        /// 结束掉所有的推流进程
        /// </summary>
        public static void StopAllProcess()
        
            WriteLog("StopAllProcess() start..");
            //结束掉所有的进程 ffmpeg进程
            List<Process> 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(); );
        

        /// <summary>
        /// 写日志
        /// </summary>
        /// <param name="msg"></param>
        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
    
        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        static void Main()
        
            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[]
            
                new HlsService()
            ;
            ServiceBase.Run(ServicesToRun);
        
    
 

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

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到左边文件夹中。

本地部署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

注意:添加的MIME 类型可能会在C#代码webconfig里的冲突,说是键值重复,请删除webconfig里的,如下即可:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <staticContent>
         
        </staticContent>
        <httpProtocol>
            <customHeaders>
                <add name=".ts" value="video/MP2T" />
            </customHeaders>
        </httpProtocol>
    </system.webServer>
</configuration>

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

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

温馨提示:可结合硬盘录像机进行组播拉取,就是一屏多画面,多个服务配置如下:

config.json


  "OnlineRootAddress": "http://192.168.100.190:8001/",
  "IsStartCheck": 1,
  "IsRecordLog": 0,
  "WaitTimeBeforeCheck": 20,
  "LoopPeriodSeconds": 10,
  "OutdateSecends": 60,
  "JudgeFileName": "play.m3u8",

  "ConfigInfos": [
    
      "CameraId": 1,
      "CameraName": "多屏",
      "OutDirName": "One",
      "MacAddress": "24-0f-9b-27-b3-25",
      "RtspPath": "rtsp://admin:gwsl@123456@192.168.100.200:554/Streaming/Channels/001?transportmode=multicast",
      "ProcessId": "0",
      "PlayUrl": "http://192.168.100.190:8001/FileDir/One/play.m3u8"
    ,
    
      "CameraId": 1,
      "CameraName": "101",
      "OutDirName": "Two",
      "MacAddress": "c0-6d-ed-79-90-1a",
      "RtspPath": "rtsp://admin:gwsl@123456@192.168.100.101:554/Streaming/Channels/1",
      "ProcessId": "1",
      "PlayUrl": "http://192.168.100.190:8001/FileDir/Two/play.m3u8"
    ,
  ]



以上是关于C# rtsp流转m3u8 解决方案的主要内容,如果未能解决你的问题,请参考以下文章

C# rtsp流转m3u8 解决方案

C# rtsp流转m3u8 解决方案

记录:通过ffmpeg rtsp转 http m3u8

记录:通过ffmpeg rtsp转 http m3u8

EasyRTSPLive摄像机NVR录像机RTSP协议实时流转RTMP协议直播流推送之搭建EasyRTMPLive拉RTSP流转RTMP测试环境的方法解析

rtmp转m3u8