纯前端实现视频混流

简介: 随着前端音视频技术的不断成熟越来越多的直播平台开始提供网页开播的直播方式,不需要再使用 OBS 或者各家的直播伴侣。

1682517766.png


概述


随着前端音视频技术的不断成熟越来越多的直播平台开始提供网页开播的直播方式,不需要再使用 OBS 或者各家的直播伴侣。

网页开播的视频流传输方式是 WebRTC,这一点没有啥难度,可以提升使用体验的点在于能否像 OBS 以及直播伴侣那样在视频流中添加文字、图片等图层以及自由的布局。

要实现这些功能需要进行混流,OBS 等都是推流之前进行的混流,直播的水印等基本都是云端实现的混流。我们网页开播选择在前端进行混流,为了了解前端混流的方案我查了很多资料。

经过几天的搜索,找到了比较靠谱的两种方式:

  • 第三方依赖库 MultiStreamsMixer
  • 将要推流的视频内容布局在 canvas 中,使用 canvas.captureStream 捕获视频流进行推送。

下面我们将依次来实现这两种混流方式。


MultiStreamsMixer


MultiStreamsMixer 有两种方式引入,使用 CDN 引入

<script src="https://www.webrtc-experiment.com/MultiStreamsMixer.js"></script>
复制代码

或者 npm 安装

$ npm install multistreamsmixer
复制代码

唯一的缺陷在于没有 TS 的类型支持,如果使用在 Typescript 的项目中需要自己声明模块类型。

这里只是 demo,我们直接使用 CDN 导入,用最原生的 HTML+JavaScript 来实现。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>混流测试</title>
</head>
<body>
  <video autoplay muted id="mixStream" width="800" height="600"></video>
  <button onclick="captureScreen()">获取屏幕</button>
  <button onclick="captureCamera()">获取摄像头</button>
  <button onclick="mixStream()">混流</button>
  <script src="https://www.webrtc-experiment.com/MultiStreamsMixer.js"></script>
  <script src="./index.js" type="text/javascript"></script>
</body>
</html>
复制代码

然后创建 index.js 并实现这三个函数

let screenStream; // 屏幕捕获流
let cameraStream; // 摄像头流
const video = document.getElementById('mixStream') // 视频元素
function gotLocalMediaStream(mediaStream) {
  video.srcObject = mediaStream;
}
async function captureScreen() {
  // await deviceCheck()
  try {
    screenStream = await navigator.mediaDevices.getDisplayMedia({
      video: {
        cursor: "always",
        frameRate: 60
      },
      audio: {
        echoCancellation: true,
        noiseSuppression: true,
        sampleRate: 44100
      }
    })
  } catch (error) {
    console.log("navigator.getDisplayMedia error: ", error)
  }
}
async function captureCamera() {
  try {
    cameraStream = await navigator.mediaDevices
      .getUserMedia({
        video: {
          frameRate: 15,
          facingMode: 'user',
        },
        audio: true // 音频
      })
  } catch (error) {
    console.log("navigator.getUserMedia error: ", error)
  }
}
function mixStream() {
  screenStream.fullcanvas = true; // 铺满全屏
  screenStream.width = screen.width; // 视频宽度
  screenStream.height = screen.height; // 视频高度
  cameraStream.width = parseInt((20 / 100) * screenStream.width);
  cameraStream.height = parseInt((20 / 100) * screenStream.height);
  cameraStream.top = screenStream.height - cameraStream.height - 40; // 上边距
  cameraStream.left = screenStream.width - cameraStream.width - 40; // 下边距
  const mixer = new MultiStreamsMixer([screenStream, cameraStream]);
  mixer.frameInterval = 1; 
  mixer.startDrawingFrames(); // 开始绘制帧
  gotLocalMediaStream(mixer.getMixedStream()) // 播放视频流
}
复制代码

这个库提供的 API 非常简单,就只需要设置视频的位置和宽高,然后调用混流 API 就可以了,最后记得设置帧率并开始渲染帧。

