一、获取摄像头数据并渲染


通过navigator.mediaDevices.getUserMedia获取到摄像头的数据,失败返回错误信息。代码如下:

let constraints = {
  video : {
    width: 640, // 宽带
    height: 480,  // 高度
    frameRate:15, // 帧率
  },
  audio : false 
}

// 获取到video元素,用于播放视频轨道
const video = document.getElementById('video');
let track;

navigator.mediaDevices.getUserMedia(constraints)
  .then((stream) => {
    track = stream.getVideoTracks()[0];
    video.srcObject = stream;
    video.play();
  })
  .catch((error) => {
    console.error(error);
  });

通过stream.getVideoTracks()[0];从媒体流中获取到视频轨道数据。

这里还要注意一些浏览器不能兼容navigator.mediaDevices.getUserMedia方法。

二、将视频轨道数据转换为可操作的Uint8ClampedArray数据


// 创建一个canvas元素,用于绘制视频轨道
const canvas = document.getElementById('canvas');
canvas.width = 640;
canvas.height = 480;
const canvasContext = canvas.getContext('2d');
// 在canvas上绘制视频帧
canvasContext.drawImage(video, 0, 0, canvas.width, canvas.height);
// 获取像素数据
const imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height);
const pixelData = imageData.data;

其中pixeData为RGBA数据,Uint8ClampedArray类型数据。

三、将RGBA数据转换为YUV420数据


YUV420介绍

YUV为三个分量,Y表示明亮度;而U和V表示的则是色度,作用是描述影像色彩及饱和度,用于指定像素的颜色。其主要的采用方式为,YUV4:4:4,YUV4:2:2,YUV4:2:0。其中YUV4:2:0为每四个Y公用一组UV,该方式最为常用。

YUV420又分为YUV420p和YUV420sp,而YUV420p又分为YV12和I420;

内存存储

I420:Y1,Y2,Y3,Y4,U1,V1 存储四个像素点。

即就可以计算出一个YUV420在内存中存放的大小:Y = width * hight,U = Y / 4,V = Y / 4,即一张YUV图像的存储空间就是 width * height *3 /2。

转换公式

 // Convert RGB to YUV
 let y = 0.299 * r + 0.587 * g + 0.114 * b;
 let u = -0.14713 * r - 0.28886 * g + 0.436 * b;
 let v = 0.615 * r - 0.51498 * g - 0.10001 * b;

综上就可得出转换方法:

function toYUV(rgba, width, height) {
  let yuv = new Uint8ClampedArray(width * height * 3 / 2);
  let yIndex = 0;
  let uIndex = width * height;
  let vIndex = uIndex + (uIndex / 4);
 
  for (let j = 0; j < height; j++) {
    for (let i = 0; i < width; i++) {
      let r = rgba[(j * width + i) * 4];
      let g = rgba[(j * width + i) * 4 + 1];
      let b = rgba[(j * width + i) * 4 + 2];
     
      // Convert RGB to YUV
      let y = 0.299 * r + 0.587 * g + 0.114 * b;
      let u = -0.14713 * r - 0.28886 * g + 0.436 * b;
      let v = 0.615 * r - 0.51498 * g - 0.10001 * b;
     
      // Write the YUV values to the output array
      yuv[yIndex++] = Math.max(0, Math.min(255, y));;
     
      // Write the U and V values every other pixel
      if (i % 2 == 0 && j % 2 == 0) {
        yuv[uIndex++] = Math.max(0, Math.min(255, u + 128));
        yuv[vIndex++] = Math.max(0, Math.min(255, v + 128));
      }
    }
  }
  return yuv;
}

该方法遍历输入的RGBA数组中的每个像素,对每个像素进行RGB到YUV的转换,并将结果写入一个新的Uint8ClampedArray中。然后,它按照YUV420格式重新组织数据,将Y值存储在输出数组的前半部分,将U和V值每隔一个像素存储在输出数组的后半部分。

在转换RGB到YUV时,该函数使用的是ITU-R BT.601的标准系数。在将U和V值存储回输出数组之前,它将它们平移了128个单位,以将它们转换为无符号8位值。最后,函数将整个图像的宽度和高度作为参数,并使用它们来计算输出数组中每个分量的索引。

请注意,此函数假定输入RGBA数据是一个按行存储的一维数组,并且每个像素使用4个字节(即RGBA格式)。如果输入数据格式不同,需要根据情况修改该函数。

四、调用转换方法并保存到文件播放


function render() {
    // 在canvas上绘制视频帧
    canvasContext.drawImage(video, 0, 0, canvas.width, canvas.height);

    // 获取像素数据
    const imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height);
    const pixelData = imageData.data;
    let yuv = toYUV(pixelData, canvas.width, canvas.height);

    fs.appendFile('./video.yuv', yuv, (err) => {
        if (err) {
            console.error(err);
            return;
        }
    });

    requestAnimationFrame(render);
}

// render();
setTimeout(render, 500);

该方法将每一帧的RGBA数据转换为YUV数据并保存到video.yuv文件中。使用FFmpeg、YUVplayer等播放器可以播放yuv文件。

请注意,应设置播放器对应的格式和宽高。