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

2023-05-16

一、简介

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

作者:realyrare

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • PX4原生固件,position_estimator_inav解读

    INAV integrated navigation组合导航 对于多旋翼的位置姿态估计系统 xff1a PX4原生固件如今已经默认使用EKF2了 xff0c 另一种情况是 使用local position estimator attitud
  • FusionCharts Free (FCF) 版本 v3.0 更新细节

    版本 v3 0 更新细节 1 新的图表类型 滚动图 柱二维 xff0c 二维和区系的二维 xff0c 堆栈柱二维 xff0c 二维结合 xff0c 结合二维 xff08 双 Y 轴 xff09 样图 样条区域图 对数坐标图 二维多图单 组合
  • dwm-1000 测距总是出现 #define SYS_STATUS_RXPTO 0x00200000UL /* Preamble detection timeout */

    ex 05b ds twr resp 程序 总是出现 致使官方的代码 无法实现通讯 define SYS STATUS RXPTO 0x00200000UL Preamble detection timeout 需要着重修改参数
  • VNC远程桌面到linux,提示connection refused(10061)解决办法

    确认server端的VNC服务开启 xff0c service vncserver start xff0c 检测状态时ok的 ps ef grep vnc xff0c 来查看不是已经开启多个vnc连接 如果有多个vnc连接 xff0c 使用
  • uio驱动框架

    核心 xff0c 利用mmap进行映射 参考资料 uio 编写实例 1 https blog csdn net wujiangguizhen article details 12453253 uio编写实例 2 https blog csd
  • enum类型被intent所携带时需要注意的地方

    一般我们在Activity之间传递对象时多用Parcelable 比如写一个class xff0c 在这个class上标明implements Parcelable并实现接口就可以用Intent putExtra String Parcel
  • dump文件,windbg

    dump文件 xff0c 在VC中的调试还是非常非常非常有用的 xff0c 因为我们也不会经每一行代码都加上日志 xff0c 当然如果你愿意 xff0c 也可以每一行都加上日志 xff1b 在Windows上 xff0c 添加dump文件有
  • 使用PyQt4制作一个音乐播放器(1)

    1 前言 最近用Python给老妈写了一个处理excel表格的小软件 xff0c 因为自己平时用Python一般都是用在数值计算领域 xff0c 所以一般使用命令行的方式交互即可 但是给老妈用发现用命令行交互方式使用并不是很方便 xff0c
  • AI 到底是怎么「想」的?

    本文作者 xff1a kurffzhou xff0c 腾讯 TEG 安全工程师 最近 xff0c Nature发表了一篇关于深度学习系统被欺骗的新闻文章 xff0c 该文指出了对抗样本存在的广泛性和深度学习的脆弱性 xff0c 以及几种可能
  • 效能优化实践:C/C++单元测试万能插桩工具

    作者 xff1a mannywang xff0c 腾讯安全平台后台开发 研发效能是一个涉及面很广的话题 xff0c 它涵盖了软件交付的整个生命周期 xff0c 涉及产品 架构 开发 测试 运维 xff0c 每个环节都可能影响顺畅 高质量地持
  • tensowflow报错tensorflow.python.framework.errors_impl.InvalidArgumentError<exception str

    tensorflow用于自己的数据集时 xff0c 在用saver restore导入模型到Session中 xff0c 导入语句报错 xff0c 异常链终止时提示 xff1a tensorflow python framework err
  • 详解HTTP中的摘要认证机制

    在上一期http blog csdn net tenfyguo article details 6167190中笔者较为详细的介绍了HTTPBasic认证在apache下的配置 xff0c 通过简单的实验演示了HTTP Basic认证的基本
  • 【ubuntu(Linux)安装Vs code并配置c++编译及cmake多文件编译】

    目录标题 VS code配置c 43 43 编译环境1 Linux系统安装2 在Ubuntu中安装VS code2 1 首先下载对应系统的VS code安装包2 2 安装VS code 3 在ubuntu系统下的vscode中配置g 43
  • yolo v4安装与使用

    yolo v4安装与使用 GPU版本 系统环境 Ubuntu18 04 cuda11 0 cudnn8 0 4 opencv3 4 4 相关链接 官网链接 xff1a https github com AlexeyAB darknet yo
  • Jetson NX克隆与烧录镜像

    前言 本文主要说明如何在Ubuntu系统上备份Jetson NX镜像 xff0c 并烧录镜像到多台NX开发板上 我的NX的系统是在SD卡上的 xff0c 所以这个方法只适用于系统装在SD卡上的开发板 参考链接 https blog csdn
  • ros2 CMakeLists.txt与packages.xml写法

    注 xff1a 尽量使用ament cmake auto来编写cmakelists txt文件 xff0c 依赖的程序包在packages xml文件中用 lt depend gt nav msgs lt depend gt 的形式声明 x
  • TX2 ubuntu18.04 改固定IP地址 eth0无本机IP

    注意事项 我是在TX2 ubuntu18 04上改的固定IP地址 其他主机上ubuntu18 04好像需要另一种方法 xff0c 需要你自己去百度上搜 参考博客 https blog csdn net a13662080711 articl
  • MFC与.NET混合编程

    1 xff1a VS2008 建立 MFC程序 xff1b 2 xff1a Alt 43 F7 设置 xff1a 公共语言运行库支持 xff08 选择 xff09 公共语言运行库支持 clr xff1b 3 xff1a 添加 Net相应库
  • 在TX2(Jetpack4.2+kernel4.9.140+librealsense v2.22)上安装D435i驱动

    在TX2 xff08 Jetpack4 2 43 kernel4 9 140 43 librealsense v2 22 xff09 上安装D435i驱动 前言准备工作系统版本参考资料 操作步骤 前言 用了很长时间 xff0c 参考了很多博