主要 API 如下:

  1. getMixedStream: (function) 返回媒体流;
  2. frameInterval: (property) 设置帧间隔;
  3. startDrawingFrames: (function) 开始生成视频流;
  4. resetVideoStreams: (function) 用新的视频流替换所有现有的视频流;
  5. releaseStreams: (function) 停止当前的混流;
  6. appendStreams: (function) 添加新的流(任何时间都可以)。

效果预览

1682517853(1).png

这种方案实现混流还是很简单的。不足之处是只能基于视频流进行混流,如果想要添加一些文字图片之类的目前我还没有找到实现方案。

canvas

核心 API MediaStream = canvas.captureStream(frameRate);

  • frameRate 可选,设置双精准度浮点值为每个帧的捕获速率。如果未设置,则每次画布更改时都会捕获一个新帧。如果设置为0,则会捕获单个帧。

基于 canvas 我们可以实现更丰富的视频流内容,例如文字标题,图片水印等(不得不说,canvas 是真滴强大)。

1682517873(1).png

实现思路:

  1. 捕获屏幕/摄像头,生成视频元素并播放;
  2. 将视频元素在 canvas 中进行绘制;
  3. 添加额外的渲染内容;
  4. 捕获 canvas 的视频流。

下面我们将给予这个思路来对之前的混流方式进行改造。

捕获摄像头和屏幕分享的过程和之前是一样的,区别在于这里我们需要生成 video 元素,因为需要为 canvas 提供渲染素材

function genVideo(stream, width = ScreenWidth, height = ScreenHeight) {
  const videoEl = document.createElement('video');
  videoEl.autoplay = true;
  videoEl.srcObject = stream;
  videoEl.width = width;
  videoEl.height = height;
  videoEl.play();
  return videoEl;
}
async function captureScreen() {
  try {
    screenStream = await navigator.mediaDevices.getDisplayMedia({
      video: {
        cursor: "always",
        frameRate: 60
      },
      audio: false
    })
    screenVideo = genVideo(screenStream);
  } catch (error) {
    console.log("navigator.getDisplayMedia error: ", error)
  }
}
async function captureCamera() {
  try {
    cameraStream = await navigator.mediaDevices
      .getUserMedia({
        video: { // 视频
          frameRate: 15,
          facingMode: 'user',
        },
        audio: false // 音频
      })
    cameraVideo = genVideo(cameraStream, cameraWidth, cameraHeight)
  } catch (error) {
    console.log("navigator.getUserMedia error: ", error)
  }
}
复制代码

之所以要生成 video 元素是因为在 canvas 中绘制图片必须要指定的类型,否则会报错

1682517895(1).png

我们需要不断的在 canvas 中绘制最新的视频帧

/**
 * 
 * @param {HTMLVideoElement} screenEl 屏幕捕获视频元素
 * @param {HTMLVideoElement} cameraEl 摄像头视频元素
 */
function drawToCanvasScreen(screenEl, cameraEl) {
  canvasContext.drawImage(screenEl, 0, 0)
  canvasContext.drawImage(cameraEl, cameraLeft, cameraTop, cameraWidth, cameraHeight)
  // // 设置字体
  // canvasContext.font = "40px Microsoft YaHei";
  // canvasContext.fillStyle = "#409eff";
  // canvasContext.textAlign = "center";
  // // 添加文字和位置
  // canvasContext.fillText("custom title", 100, 50);
  setTimeout(drawToCanvasScreen.bind(undefined, screenEl, cameraEl), 100);
}
复制代码

如果需要绘制其他元素可以在这个过程中添加,如添加文字标题等

最后我们需要从 canvas 中捕获视频流。

function mixStreamCanvas() {
  drawToCanvasScreen(screenVideo, cameraVideo)
  gotLocalMediaStream(mixCanvas.captureStream())
}
复制代码

当然 canvas 的方案也不是完美的, 使用 canvas 捕获的视频流只有画面没有声音,需要再使用 Audio API 捕获音频轨道,或者在捕获视频流绘制到 canvas 之前分理出音频轨道。

效果预览

1682517919(1).png


