融媒炁 發表於 2020-11-24 16:20:00

C#推流RTMP,摄像头、麦克风、桌面、声卡(附源码)

<p> <span style="font-family: &quot;Microsoft YaHei&quot;"> 这段时间一直都在研究推流的技术,经过断断续续将近两个月的摸索实践,终于能稳定地推流了。</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">&nbsp; &nbsp; &nbsp; &nbsp;这个demo的主要功能就是将采集到的摄像头或桌面的视频、以及麦克风或声卡的音频数据推到Nginx-RTMP服务器上,再由Web浏览器去拉流并播放。<br></span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">  接下来介绍Demo整个功能的实现原理和代码逻辑,大家可以从文末下载源码后,对照源码再来看下面的介绍就会更清晰些。</span></p>
<h2><span style="font-family: &quot;Microsoft YaHei&quot;">一.客户端实现</span></h2>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">&nbsp; &nbsp; &nbsp; &nbsp;客户端的界面效果图如下所示:</span></p>
<p>  <img src="https://img2020.cnblogs.com/blog/9005/202011/9005-20201124151751911-113086656.png" alt="" width="296" height="405" loading="lazy"></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">  客户端的具体功能:可以采集摄像头或者桌面图像,也可以采集麦克风与声卡的声音 并将它们推送到Nginx流服务器上。</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">  从上面功能就可以看出这里需要有多个采集器来采集相关的数据:摄像头采集器、麦克风采集器、桌面采集器、以及声卡采集器。如果需要将麦克风或声卡的声音混音(比如,主播一边用电脑播放背景音乐一边播讲),则还需要使用混音器。</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">&nbsp; &nbsp; &nbsp; &nbsp;在点击启动设备按钮时,我们就需要来启动各自对应的采集器,并开始采集:</span></p>
<div class="cnblogs_code">
<pre>      <span style="color: rgba(0, 0, 255, 1)">#region</span> 设置采集器
      <span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.radioButton_desktop.Checked)
      {
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">桌面采集器
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">如果需要录制鼠标的操作,第二个参数请设置为true</span>
            <span style="color: rgba(0, 0, 255, 1)">this</span>.desktopCapturer = <span style="color: rgba(0, 128, 128, 1)">CapturerFactory</span>.CreateDesktopCapturer(frameRate, <span style="color: rgba(0, 0, 255, 1)">false</span>,<span style="color: rgba(0, 0, 255, 1)">new</span> Rectangle(<span style="color: rgba(128, 0, 128, 1)">0</span>,<span style="color: rgba(128, 0, 128, 1)">0</span>,<span style="color: rgba(128, 0, 128, 1)">1920</span>,<span style="color: rgba(128, 0, 128, 1)">1080</span><span style="color: rgba(0, 0, 0, 1)">));
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.desktopCapturer.ImageCaptured += <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.Form1_ImageCaptured;
   
      }
      </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.radioButton_camera.Checked)
      {
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">摄像头采集器</span>
            <span style="color: rgba(0, 0, 255, 1)">this</span>.cameraCapturer = <span style="color: rgba(0, 128, 128, 1)">CapturerFactory</span>.CreateCameraCapturer(<span style="color: rgba(128, 0, 128, 1)">0</span>, <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.defaultVideoSize, frameRate);
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.cameraCapturer.ImageCaptured += <span style="color: rgba(0, 0, 255, 1)">new</span> CbGeneric&lt;Bitmap&gt;(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.Form1_ImageCaptured);
      }

      </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.checkBox_micro.Checked)
      {
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">麦克风采集器</span>
            <span style="color: rgba(0, 0, 255, 1)">this</span>.microphoneCapturer = <span style="color: rgba(0, 128, 128, 1)">CapturerFactory</span>.CreateMicrophoneCapturer(<span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">);
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.microphoneCapturer.CaptureError += <span style="color: rgba(0, 0, 255, 1)">new</span> CbGeneric&lt;Exception&gt;(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.CaptureError);
      }

      </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.checkBox_soundCard.Checked)
      {
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">声卡采集器 【目前声卡采集仅支持vista以及以上系统】扬声器 属性 高级设置 16位 48000HZ(DVD音质)</span>
            <span style="color: rgba(0, 0, 255, 1)">this</span>.soundcardCapturer =<span style="color: rgba(0, 0, 0, 1)"><span style="color: rgba(0, 128, 128, 1)"> CapturerFactory</span>.CreateSoundcardCapturer();
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.soundcardCapturer.CaptureError += <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.CaptureError;
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span>.soundcardCapturer.SampleRate != <span style="color: rgba(128, 0, 128, 1)">48000</span><span style="color: rgba(0, 0, 0, 1)">)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> Exception(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">声卡采样率必须为48000HZ</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
            }
            audioSampleRate </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.soundcardCapturer.SampleRate;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.channelCount = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.soundcardCapturer.ChannelCount;
      }

      </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span>.checkBox_micro.Checked &amp;&amp; <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.checkBox_soundCard.Checked)
      {
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">混音器</span>
            <span style="color: rgba(0, 0, 255, 1)">this</span>.audioMixter = <span style="color: rgba(0, 128, 128, 1)">CapturerFactory</span>.CreateAudioMixter(<span style="color: rgba(0, 0, 255, 1)">this</span>.microphoneCapturer, <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.soundcardCapturer,
                SoundcardMode4Mix.DoubleChannel, </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">);
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.audioMixter.AudioMixed +=<span style="color: rgba(0, 0, 0, 1)"> audioMixter_AudioMixed;
            audioSampleRate </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.audioMixter.SampleRate;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.channelCount = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.audioMixter.ChannelCount;
      }

      </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.checkBox_micro.Checked)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.microphoneCapturer.AudioCaptured +=<span style="color: rgba(0, 0, 0, 1)"> audioMixter_AudioMixed;
      }
      </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.checkBox_soundCard.Checked)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.soundcardCapturer.AudioCaptured +=<span style="color: rgba(0, 0, 0, 1)"> audioMixter_AudioMixed;
      }
      </span><span style="color: rgba(0, 0, 255, 1)">#endregion</span>


      <span style="color: rgba(0, 0, 255, 1)">#region</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">开始采集</span>
      <span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.checkBox_micro.Checked)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.microphoneCapturer.Start();
      }
      </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.checkBox_soundCard.Checked)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.soundcardCapturer.Start();
      }

      </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.radioButton_camera.Checked)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.cameraCapturer.Start();
      }
      </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.radioButton_desktop.Checked)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.desktopCapturer.Start();
      }
      </span><span style="color: rgba(0, 0, 255, 1)">#endregion</span>   </pre>
</div>
<p>  <span style="font-family: &quot;Microsoft YaHei&quot;">开始采集后,我们就可以点击开始推流按钮,初始化推流器,将采集的数据推到流服务器上:</span></p>
<div class="cnblogs_code">
<pre>      <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">TODO 开始录制桌面,依据 声音复选框 来选择使用 声卡 麦克风 还是混合录制, 图像复选框来选择 图像的采集器</span>
      <span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)">
      {
            </span><span style="color: rgba(0, 0, 255, 1)">int</span> videoWidth = <span style="color: rgba(128, 0, 128, 1)">0</span>, videoHeight = <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">;
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.radioButton_desktop.Checked)
            {
                videoWidth </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.desktopCapturer.VideoSize.Width;
                videoHeight </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.desktopCapturer.VideoSize.Height;
            }
            </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
                videoWidth </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.defaultVideoSize.Width;
                videoHeight </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.defaultVideoSize.Height;
            }<br>
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.streamPusher.UpsideDown4RGB24 = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.streamPusher.<span style="color: rgba(255, 0, 0, 1)">Initialize</span>(<span style="color: rgba(128, 0, 0, 1)">"192.168.1.56</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(0, 0, 255, 1)">9000</span>, <span style="color: rgba(0, 0, 255, 1)">true</span>, <span style="color: rgba(0, 0, 255, 1)">this</span>.streamID, videoWidth, videoHeight, <span style="color: rgba(0, 0, 0, 1)">NPusher</span>.<span style="color: rgba(0, 128, 128, 1)">InputAudioDataType</span>.PCM, <span style="color: rgba(0, 0, 0, 1)">NPusher</span>.<span style="color: rgba(0, 128, 128, 1)">InputVideoDataType</span>.RGB24,<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.channelCount);
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.isPushing = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.button_start.Enabled = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.button_stop.Enabled = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.button3.Enabled = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
            </span><span style="color: rgba(0, 0, 255, 1)">this</span>.ShowStateMsg(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">推流中...</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
      }
      </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (Exception ee)
      {
            MessageBox.Show(ee.Message);
      }</span></pre>
</div>
<p> &nbsp; 上述代码中红色标记部分,即是初始化推流器:由于我们采集到的视频是H264数据,声音是PCM数据,所以,在初始化时,选择InputAudioDataType.PCM和InputVideoDataType.RGB24。</p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">&nbsp; &nbsp; &nbsp; 在采集时我们预定了对应的采集事件,采集到数据后我们就加到推流器中,它会自动将数据推到我们的Nginx服务器上:</span></p>
<div class="cnblogs_code">
<pre>      <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">采集到的视频或桌面图像</span>
      <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> Form1_ImageCaptured(Bitmap img)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span>.radioButton_camera.Checked)<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">显示摄像头的图像到窗体</span>
