# Web 音视频(三)在浏览器中创建视频

Web 音视频目录

音视频工作流程

在 WebCodecs 之前,由于编解码能力的缺失,几乎无法在纯浏览器中编辑、创建视频。
WebCodecs 补齐了编解码能力,相当于在浏览器中提供了视频创作能力。

预计 WebCodecs 将会像 HTML5 技术(Video、Audio、MSE...)一样对用户习惯带来巨大改变,HTML5 作用于视频消费端,WebCodecs 作用于视频生产端。

本章介绍如何在浏览器中创建视频,如何解析视频请阅读上一章

你可以跳过原理介绍,直接查看 WebAV 生成 MP4 示例

# 采集与编码

前面的文章已介绍过 WebCodecs 使用 VideoFrame、AudioData 来描述音视频原始数据。
请参考 WebCodecs 核心 API

常见的音视频源有:MediaStream(摄像头、麦克风、分享屏幕)、Canvas、Video 标签、文件流等...

第一步,将这些源对象转换成 VideoFrame、AudioData 对象,方法有:

  1. 使用 MediaStreamTrackProcessor (opens new window) 将 MediaStream 转换为 ReadableStream<VideoFrame> 、 ReadableStream<AudioData>MDN 有示例代码
  2. 直接将 Canvas、Video 标签传递给 VideoFrame 的构建函数 new VideoFrame(canvas)
  3. 由解码器(VideoDecoder 、 AudioDecoder)解码本地或网络文件,得到 VideoFrame、AudioData
  4. AudioContext (opens new window) 获取音频原始数据创建 AudioData 对象,后续【音频数据处理】文章再介绍

第二步,将 VideoFrame、AudioData 传入编码器(VideoEncoder、AudioEncoder)

const encoder = new VideoEncoder({
  error: console.error,
  output: (chunk, meta) => {
    // chunk: EncodedVideoChunk 等待封装
    // meta 在下一步封装 SDK 创建轨道时需要
  },
});
encoder.configure({
  codec: 'avc1.4D0032', // H264
  width: 1280,
  height: 720,
});

let timeoffset = 0;
let lastTime = performance.now();
setInterval(() => {
  const duration = (performance.now() - lastTime) * 1000;
  encoder.encode(
    new VideoFrame(canvas, {
      // 这一帧画面,持续 33ms,duration 单位 μs
      duration,
      timestamp: timeoffset,
    })
  );
  timeoffset += duration;
}, 33);

WARNING

当高频调用 encoder.encode 时应根据当前编码器的队列大小 encoder.encodeQueueSize 决定是否需要暂停,队列中的 VideoFrame 数量过多会爆掉显存,导致性能极其低下

# 封装

编码器(VideoEncoder、AudioEncoder)将一帧帧原始数据编码(压缩)后会输出 EncodedVideoChunk、EncodedAudioChunk 对象,然后由封装程序将他们封装(muxing)成对应格式的视频文件。

我们继续使用 mp4box.js 来演示封装 mp4 文件。

MP4 将一个编码后的数据包抽象为 Sample,与 EncodedVideoChunk、EncodedAudioChunk 对象一一对应。
MP4 将不同类型的数据(音频、视频)分组抽象为 Track,分组管理不同类型的 Sample。

代码示例
const file = mp4box.createFile()
// 创建视频轨道
const videoTrackId = file.addTrack({
  timescale: 1e6,
  width: 1280,
  height: 720,
  // meta 来原于 VideoEncoder output 的参数
  avcDecoderConfigRecord: meta.decoderConfig.description
})
// 创建音频轨道
const audioTrackId = file.addTrack({
  timescale: 1e6,
  samplerate: 48000,
  channel_count: 2,
  type: 'mp4a' // AAC
  // meta 来原于 AudioEncoder output 的参数
  description: createESDSBox(meta.decoderConfig.description)
})

/**
 * EncodedAudioChunk | EncodedVideoChunk 转换为 MP4 addSample 需要的参数
 */
function chunk2MP4SampleOpts (
  chunk: EncodedAudioChunk | EncodedVideoChunk
): SampleOpts & {
  data: ArrayBuffer
} {
  const buf = new ArrayBuffer(chunk.byteLength)
  chunk.copyTo(buf)
  const dts = chunk.timestamp
  return {
    duration: chunk.duration ?? 0,
    dts,
    cts: dts,
    is_sync: chunk.type === 'key',
    data: buf
  }
}

// VideoEncoder output chunk
const videoSample =  chunk2MP4SampleOpts(chunk)
file.addSample(videoTrackId, videoSample.data, videoSample)

