那些被忽略但很好用的 Web API / ImageCapture

疫情时代,视讯串流当头,用视讯镜头来做个线上摄影吧!

自从疫情爆发後,各行各业也开始进行居家办公,使得视讯软件及相关技术开始受重视,身为前端,我们也可以拿视讯镜头来做些好玩有趣的东西,而 ImageCapture 就是其中一个可以运用在这里的 API。


MediaStream

在认识 ImageCapture 之前我们必须要先了解 MediaStream,浏览器将获取的影音资讯称之为「流」(Stream),其中流又包含了「轨」(Track),如影像轨、音讯轨,而我们可以透过向使用者获取授权并透过装置来取得这些影音资讯,进而达到我们想要的操作目的。

# Navigator.mediaDevices.getUserMedia

而我们要取得 MediaStream 的手段就是要先向使用者获取设备的授权,这时候就要使用 getUserMedia

navigator.mediaDevices.getUserMedia({ video: true })
  .then(mediaStream => {
    /* use the stream */
  })
  .catch(err => {
    /* handle the error */
  });

当我们呼叫 getUserMedia 时,必须传入一个称为 constraints 的参数,该参数为一个物件,当中需要表示你想取得的 Track,例如上面我们就是传入 { video: true },来取得视讯轨。

getUserMedia 会回传 Promise 给我们,当使用者同意授权後就可以在 then 的 Callback 中取得 MediaStream。

 

# MediaStream.getVideoTracks

当我们取得 MediaStream 後,我们还需要再取得当中的 Track,之後才可以透过 ImageCapture 来操作,这时候就需要使用 MediaStream 自身的 method getVideoTracks

navigator.mediaDevices.getUserMedia({ video: true })
  .then(mediaStream => {
    const videoTrack = mediaStream.getVideoTracks()[0];
  })
  .catch(err => {
    console.log(err)
  });

要注意的是,由於一个 MediaStream 中未必只有一个 VideoTrack,所以 getVideoTracks 回传的会是阵列,记得要透过 index 索引出来。

 

ImageCapture

知道如何取得 MediaStreamTrack 後,就可以来认识 ImageCapture 了,它可以让我们建立一个图像撷取器,只要提供一个有效的 VideoTrack 给 ImageCapture 就可以进行图像的撷取:

const imageCapture = new ImageCapture(videoTrack);

# ImageCapture.takePhoto

当我们为一个 ImageCapture 绑定了 VideoTrack 後,我们就可以透过 ImageCapture 底下的 methods 来进行图像撷取了:

let imageCapture;
navigator.mediaDevices.getUserMedia({ video: true })
  .then(mediaStream => {
    const videoTrack = mediaStream.getVideoTracks()[0];
    imageCapture = new ImageCapture(videoTrack);
  })
  .catch(err => {
    console.log(err)
  });

document.querySelector("button").addEventListener("click", function() {
  imageCapture.takePhoto().then(blob => {
    console.log(blob)
  });
})

呼叫 takePhoto 後,它会回传 Promise,并且我们能在 then 的 Callback 中取得截图的 Blob 物件。

 

# ImageCapture.grabFrame

再来要介绍的则是 grabFrame,它和 takePhoto 一样是撷取 videoTrack 的影像,差别在於它回传的是 ImageBitmap 物件,而这种物件的好处是可以直接拿来画在 Canvas 上。

imageCapture.takePhoto().then(blob => {
  console.log(blob)
});

imageCapture.grabFrame().then(imageBitmap => {
  console.log(imageBitmap)
});

Blob 物件可以够过 createImageBitmap(blob) 来转换成 ImageBitmap 物件

 

实际运用

那最後我们就透过今天认识的 API 来实际做个视讯截图摄影吧,首先先准备几个按钮以及 videocanvas

<div>
  <button onclick="openCamera()">开启镜头</button>
  <button onclick="capture()">撷取画面</button>
</div>
<video></video>
<canvas></canvas>

再来是在 openCamera 的时候使用 getUserMediagetVideoTracks 来取得 MediaStreamTrack 并建立 ImageCapture。处此之外,我们还设定了 video.srcObject,如此一来我们就可以够过 <video> 标签来预览视讯画面。

var video = document.querySelector('video');
var canvas = document.querySelector('canvas');
var context = canvas.getContext('2d');
var videoTrack;
var imageCapture;

// 开启镜头
function openCamera() {
  navigator.mediaDevices
    .getUserMedia({ video: true })
    .then((stream) => {
      // 取得视讯轨并建立 imageCapture
      videoTrack = stream.getVideoTracks()[0];
      imageCapture = new ImageCapture(videoTrack);
      // 将媒体流设定到 <video> 中显示播放
      video.srcObject = stream;
      video.play();
    })
    .catch((err) => {
      console.log(err);
    });
}

//撷取画面
function capture() {
  imageCapture
    .takePhoto()
    .then(blob => {
      // 将 Blob 转成 ImageBitmap
      return createImageBitmap(blob)
    })
    .then(imageBitmap => {
      // 绘制在 canvas 上
      const { width, height } = imageBitmap;
      const ratio = video.videoWidth / width;
      canvas.setAttribute('width', width * ratio);
      canvas.setAttribute('height', height * ratio);
      context.drawImage(imageBitmap, 0, 0, width * ratio, height * ratio);
    });
}

最後只要在 capture 的时候透过 takePhoto 进行截图,并将 Blob 转成 ImageBitmap 後丢到 <canvas> 里,就大功告成罗。

完整的 code 我就放在 这里,大家可以看看实际效果。

 

其实视讯的操作没有大家想像的那麽困难,简单几个 API 就可以做到,今天做的镜头截图其实就可以做在会员的大头照设定,让使用者可以直接利用视讯镜头拍摄大头照,相当便利。


<<:  【Day22】 Transformer 新手包 (二)

>>:  Day 8 - 使用 Order API 建立测试订单

Ruby on Rails 方法的存取控制

如果你曾经在别的程序语言写过OOP,你也许对类别的方法存取限制不会太陌生。类别的方法存取限制常见的主...

Day21 - Sort

大家好我是长风青云。今天是铁人赛21天。我们算是半只脚踏入演算法的阶段。(因为Sort的部分DS和A...

第 5 天 还我漂漂拳| property binding、interface

前情提要 将英雄们显示在 Mat-Card 上後,我们进一步地要对英雄资料做点加工,并且制作英雄详细...

Day.2 什麽是时间、空间复杂度?

时间复杂度(Time complexity) 我们要怎麽知道一个程序要跑多久? 正常来说要真的执行下...

Day 10 - 转换人生跑道

简介 casting 就是资料型态之间的转换。 例如把 A type 转换成 B type。 但是这...