webrtc之拍照片

137 阅读6分钟

上一章节介绍了关于设备的检测,这节就来看看怎么使用设备。先小试牛刀,实现一个拍照功能。

之前也已经介绍过了如何使用摄像头,想一下,使用摄像头拍照是不是也能实现呢?当然是可以实现的。只是需要一点小小的帮助,比如要用到canvas

首先,要理解一个事情,视频就是很多张连续的图片快速的播放,由于间隙非常小,肉眼无法分辨出来,所以才有了连续动画的视频效果。从媒体设备中获取到的视频流就是一张张连续的图片,然后在video标签中播放就成了视频。

要实现拍照功能,就是从那一系列连续的图片中选出想要的那张即可。

此时,你可能会有点疑问,这么多张连续的图片,我要怎么才能拿到想要的那张呢?非常简单,就像是用手机拍照一样,摆好姿势,保持不动,然后点击拍照,最后那张就是了。什么?你说你想要中间的那几张?那请你翻到下一节,看看怎么录制视频,然后将录制好的视频放到剪辑软件上剪辑。

根据以上描述,可将拍照分为三个步骤:

  1. 拿到视频
  2. 拿到图片
  3. 保存图片

首先从摄像头/外部视频流中拿到视频流,放到video中进行播放,然后利用canvas进行绘制图片,最后利用a标签来下载图片。非常简单。

拿到视频

这个是已经讲过了,在《摄像头的基本使用》中有具体代码介绍。这里就简单列出来代码吧,也不多说,还不会的往前翻翻,多操作几遍。

这里还加上了模式的选择,有多种滤镜可以使用。

  <div class="content">
    <div class="box1">
      <video muted autoplay playsinline ref="videoRef" class="video1"></video>
      <div>
        选择模式:
        <el-select v-model="type">
          <el-option
            v-for="item in options"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
      </div>
      <el-button @click="takePhoto">拍照</el-button>
    </div>
    <div class="box2">
      <canvas ref="canvasRef"></canvas>
      <el-button @click="save">保存</el-button>
    </div>
  </div>
  
  // 在拍照完之后,再选择滤镜处理,最后进行保存
  // 喜欢的可以再拓展拓展
  const options = [
  {
    label: "无",
    value: "none",
  },
  {
    label: "模糊",
    value: "blur",
  },
  {
    label: "灰色",
    value: "grayscale",
  },
  {
    label: "反色",
    value: "invert",
  },
  {
    label: "色斑",
    value: "sepia",
  },
];

const constraints = {
   video: { width: 450, height: 300 },
  audio: false,
};

const init = async () => {
  try {
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    videoRef.value.srcObject = stream;
  } catch (error) {
    console.error(error);
  }
};

这部分就是拿到视频流并在video标签中播放。约束条件可以自由选择,想要更清晰点可以将分辨率放大。音频的话,可有可无,没啥影响。

拿到图片

这个就要用到canvas了,需要亿点点相关知识。没有使用过canvas的,可以去看看这里canvas的功能强大,但这里只需要使用到其中一个小功能drawImage

先简单介绍下drawIamge

drawImage(image, x, y, width, height)

image参数有几种方式:

  • HTMLImageElement: 由Image()函数构造出来的,或者任何的<img>元素
  • HTMLVideoElement:用<video>元素作为图片源,从当前视频中抓取到当前帧作为一个图像。
  • HTMLCanvasElement: 用另一个<canvas>元素作为图片源,套娃处理。

x,y参数则是代表canvas 里的起始坐标,一般都是0,0

width,height 参数则是代表宽高,即canvas的大小。

这里我们重点要用到的就是image参数中的HTMLVideoElement,正好适用于我们的场景,直接传入video元素即可。

还有一个方法toDataURL,也是将要用到的:

canvas.toDataURL(type, encoderOptions);

type参数可选,表示图片格式,默认为 image/png,也可以是 image/jpeg 或 image/webp encoderOptions参数也是可选,在指定图片格式为 image/jpeg 或 image/webp 的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。

注意,如果canvas的高度或宽度是 0,那么会返回字符串“data:,”,要记得设置宽高。

这个方法会返回一个包含 data URL的字符串,就是 base64格式。

const canvas = document.getElementById("canvas");
const dataURL = canvas.toDataURL();
console.log(dataURL);
// "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNby
// blAAAADElEQVQImWNgoBMAAABpAAFEI8ARAAAAAElFTkSuQmCC"