<span style="color: rgba(0, 0, 0, 1)">            {
                Image copy </span>=<span style="color: rgba(0, 0, 0, 1)"> ESBasic.Helpers.<span style="color: rgba(0, 128, 128, 1)">ImageHelper</span>.CopyImageDeeply(img);
                </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.DisplayVideo(copy);
            }
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.isPushing)
            {
                img.RotateFlip(RotateFlipType.Rotate180FlipY);
                </span><span style="color: rgba(0, 0, 255, 1)">byte</span>[] data =<span style="color: rgba(0, 0, 0, 1)"> ESBasic.Helpers.<span style="color: rgba(0, 128, 128, 1)">ImageHelper</span>.GetRGB24CoreData(img);
                </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.streamPusher.<span style="color: rgba(255, 0, 0, 1)">PushVideoFrame</span>(data);
            }            
      }

      </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">采集到的声卡、麦克风、声卡麦克风的混音数据</span>
      <span style="color: rgba(0, 0, 255, 1)">void</span> audioMixter_AudioMixed(<span style="color: rgba(0, 0, 255, 1)">byte</span><span style="color: rgba(0, 0, 0, 1)">[] audioData)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.isPushing)
            {
                </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span>.checkBox_soundCard.Checked &amp;&amp; !<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.checkBox_micro.Checked)
                {
                  audioData </span>= <span style="color: rgba(0, 128, 128, 1)">AudioHelper</span>.ConvertTo16kFrom48k(audioData ,<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.channelCount);
                }
                </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.streamPusher.<span style="color: rgba(255, 0, 0, 1)">PushAudioFrame</span>(audioData);
            }
      } </span></pre>
</div>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">  代码中标记为红色的部分PushVideoFrame和PushAudioFrame方法,即是将采集到的视频帧和音频帧推流到流服务器。</span></p>
<h2><span style="font-family: &quot;Microsoft YaHei&quot;">&nbsp;二.Nginx服务端部署</span></h2>
<p> <span style="font-family: &quot;Microsoft YaHei&quot;"> 这里可以在文末网盘下载服务端来部署到服务器上,其中有3个地方需要根据服务器的配置自行做修改</span></p>
<ol>
<li><span style="font-family: &quot;Microsoft YaHei&quot;">conf目录下nginx.conf 文件中 rtmp 端口 9000、http 端口8000 。</span></li>
<li><span style="font-family: &quot;Microsoft YaHei&quot;">html目录下index.html 文件中 设置流服务器的IP</span>
<div class="cnblogs_code">
<pre>src: <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">rtmp://192.168.1.56:9000/hls/</span><span style="color: rgba(128, 0, 0, 1)">"</span>+pqs._parameters.id[<span style="color: rgba(128, 0, 128, 1)">0</span>],    <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">将192.168.1.56改成流服务器的IP</span></pre>
</div>
</li>
<li>
<p>html目录下mobile.html&nbsp;文件中 也同样设置流服务器的IP&nbsp;</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">var</span> hls_url = <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">http://192.168.1.56:8000/hls/</span><span style="color: rgba(128, 0, 0, 1)">"</span> + pqs._parameters.id[<span style="color: rgba(128, 0, 128, 1)">0</span>] + <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">.m3u8</span><span style="color: rgba(128, 0, 0, 1)">"</span>; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">将192.168.1.56改成流服务器的IP</span></pre>
</div>
</li>
</ol>
<h2><span style="font-family: &quot;Microsoft YaHei&quot;">三.浏览器访问</span></h2>
<p>  <span style="font-family: &quot;Microsoft YaHei&quot;">PC的浏览器访问 http://192.168.1.56:8000/?id=aa01,其中aa01为流的ID。效果如下图</span></p>
<p>  <img src="https://img2020.cnblogs.com/blog/9005/202011/9005-20201124152028167-691624571.png" alt="" width="510" height="502" loading="lazy"></p>
<p>  <span style="font-family: &quot;Microsoft YaHei&quot;">手机浏览器访问 http://192.168.1.56:8000/mobile.html?id=aa01,其中aa01为流的ID。效果如下图</span></p>
<p>  <img src="https://img2020.cnblogs.com/blog/9005/202011/9005-20201124152042800-950412137.png" alt="" width="570" height="339" loading="lazy"></p>
<div>
<h2>&nbsp;<span style="font-family: &quot;Microsoft YaHei&quot;">四.源码下载</span></h2>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">&nbsp; &nbsp; &nbsp;(1)<span style="font-size: 16px"><strong> C#推流RTMP(摄像头、麦克风、桌面、声卡)-源码</strong></span></span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">&nbsp; &nbsp; &nbsp;(2)<strong><span style="font-size: 16px">Nginx部署版下载 网盘下载&nbsp;(提取码:&nbsp;<span style="color: rgba(255, 0, 0, 1)">1234</span>)&nbsp;&nbsp;</span></strong></span></p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<span style="font-size: 14px; font-family: &quot;Microsoft YaHei&quot;"> 注:查看Nginx运行状态可访问: <span style="color: rgba(51, 102, 255, 1)">http://192.168.1.56:8000/stat</span> 。</span></p>
<p>&nbsp;</p>
</div><br><br>
来源:https://www.cnblogs.com/zhuweisky/p/14024966.html
頁: [1]
查看完整版本: C#推流RTMP,摄像头、麦克风、桌面、声卡(附源码)