前端canvas混流,并实现视频画中画操作按钮

719 阅读6分钟

前言

在webRTC的应用中,会出现用户一边看到视频画面,一边需要干其他事情的情况,这个时候就适合采用视频画中画的功能。但是现在浏览器只能对一个视频画面实现视频画中画的功能,有时候需要同时看到屏幕共享或者多个人的视频画面的功能,这个时候我们采用混流的方案,将想要的视频画面混成一个,再通过画中画API弹出即可实现对应的效果。

视频画中画功能

画中画Picture-in-Picture

画中画 API允许网站总是在其它窗口之上创建一个浮动的视频,以便用户在其他内容站点或者设备上的应用程序交互时可以继续播放媒体。相信很多人都已经用过了这个功能,对原生video标签右键弹出菜单也能进入该模式,但是我们想自定义的时候就需要用到对应的API才行

现在提供了两个API来实现对应的功能,一个是请求画中画,一个是退出画中画

  1. requestPictureInPicture
  2. exitPictureInPicture

第一个API是由HTMLVideoElement 元素提供的 requestPictureInPicture() 方法,这个方法会发出异步请求,并以画中画的模式显示视频。第二个就是退出画中画模式。 我们可以去caniuse检查一下兼容性

image.png

在主流能支持webRTC的浏览器中已经是可以有比较高的兼容性了。

下面是一个开启视频画中画的例子

开启画中画的条件

  1. 首先我们要确认用户的浏览器是否支持画中画功能,或者用户没有关闭画中画功能,可以使用如下代码来判断是否可以开启画中画功能,这个属性是一个布尔值。
document.pictureInPictureEnabled
  1. 用户的页面没有通过Feature-Policy来禁用画中画功能
  2. video标签具有流数据
  3. video标签添加disablePictureInPicture属性

事件回调

前面说过了,用户也可以通过右键来进入画中画模式,那么这个时候我们如果需要知道当前用户是否处于画中画模式,可以使用事件监听

enterpictureinpicture event

enterpictureinpicture 事件会在 HTMLVideoElement 成功进入画中画模式时触发。

videoElement.addEventListener('enterpictureinpicture', event => { });

leavepictureinpicture event

leavepictureinpicture 事件会在 HTMLVideoElement 成功离开画中画模式时触发。

videoElement.addEventListener('leavepictureinpicture', event => { });

画中画模式操作音视频

现在已经可以弹出视频画面了,但是基于RTC场景,光弹出还是不能满足用户场景需求,比如用户想在弹出的画中画里面操作摄像头或者麦克风。这里我们就需要使用setActionHandler这个API了,由于我们要使用togglecamera,togglemicrophone,hangup,但是这三个action只兼容Chromium系列的浏览器,在Firefox和Safari是不支持的,我们只能在chrome里面使用这个操作音视频的API了。

首先我们要先在dom加载完成为这三个动作设置一个监听器

// type可选的值"hangup" | "nexttrack" | "pause" | "play" | "previoustrack" | "seekbackward" | "seekforward" | "seekto" | "skipad" | "stop" | "togglecamera" | "togglemicrophone";
navigator.mediaSession.setActionHandler(type, callback)

设置完成之后,开启视频画中画,然后只要点击对应的媒体控制项,便能执行回调方法,这里是chrome的截图,

image.png

能看到有控制麦克风、摄像头和挂断的选项,但是现在还有一个问题。我们的摄像头是有开关状态的,但是现在我想关闭后,这个icon也要变成关闭的状态才是对的,这里就需要新的API了。

setCameraActive和setMicrophoneActive

这两个方法可以设置当前用户的摄像头或者麦克的静音状态,当用户操作了麦克风或者摄像头的开关,调用对应的API更新一下即可改变icon的状态。这个API的兼容性比较差,需要较新版本的浏览器才能够支持

image.png

这里有一个完整的例子展示了如何进行画中画和画中画期间的开关摄像头麦克风操作

  • 请点击右上角进入详情查看

我们实现了对应的画中画功能,接下来我们要看看如何来实现混流的功能

通过canvas实现混流的功能

