一个小时开发的直播推拉流软件来了

  • A+
所属分类:.NET技术
摘要

一、简介 目前市面上直播推流的软件有很多,拉流也很常见。近期因为业务需要,需要搭建一整套服务端推流,客户端拉流的程序。随即进行了展开研究,花了一个小时做了个基于winfrom桌面版的推拉流软件。另外稍微啰嗦两句,主要怕你们翻不到最下面。目前软件还是一个简化版的,但已足够日常使用,比如搭建一套餐馆的监控,据我了解,小餐馆装个监控一般3000—5000,如果自己稍微懂点软件知识,几百元买几个摄像头+一台电脑,搭建的监控不足千元,甚至一两百元足够搞定了。这是我研究这套软件的另外一个想法。

一、简介

目前市面上直播推流的软件有很多,拉流也很常见。近期因为业务需要,需要搭建一整套服务端推流,客户端拉流的程序。随即进行了展开研究,花了一个小时做了个基于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 confnginx-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);             }         }     } }

View Code

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();             //    }             //}         }     } }

View Code

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; //关闭窗体             }         }     } }

View Code

6、界面展示:

一个小时开发的直播推拉流软件来了

一个小时开发的直播推拉流软件来了

一个小时开发的直播推拉流软件来了

三、目前实现的功能

  1. winfrom桌面播放(拉流)

  2. 推流(直播)

  3. (直播)推流录屏

  4. ....想到再加上去

四、如何使用

  1. 克隆或下载程序后可以使用vs打开解决方案 、然后选择debug或relase方式进行编译,建议relase,编译后的软件在Bindebug|relase目录下。

  2. 双击Bindebug|relase目录下 MnNiuVideo.exe 即可运行起来。

  3. 软件打开后,选择本机相机(如果本机有多个相机任意选一个)、点击开始直播(推流),然后点击开始播放(拉流)。

  4. 关于其他问题或者详细介绍建议直接看源码。

五、最后

可能一眼看去UI比较丑,多年没有使用过winfrom,其实winform本身控件开发的界面就比较丑,界面这块不属于核心,也可以使用web端拉流,手机端拉流,都是可行的。所用技术略有差别。另外,代码这块目前也谈不上多么规范,请轻拍,后期抽时间部分代码都会进行整合调整。后面想到的功能会定期更新,长期维护。软件纯绿色版,基于MIT协议开源,也可自行修改。

源码地址

码云:https://gitee.com/shenniu_code_group/mn-niu-video

github:https://github.com/realyrare/MnNiuVideo