总结


两种方案各有优缺点,需要根据个人需求去斟酌

  • MultiStreamsMixer
  • 优点
  • 使用方便;
  • 保留原声,不再需要手动额外操作声音;
  • 缺点
  • 自定义视频流之外的内容不是很方便(结合 canvas 方案,将额外的内容转为视频流进行混流);
  • 没有 TS 类型支持;
  • canvas
  • 优点
  • 自定义内容元素,包括文本和图片等;
  • 原生 API 不需要安装额外的依赖,产物体积更小;
  • 缺点
  • captureStream 无法捕获声音,需要自己手动处理;

补充:今天看了一下源码,MultiStreamMixer 也是基于 canvas 进行的封装,所以本质上前端混流只有一种方式——canvas,前者只是封装了一些常用的操作。

1682517946(1).png

相关文章
|
Web App开发 移动开发 JavaScript
【前端用法】HTML5 Video标签如何屏蔽右键视频另存为的js代码以及如何禁用浏览器控件,Video 禁止鼠标右键下载
【前端用法】HTML5 Video标签如何屏蔽右键视频另存为的js代码以及如何禁用浏览器控件,Video 禁止鼠标右键下载
359 0
|
2月前
|
前端开发 JavaScript 编译器
不走弯路,纯前端如何把图片导出成视频!
【10月更文挑战第3天】不走弯路,纯前端如何把图片导出成视频!
50 3
|
7月前
|
前端开发
前端input上传文件获取视频或音频的时长
前端input上传文件获取视频或音频的时长
233 0
|
5月前
|
前端开发
ElementPlus卡片如何能够一行呈四,黑马UI前端布局视频资料,element样式具体的细节无法修改,F12找到那个位置,可能在其他组件写了错误,找到那个位置,围绕着位置解决问题最快了,卡片下边
ElementPlus卡片如何能够一行呈四,黑马UI前端布局视频资料,element样式具体的细节无法修改,F12找到那个位置,可能在其他组件写了错误,找到那个位置,围绕着位置解决问题最快了,卡片下边
|
5月前
|
前端开发 PHP 数据格式
【附带效果视频】php接口给前端返回流式数据,php使用event-stream进行数据推送,循环一次输出一次
【附带效果视频】php接口给前端返回流式数据,php使用event-stream进行数据推送,循环一次输出一次
182 0
|
7月前
|
XML JSON 前端开发
【Flutter前端技术开发专栏】Flutter中的图片、视频与网络资源加载
【4月更文挑战第30天】Flutter是谷歌的开源前端框架,因其高性能、流畅UI和多端运行能力受开发者喜爱。本文聚焦于Flutter中的资源加载:使用`Image`组件加载静态、网络和本地图片;通过`video_player`库加载和播放视频;利用`http`包进行网络资源请求。掌握这些技巧将有助于提升Flutter应用的开发效率和质量。
49 0
【Flutter前端技术开发专栏】Flutter中的图片、视频与网络资源加载
|
7月前
|
编解码 前端开发 JavaScript
纯前端也能实现视频转GIF
纯前端也能实现视频转GIF
|
前端开发 JavaScript
【前端用法】前端JS获取视频时长的写法
【前端用法】前端JS获取视频时长的写法
133 0
|
7月前
|
资源调度 前端开发 JavaScript
推荐一款可以自动创建视频的前端Ract框架
推荐一款可以自动创建视频的前端Ract框架
|
7月前
|
存储 小程序 API
对象存储OSS产品常见问题之前端直传视频获取视频的长度获得多少秒如何解决
对象存储OSS是基于互联网的数据存储服务模式,让用户可以安全、可靠地存储大量非结构化数据,如图片、音频、视频、文档等任意类型文件,并通过简单的基于HTTP/HTTPS协议的RESTful API接口进行访问和管理。本帖梳理了用户在实际使用中可能遇到的各种常见问题,涵盖了基础操作、性能优化、安全设置、费用管理、数据备份与恢复、跨区域同步、API接口调用等多个方面。
487 0

热门文章

最新文章