1、簡介nginx
目前市面上直播推流的軟件有不少,拉流也很常見。近期由於業務須要,須要搭建一整套服務端推流,客戶端拉流的程序。隨即進行了展開研究,花了一個小時作了個基於winfrom桌面版的推拉流軟件。另外稍微囉嗦兩句,主要怕大家翻不到最下面。目前軟件仍是一個簡化版的,但已足夠平常使用,好比搭建一套餐館的監控,據我瞭解,小餐館裝個監控通常3000—5000,若是本身稍微懂點軟件知識,幾百元買幾個攝像頭+一臺電腦,搭建的監控不足千元,甚至一兩百元足夠搞定了。這是我研究這套軟件的另一個想法。git
2、使用的技術棧:github
一、nginx web
二、ffmpeg shell
三、asp.net framework4.5 winfrom c#
四、開發工具vs2019 服務器
五、開發語言c#app
關於以上技術大致作下說明,使用nginx作爲代理節點服務器,基於ffmpeg作推流,asp.net framework4.5 winfrom 作爲桌面應用。不少人比較陌生的多是ffmpeg,把它理解爲視頻處理最經常使用的開源軟件。關於它的更多詳細文章能夠去看阮一峯對它的介紹。「FFmpeg 視頻處理入門教程」。asp.net
5.1啓動nginx的核心代碼ide
using MnNiuVideoApp.Common; using System; using System.Diagnostics; using System.IO; using System.Windows.Forms; namespace MnNiuVideoApp { public class NginxProcess { //nginx的進程名 public string _nginxFileName = "nginx"; public string _stop = "stop.bat"; public string _start = "start.bat"; //nginx的文件路徑名 public string _nginxFilePath = string.Empty; //nginx的啓動參數 public string _arguments = string.Empty; //nginx的工做目錄 public string _workingDirectory = string.Empty; public int _processId = 0; public NginxProcess() { string basePath = FileHelper.LoadNginxPath(); string nginxPath = $@"{basePath}\nginx.exe"; _nginxFilePath = Path.GetFullPath(nginxPath); _workingDirectory = Path.GetDirectoryName(_nginxFilePath); _arguments = @" -c \conf\nginx-win.conf"; } //關掉全部nginx的進程,格式必須這樣,有空格存在 taskkill /IM nginx.exe /F /// <summary> /// 啓動服務 /// </summary> /// <returns></returns> public void StartService() { try { if (ProcessesHelper.IsCheckProcesses(_nginxFileName)) { LogHelper.WriteLog("nginx進程已經啓動過了"); } else { var sinfo = new ProcessStartInfo { FileName = _nginxFilePath, Verb = "runas", WorkingDirectory = _workingDirectory, Arguments = _arguments }; #if DEBUG sinfo.UseShellExecute = true; sinfo.CreateNoWindow = false; #else sinfo.UseShellExecute = false; #endif using (var process = Process.Start(sinfo)) { //process?.WaitForExit(); _processId = process.Id; } } } catch (Exception e) { LogHelper.WriteLog(e.Message); MessageBox.Show(e.Message); } } /// <summary> /// 關閉nginx全部進程 /// </summary> /// <returns></returns> public void StopService() { ProcessesHelper.KillProcesses(_nginxFileName); } /// <summary> /// 須要以管理員身份調用才能起做用 /// </summary> public void KillAll() { try { ProcessStartInfo sinfo = new ProcessStartInfo(); #if DEBUG sinfo.UseShellExecute = true; // sinfo.CreateNoWindow = true; #else sinfo.UseShellExecute = false; #endif sinfo.FileName = _nginxFilePath; sinfo.Verb = "runas"; sinfo.WorkingDirectory = _workingDirectory; sinfo.Arguments = $@"{_workingDirectory}\taskkill /IM nginx.exe /F "; using (Process _process = Process.Start(sinfo)) { _processId = _process.Id; } } catch (Exception ex) { MessageBox.Show(ex.Message); } } } }
5.2啓動ffmpeg進程的核心代碼
using MnNiuVideoApp.Common; using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; namespace MnNiuVideoApp { public class VideoProcess { private static string _ffmpegPath = string.Empty; static VideoProcess() { _ffmpegPath = FileHelper.LoadFfmpegPath(); } /// <summary> /// 調用ffmpeg.exe 執行命令 /// </summary> /// <param name="Parameters">命令參數</param> /// <returns>返回執行結果</returns> public static void Run(string parameters) { // 設置啓動參數 ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.Verb = "runas"; startInfo.FileName = _ffmpegPath; startInfo.Arguments = parameters; #if DEBUG startInfo.CreateNoWindow = false; startInfo.UseShellExecute = true; //將輸出信息重定向 //startInfo.RedirectStandardOutput = true; #else //設置不在新窗口中啓動新的進程 startInfo.CreateNoWindow = true; //不使用操做系統使用的shell啓動進程 startInfo.UseShellExecute = false; #endif using (var proc = Process.Start(startInfo)) { proc?.WaitForExit(3000); } //finally //{ // if (proc != null && !proc.HasExited) // { // //"即將殺掉視頻錄製進程,Pid:{0}", proc.Id)); // proc.Kill(); // proc.Dispose(); // } //} } } }
5.3 窗體裏面事件的核心代碼
using MnNiuVideoApp; using MnNiuVideoApp.Common; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace MnNiuVideo { public partial class PlayerForm : Form { public PlayerForm() { InitializeComponent(); new NginxProcess().StopService(); //獲取本機全部相機 var cameras = CameraUtils.ListCameras(); if (toolStripComboBox1.ComboBox != null) { var list = new List<string>() { "--請選擇相機--" }; foreach (var item in cameras) { list.Add(item.FriendlyName); } toolStripComboBox1.ComboBox.DataSource = list; } } TstRtmp rtmp = new TstRtmp(); Thread thPlayer; private void StartPlayStripMenuItem_Click(object sender, EventArgs e) { StartPlayStripMenuItem.Enabled = false; TaskScheduler uiContext = TaskScheduler.FromCurrentSynchronizationContext(); Task t = Task.Factory.StartNew(() => { if (thPlayer != null) { rtmp.Stop(); thPlayer = null; } else { string path = FileHelper.GetLoadPath(); pic.Image = Image.FromFile(path); thPlayer = new Thread(DeCoding) { IsBackground = true }; thPlayer.Start(); StartPlayStripMenuItem.Text = "中止播放"; //StartPlayStripMenuItem.Enabled = true; } }).ContinueWith(m => { StartPlayStripMenuItem.Enabled = true; Console.WriteLine("任務結束"); }, uiContext); } /// <summary> /// 播放線程執行方法 /// </summary> private unsafe void DeCoding() { try { Console.WriteLine("DeCoding run..."); Bitmap oldBmp = null; // 更新圖片顯示 TstRtmp.ShowBitmap show = (bmp) => { this.Invoke(new MethodInvoker(() => { if (this.pic.Image != null) { this.pic.Image = null; } if (bmp != null) { this.pic.Image = bmp; } if (oldBmp != null) { oldBmp.Dispose(); } oldBmp = bmp; })); }; //線程間操做無效 var url = string.Empty; this.Invoke(new Action(() => { url = PlayAddressComboBox.Text.Trim(); })); if (string.IsNullOrEmpty(url)) { MessageBox.Show("播放地址爲空!"); return; } rtmp.Start(show, url); } catch (Exception ex) { Console.WriteLine(ex); } finally { Console.WriteLine("DeCoding exit"); rtmp?.Stop(); thPlayer = null; this.Invoke(new MethodInvoker(() => { StartPlayStripMenuItem.Text = "開始播放"; StartPlayStripMenuItem.Enabled = true; })); } } private void DesktopRecordStripMenuItem_Click(object sender, EventArgs e) { var path = FileHelper.VideoRecordPath(); if (string.IsNullOrEmpty(path)) { MessageBox.Show("視頻存放文件路徑爲空"); } string args = $"ffmpeg -f gdigrab -r 24 -offset_x 0 -offset_y 0 -video_size 1920x1080 -i desktop -f dshow -list_devices 0 -i video=\"Integrated Webcam\":audio=\"麥克風(Realtek Audio)\" -filter_complex \"[0:v] scale = 1920x1080[desktop];[1:v] scale = 192x108[webcam];[desktop][webcam] overlay = x = W - w - 50:y = H - h - 50\" -f flv \"rtmp://127.0.0.1:20050/myapp/test\" -map 0 {path}"; VideoProcess.Run(args); StartLiveToolStripMenuItem.Text = "正在直播"; } private void LiveRecordStripMenuItem_Click(object sender, EventArgs e) { var path = FileHelper.VideoRecordPath(); if (string.IsNullOrEmpty(path)) { MessageBox.Show("視頻存放文件路徑爲空"); } var args = $" -f dshow -re -i video=\"Integrated Webcam\" -tune zerolatency -vcodec libx264 -preset ultrafast -b:v 400k -s 704x576 -r 25 -acodec aac -b:a 64k -f flv \"rtmp://127.0.0.1:20050/myapp/test\" -map 0 {path}"; VideoProcess.Run(args); StartLiveToolStripMenuItem.Text = "正在直播"; } /// <summary> /// 開始直播(服務端開始推流) /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void StartLiveToolStripMenuItem_Click(object sender, EventArgs e) { try { if (toolStripComboBox1.ComboBox != null) { string camera = toolStripComboBox1.ComboBox.SelectedText; if (string.IsNullOrEmpty(camera)) { MessageBox.Show("請選擇要使用的相機"); return; } var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Icon"); var imgPath = Path.Combine(path + "\\", "stop.jpg"); StartLiveToolStripMenuItem.Enabled = false; StartLiveToolStripMenuItem.Image = Image.FromFile(imgPath); string args = $" -f dshow -re -i video=\"{camera}\" -tune zerolatency -vcodec libx264 -preset ultrafast -b:v 400k -s 704x576 -r 25 -acodec aac -b:a 64k -f flv \"rtmp://127.0.0.1:20050/myapp/test\""; VideoProcess.Run(args); } StartLiveToolStripMenuItem.Text = "正在直播"; } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void PlayerForm_Load(object sender, EventArgs e) { // if (toolStripComboBox1.ComboBox != null) toolStripComboBox1.ComboBox.SelectedIndex = 0; } private void PlayerForm_FormClosed(object sender, FormClosedEventArgs e) { this.Dispose(); this.Close(); } private void PlayerForm_FormClosing(object sender, FormClosingEventArgs e) { DialogResult dr = MessageBox.Show("您是否退出?", "提示:", MessageBoxButtons.OKCancel, MessageBoxIcon.Information); if (dr != DialogResult.OK) { if (dr == DialogResult.Cancel) { e.Cancel = true; //不執行操做 } } else { new NginxProcess().StopService(); Application.Exit(); e.Cancel = false; //關閉窗體 } } } }
六、界面展現:
3、目前實現的功能
winfrom桌面播放(拉流)
推流(直播)
(直播)推流錄屏
....想到再加上去
4、如何使用
克隆或下載程序後可使用vs打開解決方案 、而後選擇debug或relase方式進行編譯,建議relase,編譯後的軟件在Bin\debug|relase目錄下。
雙擊Bin\debug|relase目錄下 MnNiuVideo.exe 便可運行起來。
軟件打開後,選擇本機相機(若是本機有多個相機任意選一個)、點擊開始直播(推流),而後點擊開始播放(拉流)。
關於其餘問題或者詳細介紹建議直接看源碼。
5、最後
可能一眼看去UI比較醜,多年沒有使用過winfrom,其實winform自己控件開發的界面就比較醜,界面這塊不屬於核心,也可使用web端拉流,手機端拉流,都是可行的。所用技術略有差異。另外,代碼這塊目前也談不上多麼規範,請輕拍,後期抽時間部分代碼都會進行整合調整。後面想到的功能會按期更新,長期維護。軟件純綠色版,基於MIT協議開源,也可自行修改。
源碼地址: