一、简介
目前市面上直播推流的软件有很多,拉流也很常见。近期因为业务需要,需要搭建一整套服务端推流,客户端拉流的程序。随即进行了展开研究,花了一个小时做了个基于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 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 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.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协议开源,也可自行修改。
源码地址: