一、简介
目前市面上直播推流的软件有很多,拉流也很常见。近期因为业务需要,需要搭建一整套服务端推流,客户端拉流的程序。随即进行了展开研究,花了一个小时做了个基于winfrom桌面版的推拉流软件。另外稍微啰嗦两句,主要怕你们翻不到最下面。目前软件还是一个简化版的,但已足够日常使用,比如搭建一套餐馆的监控,据我了解,小餐馆装个监控一般3000—5000,如果自己稍微懂点软件知识,几百元买几个摄像头+一台电脑,搭建的监控不足千元,甚至一两百元足够搞定了。这是我研究这套软件的另外一个想法。
二、使用的技术栈:
1、nginx
2、ffmpeg
3、asp.net framework4.5 winfrom
4、开发工具vs2019
5、开发语言c#
关于以上技术大体做下说明,使用nginx做为代理节点服务器,基于ffmpeg做推流,asp.net framework4.5 winfrom 做为桌面应用。很多人比较陌生的可能是ffmpeg,把它理解为视频处理最常用的开源软件。关于它的更多详细文章可以去看阮一峰对它的介绍。“FFmpeg 视频处理入门教程”。
5.1启动nginx的核心代码
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; //关闭窗体 } } } }
6、界面展示:
三、目前实现的功能
-
winfrom桌面播放(拉流)
-
推流(直播)
-
(直播)推流录屏
-
....想到再加上去
四、如何使用
-
克隆或下载程序后可以使用vs打开解决方案 、然后选择debug或relase方式进行编译,建议relase,编译后的软件在Bin\debug|relase目录下。
-
双击Bin\debug|relase目录下 MnNiuVideo.exe 即可运行起来。
-
软件打开后,选择本机相机(如果本机有多个相机任意选一个)、点击开始直播(推流),然后点击开始播放(拉流)。
-
关于其他问题或者详细介绍建议直接看源码。
五、最后
可能一眼看去UI比较丑,多年没有使用过winfrom,其实winform本身控件开发的界面就比较丑,界面这块不属于核心,也可以使用web端拉流,手机端拉流,都是可行的。所用技术略有差别。另外,代码这块目前也谈不上多么规范,请轻拍,后期抽时间部分代码都会进行整合调整。后面想到的功能会定期更新,长期维护。软件纯绿色版,基于MIT协议开源,也可自行修改。
源码地址:
码云:https://gitee.com/shenniu_code_group/mn-niu-video
github:https://github.com/realyrare/MnNiuVideo