当前位置: 首页 > news >正文

webrtc快速入门——使用 WebRTC 拍摄静止的照片

文章目录

  • 使用 getUserMedia() 拍摄静态照片
    • HTML 标记
    • JavaScript 代码
    • 初始化
      • startup() 函数
        • 获取元素引用
        • 获取流媒体
      • 监听视频开始播放
        • 处理按钮上的点击
        • 包装 startup() 方法
      • 清理照片框
      • 从流中捕获帧
  • 例子代码
      • HTML代码
      • CSS代码
      • JavaScript代码
  • 过滤器
  • 使用特定设备

使用 getUserMedia() 拍摄静态照片

介绍了如何在 WebRTC 的支持下可以访问到电脑或者手机的摄像头并且使用它来拍摄照片。

本文介绍在支持 getUserMedia() 的计算机或手机上如何使用 navigator.mediaDevices.getUserMedia() 访问摄像机,并用其拍照。

HTML 标记

我们的 HTML 界面有两个主要的操作部分:流和捕获面板以及演示面板。它们俩都在它们自己的 <div> 中并排渲染,以便于添加样式和控制。

左边的面板包含两个组件:一个 <video> 元素,它将接收来自 navigator.mediaDevices.getUserMedia() 的流,以及用于用户点击以捕获视频帧的 <button>

<div class="camera"><video id="video">视频流目前不可用。</video><button id="startbutton">拍摄照片</button>
</div> 

这很简单,当我们进入 JavaScript 代码时,我们将看到它们是如何紧密联系在一起的。

接下来,我们有一个 <canvas> 元素,捕获的帧被存储到其中,可能以某种方式进行操作,然后转换为输出图像文件。通过使用样式 display:none 将画布保持隐藏,以避免画面的混乱——用户不需要看到这个中间过程。

我们还有一个 <img> 元素,我们将在其中绘制图像——这是让用户看到的最终显示。

<canvas id="canvas"> </canvas>
<div class="output"><img id="photo" alt="捕获的图像会显示在这里。" />
</div> 

这是所有相关的 HTML。其余的只是一些页面布局和提供一个返回页面链接的些许文本。

JavaScript 代码

现在来看看 JavaScript 代码。我们将把它分解成几个小的部分,使其更容易解释。

初始化

我们首先将整个脚本包装在匿名函数中,以避免使用全局变量,然后设置我们将要使用的各种变量。

(() => {const width = 320;    const height = 0;     const streaming = false;let video = null;let canvas = null;let photo = null;let startbutton = null; 

这些变量分别是:

width

无论输入视频的尺寸如何,我们将把所得到的图像缩放到宽度为 320 像素。

height

给定流的 width 和宽高比,计算出图像的输出高度。

streaming

指示当前是否有活动的视频流正在运行。

video

这将是页面加载完成后对 <video> 元素的引用。

canvas

这将是页面加载完成后对 <canvas> 元素的引用。

photo

这将在页面加载完成后引用 <img> 元素。

startbutton

这将引用用于触发捕获的 <button> 元素。我们会在页面加载完成之后得到。

startup() 函数

当页面加载完成时,提供给 EventTarget.addEventListenerstartup() 函数将会运行。此函数的作用是请求访问用户的网络摄像头,将用于输出的 <img> 初始化为默认状态,并建立从相机接收每帧视频所需的事件监听器,并在点击按钮捕获图像时作出反应。

获取元素引用

首先,我们参考我们需要访问的主要内容。

 function startup() {video = document.getElementById('video');canvas = document.getElementById('canvas');photo = document.getElementById('photo');startbutton = document.getElementById('startbutton'); 
获取流媒体

接下来的任务是获取媒体流:

navigator.mediaDevices.getUserMedia({ video: true, audio: false }).then((stream) => {video.srcObject = stream;video.play();}).catch((err) => {console.error(`An error occurred: ${err}`);}); 

在这里,我们调用 MediaDevices.getUserMedia() 并请求视频流(无音频)。它返回一个 promise,我们给它附加成功和失败情况下的回调方法。

成功回调接收一个 stream 对象作为输入。它是新视频的 <video> 元素的源。

一旦流被链接到 <video> 元素,我们通过调用 HTMLMediaElement.play() 开始播放。

如果打开流失败,则调用失败回调函数。在没有连接兼容的相机,或者用户拒绝访问时,则会发生这种情况。

监听视频开始播放

<video> 上调用 HTMLMediaElement.play() 之后,在视频流开始流动之前,有一段(希望简短)的时间段过去了。为了避免在此之前一直阻塞,我们为 video 加上一个 canplay 事件的监听器,当视频播放实际开始时会触发该事件。那时,视频对象中的所有属性都已基于流的格式进行配置。

video.addEventListener("canplay",(ev) => {if (!streaming) {height = (video.videoHeight / video.videoWidth) * width;video.setAttribute("width", width);video.setAttribute("height", height);canvas.setAttribute("width", width);canvas.setAttribute("height", height);streaming = true;}},false,
); 

这个回调什么都不做,除非它是第一次被调用;这是通过查看我们的 streaming 变量的值进行测试,这是第一次运行此方法时为 false

如果这是第一次运行,我们会根据视频的实际大小,video.videoWidth 和要渲染视频宽度的宽度(witdh)之间的大小差异来设置视频的高度。

最后,通过在视频和画布上调用 Element.setAttribute() 来设置视频和画布的宽度(witdh)和高度(height),以使得两者相互匹配。最后,我们将 streaming 变量设置为 true,以防止我们无意中再次运行此设置代码。

处理按钮上的点击

为了在每次用户点击 startbutton 时捕获静态照片,我们需要向按钮添加一个事件监听器,以便在发出 click 事件时被调用:

startbutton.addEventListener("click",(ev) => {takepicture();ev.preventDefault();},false,
); 

这个方法很简单:它只是调用我们的 takepicture() 函数,在从流中捕获帧的部分中定义,然后在接收的事件上调用 Event.preventDefault(),以防止点击被多次处理。

包装 startup() 方法

startup() 方法中只有两行代码:

这就是我们调用 clearphoto() 方法的地方,我们将在下面的清理照片框部分进行描述。

清理照片框

清理照片框包括创建一个图像,然后将其转换为可以显示最近捕获的帧的 <img> 元素使用的格式。该代码如下所示:

function clearphoto() {const context = canvas.getContext("2d");context.fillStyle = "#AAA";context.fillRect(0, 0, canvas.width, canvas.height);const data = canvas.toDataURL("image/png");photo.setAttribute("src", data);
} 

我们首先得到对我们用于屏幕外渲染的隐藏的 <canvas> 元素的引用。接下来,我们将 fillStyle 设置为 #AAA(相当浅的灰色),并通过调用 fillRect() 来填充整个画布。

最后在此功能中,我们将画布转换为 PNG 图像,并调用 photo.setAttribute() 以使我们捕获的静止框显示图像。

从流中捕获帧

最后一个定义的功能是整个练习的重点:takepicture() 函数,其捕获当前显示的视频帧的作业将其转换为 PNG 文件,并将其显示在捕获的帧框中。代码如下所示:

function takepicture() {const context = canvas.getContext("2d");if (width && height) {canvas.width = width;canvas.height = height;context.drawImage(video, 0, 0, width, height);const data = canvas.toDataURL("image/png");photo.setAttribute("src", data);} else {clearphoto();}
} 

正如我们需要处理画布内容的情况一样,我们首先得到隐藏画布的 2D 绘图上下文

然后,如果宽度和高度都是非零(意味着至少有潜在有效的图像数据),我们将画布的宽度和高度设置为与捕获帧的宽度和高度相匹配,然后调用 drawImage() 将视频的当前帧绘制到上下文中,用帧图像填充整个画布。

一旦画布包含捕获的图像,我们通过调用它的 HTMLCanvasElement.toDataURL() 将它转换为 PNG 格式; 最后,我们调用 photo.setAttribute() 来使我们捕获的静态框显示图像。

如果没有可用的有效图像(即宽度和高度均为 0),则通过调用 clearphoto() 清除捕获帧框的内容。

例子代码

HTML代码

<div class="contentarea"><h1>MDN——navigator.mediaDevices.getUserMedia(): 静态照片拍摄演示</h1><p>此示例演示了如何使用内置的网络摄像头来获取媒体流,并从中获取图像,以使用该图像来创建一个PNG 图像。</p><div class="camera"><video id="video">视频流目前不可用。</video><button id="startbutton">拍摄照片</button></div><canvas id="canvas"> </canvas><div class="output"><img id="photo" alt="捕获的图像会显示在这里。" /></div><p>访问我们的文章:<ahref="https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC_API/Taking_still_photos">使用 getUserMedia() 拍摄静态照片</a>以详细了解此处使用的技术。</p>
</div>

CSS代码

#video {border: 1px solid black;box-shadow: 2px 2px 3px black;width: 320px;height: 240px;
}#photo {border: 1px solid black;box-shadow: 2px 2px 3px black;width: 320px;height: 240px;
}#canvas {display: none;
}.camera {width: 340px;display: inline-block;
}.output {width: 340px;display: inline-block;vertical-align: top;
}#startbutton {display: block;position: relative;margin-left: auto;margin-right: auto;bottom: 32px;background-color: rgba(0, 150, 0, 0.5);border: 1px solid rgba(255, 255, 255, 0.7);box-shadow: 0px 0px 1px 2px rgba(0, 0, 0, 0.2);font-size: 14px;font-family: "Lucida Grande", "Arial", sans-serif;color: rgba(255, 255, 255, 1);
}.contentarea {font-size: 16px;font-family: "Lucida Grande", "Arial", sans-serif;width: 760px;
} 