// AudioEncoder output chunk
const audioSample =  chunk2MP4SampleOpts(chunk)
file.addSample(audioTrackId, audioSample.data, audioSample)

以上代码是为了将主要过程与 API 建立对应关系,实际上还需要比较复杂的流程控制逻辑,以及进一步了解 mp4 格式知识才能编写出完整可运行的程序。

TIP

  • addSample 前必须保证音视频轨道(addTrack)都已经创建完成
  • 创建音频轨道需要传递 description(esds box),否则某些播放器将无法播放声音

# 生成文件流

使用 mp4box.js 封装编码器输出的数据,我们持有的是一个 MP4File 对象(mp4box.createFile()),将 MP4File 对象转换成 ReadableStream 可以非常方便地写入本地文件、上传到服务器。

注意释放内存引用,避免内存泄露

代码不算太长,全部贴出来了
export function file2stream(
  file: MP4File,
  timeSlice: number,
  onCancel?: TCleanFn
): {
  stream: ReadableStream<Uint8Array>;
  stop: TCleanFn;
} {
  let timerId = 0;

  let sendedBoxIdx = 0;
  const boxes = file.boxes;
  const tracks: Array<{ track: TrakBoxParser; id: number }> = [];

  const deltaBuf = (): Uint8Array | null => {
    // boxes.length >= 4 表示完成了 ftyp moov,且有了第一个 moof mdat
    // 避免moov未完成时写入文件,导致文件无法被识别
    if (boxes.length < 4 || sendedBoxIdx >= boxes.length) return null;

    if (tracks.length === 0) {
      for (let i = 1; true; i += 1) {
        const track = file.getTrackById(i);
        if (track == null) break;
        tracks.push({ track, id: i });
      }
    }

    const ds = new mp4box.DataStream();
    ds.endianness = mp4box.DataStream.BIG_ENDIAN;

    for (let i = sendedBoxIdx; i < boxes.length; i++) {
      boxes[i].write(ds);
      delete boxes[i];
    }
    // 释放引用,避免内存泄露
    tracks.forEach(({ track, id }) => {
      file.releaseUsedSamples(id, track.samples.length);
      track.samples = [];
    });
    file.mdats = [];
    file.moofs = [];

    sendedBoxIdx = boxes.length;
    return new Uint8Array(ds.buffer);
  };

  let stoped = false;
  let canceled = false;
  let exit: TCleanFn | null = null;
  const stream = new ReadableStream({
    start(ctrl) {
      timerId = self.setInterval(() => {
        const d = deltaBuf();
        if (d != null && !canceled) ctrl.enqueue(d);
      }, timeSlice);

      exit = () => {
        clearInterval(timerId);
        file.flush();
        const d = deltaBuf();
        if (d != null && !canceled) ctrl.enqueue(d);

        if (!canceled) ctrl.close();
      };

      // 安全起见,检测如果start触发时已经 stoped
      if (stoped) exit();
    },
    cancel() {
      canceled = true;
      clearInterval(timerId);
      onCancel?.();
    },
  });

  return {
    stream,
    stop: () => {
      if (stoped) return;
      stoped = true;
      exit?.();
    },
  };
}

以上步骤,就是在浏览器中创建视频文件的全过程。

在 WebCodecs 之前,前端开发者只能在及其有限的场景使用 ffmpeg.wasm、MediaRecorder 创建视频文件。
现在利用 WebCodecs 则可以快速创建视频文件,并进行非常细致的帧控制,为多样的产品功能提供底层技术支持。

# WebAV 生成视频示例

整个过程的原理不算难,文章的前两张图基本概括了,如果从零开始实现,还是有非常多的细节需要处理,以及更深入地学习一些 mp4 文件相关知识。

你可以略过细节,使用 @webav/av-cliper 提供的工具函数 recodemux 、 file2stream 来快速创建视频文件。

以下是从 canvas 创建视频的示例

import { recodemux, file2stream } from '@webav/av-cliper'

const muxer = recodemux({
  video: {
    width: 1280,
    height: 720,
    expectFPS: 30
  },
  // 后续文章介绍如何处理音频数据
  audio: null
})

let timeoffset = 0
let lastTime = performance.now()
setInterval(() => {
  const duration = (performance.now() - lastTime) * 1000
  muxer.encodeVideo(video
    new VideoFrame(canvas, {
      // 这一帧画面,持续 33ms,duration 单位 μs
      duration,
      timestamp: timeoffset
    })
  )
  timeoffset += duration
}, 33)

const { stream } = file2stream(muxer.mp4file, 500)
// upload or write stream

体验使用不同素材创建视频 DEMO (opens new window); 查看 recodemux 详细用例 (opens new window)

