编程技术文章分享与教程

网站首页 > 技术文章 正文

从图像到视频:Web Codecs API编码技术解析

hmc789 2024-11-11 12:49:40 技术文章 2 ℃

初探Web Codecs API 三

前言

在之前的文章中,咱们简单的介绍了解码相关的东西,这一节咱们来简单聊聊编码相关的东西。

编码的目的就是为了压缩,去除空间、时间维度的冗余。

这里又不得不提起前面所说的I 帧P 帧B 帧IDR 帧

众所周知,视频是连续的图像序列,由连续的帧构成,一帧就是一幅图像。

直接存储图片需要占用大量的存储空间,而且传输也不方便。

为了解决这个问题,于是视频编码出现了,它的作用就是将一张一张的图片压缩成一个视频格式,以便通过特定的解码器对其进行解码、播放。

介绍

其大概过程如下:

编码器

首先来说,我们要明确我们使用什么编码格式,然后再对编码器进行设置。

编码器是 WebCodecs API 中的 VideoEncoder 类, 在我们对其实例化后,就需要通过 configure 方法进行配置;

我们可以通过 isConfigSupported 方法去验证配置是否支持;

Config 的参数如下:

codec:包含有效编解码器字符串的字符串;
width: 比特率调整前输出 Encoded VideoChunk 的宽度,单位为像素(可选);
height:比特率调整前输出 Encoded VideoChunk 的高度,单位为像素(可选);
displayWidth:显示时每个输出 Encoded VideoChunk 的预期显示宽度,单位为像素(可选);
displayHeight:显示时每个输出 Encoded VideoChunk 的预期显示高度,单位为像素(可选);
hardwareAcceleration:编解码器硬件加速方法的提示;
bitrate:含编码视频的平均比特率,单位为比特每秒;
framerate:预期的帧速率,单位为每秒帧数;
alpha:是否丢弃 VideoFrame 输入的 alpha 分量;
scalabilityModeWebRTC中定义的编码可伸缩性模式标识符;
bitrateMode:比特率模式(可选);
latencyMode: 配置此编解码器延迟行为的值(可选);

关于配置参数的具体内容请查阅 MDN

我们使用的配置为:

{
    codec: "avc1.420034",
    width: WIDTH, // 960
    height: HEIGHT,// 540
    hardwareAcceleration: "prefer-software",
    avc: { format: "annexb" }, // 这个好像有点问题
}

这样我们就配置好了编码器

编码器是为了对帧进行编码,那么我们的帧从哪里来呢?

就要用到 Web Codecs API 中的 VideoFrame 类;

创建帧对其实例化就行;

其参数如下:

image:新视频帧的图像数据的图像,可以是 SVGImageElement、HTMLVideoElement、HTMLCanvasElement、ImageBitmap、OffscreenCanvas 或其他 VideoFrame。
options:{
    duration: 帧的持续时间,单位为微秒(可选);
    timestamp: 帧的时间戳,单位为微秒;
    alpha:如何处理alpha通道(可选);
    visibleRect:VideoFrame可见矩形的对象(可选);
    displayWidth:应用宽高比调整后显示的VideoFrame的宽度,单位为像素(可选);
    displayHeight:应用宽高比调整后显示的VideoFrame的高度,单位为像素(可选);
}

我们为了方便 参数只选择了必要的 timestamp

const frame = new VideoFrame(canvas, { timestamp });

到这里我们的编码所需的图像就准备好了。

调用videoEncoder.encode();

其参数如下:

frame:VideoFrame 对象,
options:{
    keyFrame: 是否为编码为一个关键帧;
    // 还存在对编码器的配置,与比特率模式(bitrateMode)相关
}

这样,我们就得到了编码后的 Encoded VideoChunk 对象;编码到此结束。

生成视频文件

但是这玩意怎么变成视频文件呢?