JavaScript代码

(() => {// The width and height of the captured photo. We will set the// width to the value defined here, but the height will be// calculated based on the aspect ratio of the input stream.const width = 320; // We will scale the photo width to thislet height = 0; // This will be computed based on the input stream// |streaming| indicates whether or not we're currently streaming// video from the camera. Obviously, we start at false.let streaming = false;// The various HTML elements we need to configure or control. These// will be set by the startup() function.let video = null;let canvas = null;let photo = null;let startbutton = null;function showViewLiveResultButton() {if (window.self !== window.top) {// Ensure that if our document is in a frame, we get the user// to first open it in its own tab or window. Otherwise, it// won't be able to request permission for camera access.document.querySelector(".contentarea").remove();const button = document.createElement("button");button.textContent = "查看以上示例代码的实时演示";document.body.append(button);button.addEventListener("click", () => window.open(location.href));return true;}return false;}function startup() {if (showViewLiveResultButton()) {return;}video = document.getElementById("video");canvas = document.getElementById("canvas");photo = document.getElementById("photo");startbutton = document.getElementById("startbutton");navigator.mediaDevices.getUserMedia({ video: true, audio: false }).then((stream) => {video.srcObject = stream;video.play();}).catch((err) => {console.error(`An error occurred: ${err}`);});video.addEventListener("canplay",(ev) => {if (!streaming) {height = video.videoHeight / (video.videoWidth / width);// Firefox currently has a bug where the height can't be read from// the video, so we will make assumptions if this happens.if (isNaN(height)) {height = width / (4 / 3);}video.setAttribute("width", width);video.setAttribute("height", height);canvas.setAttribute("width", width);canvas.setAttribute("height", height);streaming = true;}},false,);startbutton.addEventListener("click",(ev) => {takepicture();ev.preventDefault();},false,);clearphoto();}// Fill the photo with an indication that none has been// captured.function clearphoto() {const context = canvas.getContext("2d");context.fillStyle = "#AAA";context.fillRect(0, 0, canvas.width, canvas.height);const data = canvas.toDataURL("image/png");photo.setAttribute("src", data);}// Capture a photo by fetching the current contents of the video// and drawing it into a canvas, then converting that to a PNG// format data URL. By drawing it on an offscreen canvas and then// drawing that to the screen, we can change its size and/or apply// other changes before drawing it.function takepicture() {const context = canvas.getContext("2d");if (width && height) {canvas.width = width;canvas.height = height;context.drawImage(video, 0, 0, width, height);const data = canvas.toDataURL("image/png");photo.setAttribute("src", data);} else {clearphoto();}}// Set up our event listener to run the startup process// once loading is complete.window.addEventListener("load", startup, false);
})();

过滤器

