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

简介: 目前市面上直播推流的软件有很多,拉流也很常见。近期因为业务需要,需要搭建一整套服务端推流,客户端拉流的程序。随即进行了展开研究,花了一个小时做了个基于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 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、界面展示:
1046844-20210119215955444-893869733.png
1046844-20210119222208566-1094089604.png

三、目前实现的功能

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

相关文章
|
3月前
|
开发工具 Android开发 开发者
Android平台如何不推RTMP|不发布RTSP流|不实时录像|不回传GB28181数据时实时快照?
本文介绍了一种在Android平台上实现实时截图快照的方法,尤其适用于无需依赖系统接口的情况,如在RTMP推送、RTSP服务或GB28181设备接入等场景下进行截图。通过底层模块(libSmartPublisher.so)实现了截图功能,封装了`SnapShotImpl.java`类来管理截图流程。此外,提供了关键代码片段展示初始化SDK实例、执行截图、以及在Activity销毁时释放资源的过程。此方案还考虑到了快照数据的灵活处理需求,符合GB/T28181-2022的技术规范。对于寻求更灵活快照机制的开发者来说,这是一个值得参考的设计思路。
|
3月前
|
数据采集 编解码 开发工具
Android平台实现无纸化同屏并推送RTMP或轻量级RTSP服务(毫秒级延迟)
一个好的无纸化同屏系统,需要考虑的有整体组网、分辨率、码率、实时延迟、音视频同步和连续性等各个指标,做容易,做好难
解决直播间源码音视频不同步问题的有效方式
我们就实现了直播间源码技术智能音视频同步功能,智能音视频同步功能有利于提高直播间源码平台直播质量、直播互动、用户体验与传递信息等作用,是不可或缺的重要功能之一。
解决直播间源码音视频不同步问题的有效方式
|
编解码 监控 C++
H264音视频直播系统 服务器端+客户端源码 可用于视频聊天、视频会议
H264音视频直播系统 服务器端+客户端源码 可用于视频聊天、视频会议
142 0
|
Web App开发 移动开发 算法
关于 TRTC (实时音视频通话模式)在我司的实践 #78
关于 TRTC (实时音视频通话模式)在我司的实践 #78
336 0
|
编解码 移动开发 小程序
视频直播技术干货:一文读懂主流视频直播系统的推拉流架构、传输协议等
本文将通过介绍实时视频直播技术体系,包括常用的推拉流架构、传输协议等,让你对现今主流的视频直播技术有一个基本的认知。
445 1
视频直播技术干货:一文读懂主流视频直播系统的推拉流架构、传输协议等
|
Web App开发 编解码 网络协议
阿里云低延时直播RTS能力升级 让直播推流效果更佳
针对主播推流使用RTMP存在的TCP链接耗时过长、拥塞控制完全依赖TCP传输层、无法提供实时带宽数据来动态调整视频编码码率等问题引起的推流延迟和卡顿。阿里云低延时直播RTS(Real-time Streaming)产品在下行UDP改造的基础上,进行上行UDP底层WebRTC技术优化,通过发布移动端、PC端推流RTS SDK插件来提升整个行业的主播推流质量,提供低延时、低卡顿、安全可靠的直播观看体验。客户端接入简单,只需要在OBS端嵌入RTS SDK即可新增一个推流协议,无需改变原有的推流端采集架构。
1940 0
|
边缘计算 编解码 监控
直播软件开发,低延时直播源码的特性分析
直播软件开发,低延时直播源码的特性分析
|
监控 机器人 BI
【上新】场景化能力包|互动消息智能推送
[DING]~好久不见~场景化能力包,本期场景化能力包推荐的是互动消息智能推送,一起来看看吧
【上新】场景化能力包|互动消息智能推送
|
监控 黑灰产治理
直播平台开发干货分享——标准直播及快、慢直播的特性
 所谓自己做直播平台开发,要结合不同的应用场景,相对应的功能、硬件、软件配套技术也不同。根据应用场景的不同,自建直播平台可以分为标准直播、快直播和慢直播。本文将简单地为大家分析一下这三点的特性。
直播平台开发干货分享——标准直播及快、慢直播的特性