# 附录

💗 博主正处于裸辞待业状态,欢迎 商务合作 💗

相关文章

WebAV SDK(Web 视频编辑)V1 发布

前言 WebAV 是基于 WebCodecs 构建的 SDK,用于在 Web 平台上创建/编辑视频文件。 V1 对项目来说是里程碑版本,意味着 API 已经稳定,且功能的稳定性也经过了长时间的考验,可用于生产环境。 我在 20 年加入 B 站,开始接触 Web 音视频相关的知识(Web 播放器), WebCodecs API 在 21 年发布; 我预期 WebCode ...

纯 Web 视频剪辑

前言 WebCodecs API 为 Web 平台提供了音视频编解码能力,使得在 Web 平台(网页、Electron)上实现高效、专业的视频剪辑成品成为可能。 读者可阅读笔者的入门系列文章获取更详细的信息,或直接使用 WebAV 开源项目在浏览器中创建/编辑音视频文件。 背景 & 方案 为了解决主播投稿场景中,需要对直播视频进行简单编辑的诉求, ...

Google IO 分享 WebCodecs、OPFS 文字版

背景 2024 北京 Google I/O 邀请我参加合作者开发者论坛,主题是 "Build powerful Web App"; 笔者近期在公司项目中实践 WebCodecs,对应的开源项目是 WebAV,在 Web 音视频领域算是相对前沿的探索; 本文主要分享基于 WebCodecs、OPFS 实现的视频剪辑产品,探讨这些 API 还有哪些应用场景; W ...

WebCodecs 性能表现及优化思路

笔者开源 WebAV 已经一年半,还写了系列文章帮助初学者入门 Web 音视频。 之前一直隐隐担心在 Web 平台处理音视频与 Native APP 会有明显性能差距,因为 WebCodecs API 毕竟被浏览器代理了一层,且一些数据处理需要 js 配合,不确定有多大的性能损耗。 相信刚接触 WebCodecs 的读者也非常关心它的性能表现如何。 ...

WebCodecs 编码字符串(codec)介绍

笔者的 Web 音视频系列文章 视频播放经常会看到这样的字符串 video/mp4; codecs="avc1.4d002a",WebCodecs 编解码器初始化也需要配置 codec 参数,本文尝试简单介绍编码字符串含义。 视频编码分许多种类,每个种类还分多个版本,不同种类、版本对应的编解码算法、支持的能力(分辨率上限、色深等等)不同 ...

WebCodecs 开启 Web 音视频新篇章

你可以先略过下面的无聊文字,体验一番 WebCodecs 的实力 WebCodecs 是什么 WebCodecs 是一个 Web 规范,21 年 9 月份在 Chrome 94 中实现 WebCodecs 提供访问编解码能力的接口,可精细控制音视频数据 Web 音视频 API 存在什么问题 音视频技术在 Web 平台上的应用非常广泛,已有许多 Web ...

【译】WebCodecs 说明

本文翻译至 WebCodecs Explainer 问题与动机 已有许多 Web API 在内部使用媒体编解码器来支持特定用途,比如: HTMLMediaElement and Media Source Extensions WebAudio (decodeAudioData) MediaRecorder WebRTC 但是还没有一种通用的方式来灵活 ...

Web 音视频(六)图像素材处理

Web 音视频目录 前序章节介绍了如何在浏览器中解析、创建视频,以及给视频添加一些自定义素材(图片、音频、文字...); 本章介绍如何给图像素材加特效、加动画,实现转场、移动水印、图像滤镜美化等功能。 你可以跳过原理介绍,直接查看 WebAV 示例 素材动画 在视频制作中实现动画跟其他场景略有不同,因为视频 ...

Web 音视频(七)中期回顾

Web 音视频目录 个人回顾 本系列更新至今,持续了一个月时间,内容可以总结为音频、视频数据的 解析 - 处理 - 合成,在浏览器中比较粗粒度地实现音视频编辑的主要环节,差不多是把 WebAV 项目的原理讲完了。 讲解的知识非常浅,目标读者是准备在 Web 平台进行音视频开发的新手; 这是我第一次进行高频率技术写作,将一 ...

Web 音视频(五)在浏览器中合成视频

Web 音视频目录 经过前序章节的介绍,读者能大致了解如何在播放器中解析、创建视频; 本章介绍何在浏览器中合成视频,这是视频编辑中最基础的功能。 你可以跳过原理介绍,直接查看 WebAV 合成视频示例 在视频上叠加素材 常见的素材有:视频、音频、图片、文字 [在浏览器中创建视频](/posts/2 ...