由于我们通过从 <video> 元素中抓取帧来捕获用户网络摄像头的图像,因此我们可以非常轻松地将过滤器和有趣的效果应用于视频。事实证明,使用 filter 属性应用于元素的任何 CSS 过滤器都会影响捕获的照片。这些过滤器可以从简单(使图像黑白)到复杂(高斯模糊和色调旋转)。

你可以使用例如 Firefox 开发者工具的样式编辑器来播放此效果;有关如何执行此操作的详细信息,请参阅编辑 CSS 过滤器。

使用特定设备

如果需要,你可以将允许的视频源限定为特定的设备或特定的一组设备。要做到这一点,请调用 MediaDevices.enumerateDevices。若返回的 promise 兑现了一个 MediaDeviceInfo 对象(描述了可用的设备)数组,可以从中选取一个你想要允许的设备,并将对应的 deviceIdMediaTrackConstraints 对象的 deviceId 作为参数传入到 getUserMedia() 中。

相关文章:

webrtc快速入门——使用 WebRTC 拍摄静止的照片

文章目录 使用 getUserMedia() 拍摄静态照片HTML 标记JavaScript 代码初始化startup() 函数获取元素引用获取流媒体 监听视频开始播放处理按钮上的点击包装 startup() 方法 清理照片框从流中捕获帧 例子代码HTML代码CSS代码JavaScript代码 过滤器使用特定设备 使用 getUserMedi…...

预约按摩app软件开发定制足浴SPA上们服务小程序

同城按摩小程序是一种基于地理位置服务的小程序&#xff0c;它可以帮助用户快速找到附近的按摩师&#xff0c;并提供在线预约、评价、支付等功能。用户可以通过手机或者其他移动设备访问同城按摩小程序&#xff0c;实现足不出户就能预约到专业的按摩服务。 一、同城按摩小程序的…...

jenkins出错与恢复

如果你的jenkins出现了如下图所示问题&#xff08;比如不能下载插件&#xff0c;无法保存任务等&#xff09;&#xff0c;这个时候就需要重新安装了。 一、卸载干净jenknis 要彻底卸载 Jenkins&#xff0c;您可以按照以下步骤进行操作&#xff1a; 1、停止 Jenkins 服务&…...

ssh免密登录的原理RSA非对称加密的理解

RSA非对称加密&#xff0c;是采用公钥加密私钥解密的原则。 举个例子SSH的免密登录 SSH免密登录是通过使用公钥加密技术实现的。以下是SSH免密登录的原理&#xff1a; 1. 生成密钥对&#xff1a;首先&#xff0c;在客户端上生成一对密钥&#xff0c;包括一个私钥和一个公钥。私…...