实现思路解析

首先我们要知道canvas是可以通过一个叫captureStream的API来获取一个实时的画面。接下来我们要将获取到的视频流绘制到canvas里面,可以将视频流挂载在video标签里面,然后通过canvas的drawImage的API绘制到canvas里面,由于绘制只是绘制一帧画面,所以需要通过setTimeout或者requestAnimationFrame来更新画面。绘制完成之后需要通过captureStream来获取流,接下来挂载在video标签即完成混流操作。

屏幕共享和摄像头混流实现步骤

以下代码基于vue3

  1. 首先我们先去获取用户的音视频
const videoStream = ref<MediaStream>()
const screenStream = ref<MediaStream>()

// 获取视频
const getVideo = async () => {
  videoStream.value = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true
  })
}

// 获取屏幕共享
const getScreenStream = async () => {
  screenStream.value = await navigator.mediaDevices.getDisplayMedia()
}

2.接下来是创建一个canvas,将他们绘制到一起

const canvasEl = ref<HTMLCanvasElement>(document.createElement('canvas'))
const canvasContext = ref<CanvasRenderingContext2D>()
const isStopDraw = ref(false)

// 混合屏幕共享和视频
const startMixScreenAndVideo = async () => {
  // 定义canvas的宽高
  canvasEl.value.width = 800;
  canvasEl.value.height = 448;

  canvasContext.value = canvasEl.value.getContext('2d')!;

  // 创建好video标签
  const screenEl = genVideo(screenStream.value!);
  const videoEl = genVideo(videoStream.value!, 200, 112);

  // 将视频标签绘制到canvas上  
  drawToCanvasScreenAndVideo(screenEl, videoEl);
}

// 将流放在video标签里面播放
const genVideo = (stream: MediaStream, width = 800, height = 448) => {
  const videoEl = document.createElement('video');
  // videoEl.muted = true;
  // videoEl.volume = 0;
  videoEl.autoplay = true;
  videoEl.srcObject = stream;
  videoEl.width = width;
  videoEl.height = height;
  videoEl.play();
  return videoEl;
}

const drawToCanvasScreenAndVideo = (screenEl: HTMLVideoElement, videoEl: HTMLVideoElement) => {
  // 在不断更新视频画面递归的时候,如果已经中断了需要停止
  if (isStopDraw.value) return;

  // 将屏幕共享绘制满canvas
  canvasContext.value!.drawImage(screenEl, 0, 0, 800, 448);
  // 将摄像头绘制到canvas的右下角
  canvasContext.value!.drawImage(videoEl, 600, 336, 200, 112);

  // 不断更新canvas的画面,这里也可以采用requestAnimationFrame来实现
  setTimeout(drawToCanvasScreenAndVideo.bind(undefined, screenEl, videoEl), 100);
}

3.接下来就是获取流,放到video标签里面播放即可

const video = ref<HTMLVideoElement>();
const stream = canvasEl.value.captureStream();
if (video.value) {
  video.value.srcObject = stream!;
}

下图是合并之后的效果,右下角是我的obs虚拟摄像头 image.png

给视频流添加名字

有时候我们还需要给流添加一些文字来应对一些场景,这个时候也可以用canvas的绘制文字的能力,我们可以对获取的视频流挂载一个onDraw方法,当我们不断更新canvas的时候,可以判断有没有这个onDraw函数,如果有的话我们执行对应的函数来绘制文字。

// 将流挂载一个onDraw方法
handlerStreamCallBack = (stream: MediaStream) => {
  const streamHandler = videoStream.value! as customMediaStream;
 /**
  * 放在对象里面
  * @param context canvas的context
  * @param x
  * @param y
  * @param width
  * @param height
  * @param text 需要绘制的文字
  */
  streamHandler.onDraw = (context: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, text: string) => {
    context.font = '30px "微软雅黑"';
    context.fillStyle = "red";
    context.fillText(text, x + 50, y + 50)
  }
  return streamHandler;
}

效果如下图所示 image.png

这里有一个完整的例子来展示是如何进行操作的

  • 请点击右上角进入详情查看