下面来实操看看效果,代码如下:

<div class="box2">
    <canvas ref="canvasRef"  style="display: none;"></canvas>
    <img v-if="takePhotoUrl" :src="takePhotoUrl" alt="Captured Photo">
</div>

// 这是滤镜效果,在绘制canvas是要传入
const filerMap = {
  blur: "blur(3px)",
  grayscale: "grayscale(1)",
  invert: "invert(1)",
  sepia: "sepia(1)",
};

// 按下拍照时执行
const takePhoto = () => {
  const context = canvasRef.value.getContext("2d");
  const videoWidth = videoRef.value.videoWidth;
  const videoHeight = videoRef.value.videoHeight;
  canvasRef.value.width = videoWidth;
  canvasRef.value.height = videoHeight;
  context.filter = filerMap[type.value];
  context.drawImage(videoRef.value, 0, 0, videoWidth, videoHeight);
  takePhotoUrl.value = canvasRef.value.toDataURL();
};

当调用了drawImage后,再将绘制的图片变成base64格式,保存在takePhotoUrl.value中,以便保存时使用。

这里有一个点要注意,就是要给canvas设置宽高,下面这两行别忘了,不然绘制出来的图片只会是其中一部分。我刚开始用的时候就忘了设置了,还奇怪,咋一直都是绘制了不全的。

canvasRef.value.width = videoWidth;
canvasRef.value.height = videoHeight;

然后还用到了context.filter,给canvas添加滤镜效果。当然,还可以添加更多效果,可自行拓展。

效果如下:

image.png

保存图片

照片都拍完了,那肯定得保存到系统里好好欣赏才行。浏览器当然也提供了方法让我们能保存图片,主要是使用a标签。代码如下:

<div class="box2">
    <canvas ref="canvasRef"  style="display: none;"></canvas>
    <img v-if="takePhotoUrl" :src="takePhotoUrl" alt="Captured Photo">
    <el-button @click="save">保存</el-button>
</div>

const save = () => {
  if (!takePhotoUrl.value) {
    return;
  }
  const el = document.createElement("a");
  el.download = "photo"; // 设置下载的文件名,默认是'下载'
  el.href = takePhotoUrl.value; // 将takePhotoUrl赋值给a标签的href属性
  document.body.appendChild(el);
  el.click();
  el.remove(); // 移除a标签
};

在点击时动态创建一个a标签,然后设置下载的文件名,并且将通过canvas生成的takePhotoUrl赋值给a标签的href属性。然后再自动调用点击事件进行下载,最后再将a标签给移除掉。

这里download属性指示浏览器该下载而不是打开该文件,同时该属性值即下载时的文件名。但是,要想使这个属性生效,必须是同源下才可以。即同域名、同协议、同端口号时,设置的download属性才可以生效,不然就是默认值了。

效果:

image.png

photo (2).png

可以看到下载下来的图片效果还是挺不错的,是不是感觉还挺好玩的,赶快试试吧。

小节

这节内容主要是介绍了如何利用摄像头来进行拍照,并且将其保存在本地。过程还是挺简单的,重点在于第二个步骤,利用canvas来绘制图片,掌握了这个,其他的都好理解。

但是,这节还有一个非常重要的点需要知道,为什么可以利用canvas来绘制图片呢?根本在于从媒体设备中拿到的视频流,其实就是一张张连续、不间断播放的图片,然后canvas在调用drawImage时会从video元素中拿到当前正在显示的图片进行绘制,就是这句话HTMLVideoElement:用video元素作为图片源,从当前视频中抓取到当前帧作为一个图像

利用媒体设备拍照片只是一个非常基础的功能。在此基础上,还能实现比如与模型结合,替换背景图或者替换人脸;或是与其他媒体流混合在一起播放等,都是需要用到拍照片中的前两个步骤。区别在于拍照只是利用canvas绘制一次,而其他的则是要连续不断的绘制。这些功能都会在之后的章节进行介绍,当然,你也可以先动手尝试一下。

下一节来介绍如何录制视频,并保存到本地。

项目地址

前端:gitee.com/yoboom/webr…

后端:gitee.com/yoboom/webr…

项目功能如下:

image.png

感兴趣的可以直接去体验一下,欢迎star和提pr