编码数据是需要容器保存的;这里又是我们的老朋友 MP4Box.js,帮助我们封装,该部分与编码无关,所以不在我们讨论的范围内,我们只做简单介绍;

this.mp4boxfile = MP4Box.createFile();

this.videoEncodingTrackOptions = {
    timescale: 1000000,
    brands: ['isom', 'iso2', 'avc1', 'mp41'],
    avcDecoderConfigRecord: null,
};

this.encodingVideoTrack = this.mp4boxfile.addTrack(this.videoEncodingTrackOptions);

// 在Encoded的output 中:

const buffer = new ArrayBuffer(chunk.byteLength)
chunk.copyTo(buffer);
that.idx += 1

const videoEncodingSampleOptions = {
    duration: 1e6 / 30,
    dts: that.idx * 1e6 / 30,
    cts: that.idx * 1e6 / 30,
    is_sync: chunk.type === 'key',
};

that.mp4boxfile.addSample(that.encodingVideoTrack, buffer, videoEncodingSampleOptions);

// 结束时就可以通过save生成MP4
this.videoEncoder.flush().then(() => {
    this.mp4boxfile.flush();
    this.mp4boxfile.save('test.mp4')
});

但是这样生成的视频只能在浏览器中播放,原因的话是因为编码器的描述可能不匹配,导致解码的时候找不到正确的解码器,暂时也没有时间去排查和搞清楚,但是在浏览器中直接播放是可以的。

附解码及封装demo 代码;

class VideoDemo {
  constructor() {
    this.frameIndex = 0
    this.init();
  }

  /**
   * @description: 初始化 MP4
   * @return {*}
   */
  initMp4() {
    // MP4Box.js
    this.mp4boxfile = MP4Box.createFile();
    this.videoEncodingTrackOptions = {
      timescale: 1000000,
      brands: ['isom', 'iso2', 'avc1', 'mp41'],
      avcDecoderConfigRecord: null,
    };
    this.encodingVideoTrack = this.mp4boxfile.addTrack(this.videoEncodingTrackOptions);
  }
  /**
   * @description: 初始化
   * @return {*}
   */
  init() {
    this.initMp4();
    const that = this
    this.idx = 0;
    this.output = {
      output(chunk, metadata) {
        const buffer = new ArrayBuffer(chunk.byteLength)
        chunk.copyTo(buffer);
        that.idx += 1
        const videoEncodingSampleOptions = {
          duration: 1e6 / 30,
          dts: that.idx * 1e6 / 30,
          cts: that.idx * 1e6 / 30,
          is_sync: chunk.type === 'key',
        };

        that.mp4boxfile.addSample(that.encodingVideoTrack, buffer, videoEncodingSampleOptions);
      },
      error(error) {
        console.log(error);
      },
    }

    this.config = {
      codec: "avc1.420034",
      width: 960,
      height: 540,
      hardwareAcceleration: "prefer-software",
      avc: { format: "annexb" },
    }

    this.videoEncoder = new VideoEncoder(this.output);
    this.videoEncoder.configure(this.config);

  }

  /**
   * @description: 需要解码时
   * @param {*} canvas
   * @param {*} timestamp
   * @return {*}
   */
  onFrame(canvas, timestamp) {
    if (this.frameIndex > 1000) return;
    // 假设只要 1000帧
    if (this.frameIndex === 1000) {
      this.videoEncoder.flush().then(() => {
        this.mp4boxfile.flush();
        this.mp4boxfile.save('test.mp4')
      });
      console.log('end')
      return true;
    }
    const frame = new VideoFrame(canvas, { timestamp });
    // 30 帧一个关键帧
    const insert_keyframe = this.frameIndex % 30 === 0;
    this.videoEncoder.encode(frame, { keyFrame: insert_keyframe });
    // 销毁帧,避免占用大量内存
    frame.close();
    this.frameIndex += 1;
  }
}

– 欢迎点赞、关注、转发、收藏【我码玄黄】,各大平台同名。

Tags:

标签列表
最新留言