C#實現 通過ffmpeg拉取海康攝像頭rtsp流轉m3u8,並在前端播放的解決方案。


前言:

  最近拿到一個需求,客戶希望在公司自研系統上能夠播放他們自己安裝的(海康)攝像頭實時畫面。剛開始跟老大一起技術選型定的是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<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);
        }
    }
} 

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對應着各自的語法進行改造。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title></title>
    <link href="video-js.css" rel="stylesheet">
    <script src='video.js'></script>
</head>
<body>
    <style>
        .video-js .vjs-tech {position: relative !important;}
    </style>
    <div>
        <video id="myVideo" class="video-js vjs-default-skin vjs-big-play-centered" controls preload="auto" data-setup='{}' style='width: 100%;height: auto'>
            <source id="source" src="http://hls.open.ys7.com/openlive/f01018a141094b7fa138b9d0b856507b.m3u8" type="application/x-mpegURL"></source>
        </video>
    </div>
</body>
 
<script>
    // videojs 簡單使用
    var myVideo = videojs('myVideo', {
        bigPlayButton: true,
        textTrackDisplay: false,
        posterImage: false,
        errorDisplay: false,
    })
    myVideo.play();

</script>

  前端Demo下載地址: 鏈接:https://pan.baidu.com/s/1aVIkKwqzCyPoGDZjk5p6ww  提取碼:2mx1 

大概就是這樣。如果哪里有錯誤,或者需要本人提供幫助,評論留言或者郵箱 16620834081@163.com


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM