C# rtsp流转m3u8 解决方案
Posted 三天不学习
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C# rtsp流转m3u8 解决方案相关的知识,希望对你有一定的参考价值。
1、相关资源下载与讲解
1.2 ffmpeg 和 VLC media player 资源包 和 ffmpeg安装教程
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响应表头
Access-Control-Allow-Headers: Content-Type, api_key, Authorization
Access-Control-Allow-Origin :*
双击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 解决方案的主要内容,如果未能解决你的问题,请参考以下文章
EasyRTSPLive摄像机NVR录像机RTSP协议实时流转RTMP协议直播流推送之搭建EasyRTMPLive拉RTSP流转RTMP测试环境的方法解析