随机推荐

  • ROS发布静态tf变换

    方法一 xff1a include lt ros ros h gt include lt tf transform broadcaster h gt int main int argc char argv ros init argc arg
  • ROS常见问题及解决方法

    1 undefined reference to 96 tf TransformBroadcaster TransformBroadcaster 问题描述 xff1a CMakeFiles imu data dir src imu data
  • TX2制作镜像并烧写镜像

    前言 本文简要介绍TX2如何制作镜像并烧写镜像 xff0c 如果读者一开始是通过sdkmanager安装的TX2系统可直接进入笔记本中 nvidia nvidia sdk JetPack 4 2 Linux P3310 Linux for
  • php写守护进程(Daemon)

    守护进程 xff08 Daemon xff09 是运行在后台的一种特殊进程 它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件 守护进程是一种很有用的进程 php也可以实现守护进程的功能 1 基本概念 进程 每个进程都有一个父
  • clang-format 说明

    转载自 xff1a https www cnblogs com lepeCoder p 8032178 html BasedOnStyle string 这个样式用于所有没有特殊指定配置的选项 这个选项仅在clang format配置中支持
  • 路径规划

    转载 xff1a 链接 xff1a https www jianshu com p f3bab4e119cf D算法 xff0c Dijkstra算法 两个表 xff1a closed表与open表 closed表用于记录已访问过的节点 x
  • awk 筛选并计算时间

    grep 39 filter cost time 39 log file awk F 39 filtered route count 39 39 print 2 39 awk F 39 39 39 print 1 39 awk 39 BEG
  • 忽略批处理中的pause命令

    当存在一个批处理文件 run bat xff0c 其中 xff1a 64 echo 开始暂停 pause 64 echo 暂停结束 当我们执行这个run bat文件时 xff0c 运行到pause会提示需要按任意键继续 xff0c 并且命令
  • BullseyeCoverage 代码覆盖度检查工具

    昨日又有同事问我 BullseyeCoverage 的使用方法 xff0c 由于时间久远 xff0c 有些遗忘 xff0c 只后悔当初为什么没有整理记录下来 xff0c 只好重新查阅了很多文档 今日整理下比较重要的知识 xff0c 方便以后
  • 用于词义消岐的Lesk算法

    该算法由Michael E Lesk于1986年提出 xff0c 是一个基于词典的词义消岐方法 该算法认为 xff1a 一个词在词典中的词义解释与该词所在句子具有相似性 这种相似性可以由相同单词的个数来表示 xff0c 比如 cone 和
  • 通过cmake打印CMakeLists.txt中的宏

    转 cmake中宏的使用 原文路径 https blog csdn net qq 29573053 article details 80392441 首先贴一个例子进行分析 set var 34 ABC 34 macro Moo arg m
  • undefined reference to `vtable for XXXXX`

    vtable 表示的是虚表 这个错误出现时 请检查你的父类所有虚函数是否实现 或者子类是否把父类的虚函数都处理完 注意 析构函数也算
  • CMakeLists.txt ----find_package

    在linux平台下编译程序的时候通常都会使用到CMakeLists txt来制定编译规则 在查找需要链接的lib时候 通常会使用到find package 记录一下我之前用到的地方 find package 之后 最好到FindXXX cm
  • opencv学习笔记1 opencv安装及配置 一劳永逸不需要每次都重新配置

    opencv2 4 9地址 xff1a https sourceforge net projects opencvlibrary files opencv win 2 4 9 opencv 2 4 9 exe download vs2013
  • 【Linux学习笔记】关于ubuntu开机菜单栏和任务栏不见了的有效解决方法

    一 问题描述 ubuntu开机只有桌面 xff0c 没有菜单栏和任务栏 xff0c 如下图 xff1a 二 问题解决 刚学习ubuntu xff0c 总有些像我这样不折腾就不舒服的人 xff0c 今天改了一下主题 xff0c 图标什么的 x
  • 【数据结构与算法】深入浅出递归和迭代的通用转换思想

    深入浅出递归和迭代的通用转换思想 一般来说 xff0c 能用迭代的地方就不要用递归 xff01 理论上讲 xff0c 所有的递归和迭代之间都能相互转换 xff01 刷题碰到 一天一道LeetCode 130 Surrounded Regio
  • 【unix网络编程第三版】阅读笔记(二):套接字编程简介

    unp第二章主要将了TCP和UDP的简介 xff0c 这些在 TCP IP详解 和 计算机网络 等书中有很多细致的讲解 xff0c 可以参考本人的这篇博客 计算机网络 第五版 阅读笔记之五 xff1a 运输层 xff0c 这篇博客就不再赘述
  • 带你深入理解STL之Deque容器

    在介绍STL的deque的容器之前 xff0c 我们先来总结一下vector和list的优缺点 vector在内存中是分配一段连续的内存空间进行存储 xff0c 其迭代器采用原生指针即可 xff0c 因此其支持随机访问和存储 xff0c 支
  • 带你深入理解STL之Set和Map

    在上一篇博客 带你深入理解STL之RBTree中 xff0c 讲到了STL中关于红黑树的实现 xff0c 理解起来比较复杂 xff0c 正所谓前人种树 xff0c 后人乘凉 xff0c RBTree把树都种好了 xff0c 接下来就该set
  • 一个小时开发的直播推拉流软件来了

    一 简介 目前市面上直播推流的软件有很多 xff0c 拉流也很常见 近期因为业务需要 xff0c 需要搭建一整套服务端推流 xff0c 客户端拉流的程序 随即进行了展开研究 xff0c 花了一个小时做了个基于winfrom桌面版的推拉流软件