隐藏

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

发布:2021/5/17 17:55:39作者:管理员 来源:本站 浏览次数:727

一、简介


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


推流(直播)


(直播)推c#教程流录屏


…想到再加上去


四、如何使用


克隆或下载程序后可以使用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