zoukankan      html  css  js  c++  java
  • 一个小时开发的直播推拉流软件来了

    一、简介

    目前市面上直播推流的软件有很多,拉流也很常见。近期因为业务需要,需要搭建一整套服务端推流,客户端拉流的程序。随即进行了展开研究,花了一个小时做了个基于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}
    ginx.exe";
                _nginxFilePath = Path.GetFullPath(nginxPath);
                _workingDirectory = Path.GetDirectoryName(_nginxFilePath);
                _arguments = @" -c conf
    ginx-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}	askkill /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

    作者:课间一起牛

    出处:https://www.cnblogs.com/mhg215/

    声援博主:如果您觉得文章对您有帮助,请点击文章末尾的【关注我】吧!

    别忘记点击文章右下角的【推荐】支持一波。~~~///(^v^)\~~~ .

    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

    如果您有其他问题,也欢迎关注我下方的公众号,可以联系我一起交流切磋!

     B站: 课间一起牛的B站         知乎:课间一起牛的知乎

    码云:课间一起牛的码云      github:课间一起牛的github

  • 相关阅读:
    关于npm无法安装依赖包以及安装包缓慢的解决方法
    centos 上安装nodejs v8.0.0
    nginx 负载均衡
    关于前端
    递归函数
    多重循环
    闭包
    spring boot集成mybatis(2)
    spring boot集成mybatis(3)
    spring boot集成mybatis(1)
  • 原文地址:https://www.cnblogs.com/mhg215/p/14300425.html
Copyright © 2011-2022 走看看