【监督学习】基于合取子句进化算法(CCEA)和析取范式进化算法(DNFEA)解决分类问题(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

力扣每日一题41:缺失的第一个正数

题目描述&#xff1a; 给你一个未排序的整数数组 nums &#xff0c;请你找出其中没有出现的最小的正整数。 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,0] 输出&#xff1a;3示例 2&#xff1a; 输…...

OpenCV与mediapipe实践

1. 安装前准备 开发环境&#xff1a;vscode venv 设置vscode, 建立项目&#xff0c;如: t1/src, 用vscode打开&#xff0c;新建终端Terminal&#xff0c;这时可能会有错误产生&#xff0c;解决办法&#xff1a; 运行命令&#xff1a;Set-ExecutionPolicy -ExecutionPolicy …...

【css拾遗】粘性布局实现有滚动条的情况下,按钮固定在页面底部展示

效果&#xff1a; 滚动条滚动过程中&#xff0c;按钮的位置位于手机的底部 滚动条滚到底部时&#xff0c;按钮的位置正常 这个position:sticky真的好用&#xff0c;我原先的想法是利用滚动条滚动事件去控制&#xff0c;没想到css就可以解决 <template><view class…...

git 创建并配置 GitHub 连接密钥

前记&#xff1a; git svn sourcetree gitee github gitlab gitblit gitbucket gitolite gogs 版本控制 | 仓库管理 ---- 系列工程笔记. Platform&#xff1a;Windows 10 Git version&#xff1a;git version 2.32.0.windows.1 Function&#xff1a; git 创建并配置 GitHub…...

使用Premiere、PhotoShop和Audition做视频特效

今天接到一个做视频的任务&#xff0c;给一个精忠报国的视频&#xff0c;要求&#xff1a;   ①去掉人声&#xff0c;就是将唱歌的人声去掉&#xff0c;只留下伴奏&#xff1b;   ②截图视频中的横幅&#xff0c;做一个展开的效果&#xff0c;类似卷纸慢慢展开&#xff1b;…...

vueday01——动态参数

我们现在知道了 v-bind:的语法糖是: v-on:的语法糖是 我们现在来尝试一下&#xff0c;定义一个动态参数模拟点击事件按钮 <div :id"idValue" ref"myDiv">我是待测div{{ resultId }}</div> <button v-on:[eventName]"doSomething&…...

双向链表C语言版本

1、声明链表节点操作函数 linklist.h #ifndef LINKLIST_H__ #define LINKLIST_H__ #include <stdio.h> #include <stdlib.h> #include <stdbool.h>//#define TAIL_ADD #define HEAD_ADD typedef int LinkDataType; // 构造节点 struct LinkNode {LinkDataTy…...

visual studio安装时候修改共享组件、工具和SDK路径方法

安装了VsStudio后,如果自己修改了Shared路径&#xff0c;当卸载旧版本&#xff0c;需要安装新版本时发现&#xff0c;之前的Shared路径无法进行修改&#xff0c;这就很坑爹了&#xff0c;因为我运行flutter程序的时候&#xff0c;报错找不到windows sdk的位置&#xff0c;所以我…...

Motorola IPMC761 使用边缘TPU加速神经网络

Motorola IPMC761 使用边缘TPU加速神经网络 人工智能(AI)和机器学习(ML)正在塑造和推进复杂的自动化技术解决方案。将这些功能集成到硬件中&#xff0c;解决方案可以识别图像中的对象&#xff0c;分析和检测模式中的异常或找到关键短语。这些功能对于包括但不限于自动驾驶汽车…...

EM@直线的参数方程

文章目录 abstract直线参数方程从运动轨迹的角度从普通方程转换导参数方程向量法 参数方程间的转换从第3型转化为第2型方程组例 abstract 平面直线的参数方程的3种表示形式直线参数方程间的转换 直线参数方程 以下从不同角度推导直线参数方程分别记为第1,2,3形式参数方程 从…...

day08-注册功能、前端登录注册页面复制、前端登录功能、前端注册功能

1 注册功能 补充(开放文件夹内) 2 前端登录注册页面复制 4 前端注册功能 1 注册功能 # 分析前端&#xff1a;携带数据格式 {mobile:,code:,password}后端&#xff1a;-1 视图类---》注册方法-2 序列化类---》校验&#xff0c;保存&#xff08;表中字段多&#xff0c;传的少---…...

rust: function

///file: nestd.rs ///ide: RustRover 233.8264.22 /// /// /// /***自定义函数*/ pub fn function() {println!("called my::nested::function()"); }#[allow(dead_code)] fn private_function() {println!("called my::nested::private_function()"); }/…...

零代码编程:用ChatGPT批量下载谷歌podcast上的播客音频

谷歌podcast有很多播客音频&#xff0c;如何批量下载到电脑呢&#xff1f; 以这个播客为例&#xff1a; https://podcasts.google.com/feed/aHR0cHM6Ly9oYWRhcnNoZW1lc2guY29tL2ZlZWQvcG9kY2FzdC8?saX&ved0CAkQlvsGahcKEwi4uauWsvKBAxUAAAAAHQAAAAAQAg 查看网页源代码&a…...

nginx.4——正向代理和反向代理(七层代理和四层代理)

1、正向代理反向代理 nginx当中有两种代理方式 七层代理(http协议) 四层代理(tcp/udp流量转发) 七层代理 七层代理&#xff1a;代理的是http的请求和响应。 客户端请求代理服务器&#xff0c;由代理服务器转发给客户端http请求。转发到内部服务器&#xff08;可以单台&#…...

基于RuoYi-Flowable-Plus的若依ruoyi-nbcio支持自定义业务表单流程(三)

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 相应的后端也要做一些调整 1、启动流程修改如下&#xff1a; /*** 启动流程实例*/private R startProce…...

华硕笔记本终极电池拯救指南:用G-Helper实现智能充电与健康修复

华硕笔记本终极电池拯救指南&#xff1a;用G-Helper实现智能充电与健康修复 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops. Control tool for ROG Zephyrus G14, G15, G16, M16, Flow X13, Flow X16, TUF, Strix, Scar and other models …...

别光看原理了!用STM32F407从零撸一个四轴飞控代码(附完整工程)

用STM32F407从零构建四轴飞控代码实战指南 当你在论坛上看到别人分享的无人机飞行视频&#xff0c;是否也曾心动想亲手打造一套自己的飞控系统&#xff1f;市面上大多数教程止步于理论讲解&#xff0c;真正落实到代码层面的少之又少。本文将带你用STM32F407开发板&#xff0c;…...

Ludusavi:你的游戏进度守护神,三分钟搞定跨平台存档备份

Ludusavi&#xff1a;你的游戏进度守护神&#xff0c;三分钟搞定跨平台存档备份 【免费下载链接】ludusavi Backup tool for PC game saves 项目地址: https://gitcode.com/gh_mirrors/lu/ludusavi 你是否曾在电脑崩溃后&#xff0c;发现数百小时的游戏进度瞬间归零&…...

Go语言的context.WithCancel取消信号传播与资源清理在分布式系统中的协调

Go语言的context.WithCancel取消信号传播与资源清理在分布式系统中的协调 在分布式系统中&#xff0c;任务的取消与资源清理是确保系统稳定性和高效性的关键挑战。Go语言通过context包提供了优雅的解决方案&#xff0c;尤其是context.WithCancel机制&#xff0c;能够实现跨组件…...

OpenClaw操作录制:ollama-QwQ-32B学习人工流程生成自动化脚本

OpenClaw操作录制&#xff1a;ollama-QwQ-32B学习人工流程生成自动化脚本 1. 为什么需要操作录制功能 上周我在整理月度运营报告时&#xff0c;突然意识到自己正在重复第7次执行完全相同的操作流程&#xff1a;打开三个数据源表格→复制特定列→粘贴到汇总表→生成折线图→导…...

CANdb++ Editor高效使用技巧:5个隐藏功能大幅提升dbc编辑效率

CANdb Editor高效使用技巧&#xff1a;5个隐藏功能大幅提升dbc编辑效率 在汽车电子开发领域&#xff0c;Vector的CANdb Editor堪称dbc文件编辑的行业标准工具。大多数工程师都能熟练使用其基础功能&#xff0c;但真正的高手往往掌握着那些鲜为人知的"秘密武器"。本文…...

MX28智能舵机RS485底层驱动开发实战

1. MX28智能舵机底层驱动技术解析&#xff1a;基于RS485总线的嵌入式控制实现1.1 技术定位与工程价值MX28是Robotis公司推出的第二代高精度智能舵机&#xff08;Smart Actuator&#xff09;&#xff0c;采用RS485半双工差分总线通信&#xff0c;支持位置、速度、扭矩闭环控制及…...

财务银行对账费时间?RPA自动对接流水,10分钟对完1个月账

RPA自动化银行对账的优势传统手工对账通常需要财务人员逐笔核对银行流水和企业账目&#xff0c;耗时费力且易出错。RPA&#xff08;机器人流程自动化&#xff09;技术可实现银行流水与企业账务系统的自动对接&#xff0c;大幅提升效率。10分钟完成1个月账目核对已成为现实。RPA…...

终极指南:如何用Save Image as Type一键转换网页图片格式

终极指南&#xff1a;如何用Save Image as Type一键转换网页图片格式 【免费下载链接】Save-Image-as-Type Save Image as Type is an chrome extension which add Save as PNG / JPG / WebP to the context menu of image. 项目地址: https://gitcode.com/gh_mirrors/sa/Sav…...

从零搭建SRS流媒体服务器:实现RTMP推拉流的实战部署指南

1. 为什么选择SRS搭建流媒体服务器&#xff1f; 最近几年直播和实时视频的需求爆发式增长&#xff0c;很多开发者都在寻找轻量高效的流媒体服务器方案。我测试过不少开源方案&#xff0c;最终发现SRS&#xff08;Simple Realtime Server&#xff09;是最适合个人和小团队自建的…...