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

server/2025/1/23 11:11:13/

前言

<a class=音视频工作流程" height="598" src="https://i-blog.csdnimg.cn/img_convert/f1da01f5e067276e1605673bd7600d6c.png" width="1610" />​

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

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

本章介绍如何在浏览器中创建视频

采集与编码

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

常见的音视频源有: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)

javascript">const encoder = new VideoEncoder({error: console.error,output: (chunk, meta) => {// chunk: EncodedVideoChunk 等待封装// meta 在下一步封装 SDK 创建轨道时需要},
});
encoder.configure({codec: 'avc1.4D0032', // H264width: 1280,height: 720,
});let timeoffset = 0;
let lastTime = performance.now();
setInterval(() => {const duration = (performance.now() - lastTime) * 1000;encoder.encode(new VideoFrame(canvas, {// 这一帧画面,持续 33ms,duration 单位 μsduration,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。

代码示例

javascript">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.timestampreturn {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 可以非常方便地写入本地文件、上传到服务器。

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

代码不算太长,全部贴出来了

javascript">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触发时已经 stopedif (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 创建视频的示例

javascript">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) * 1000muxer.encodeVideo(videonew VideoFrame(canvas, {// 这一帧画面,持续 33ms,duration 单位 μsduration,timestamp: timeoffset}))timeoffset += duration
}, 33)const { stream } = file2stream(muxer.mp4file, 500)
// upload or write stream

关于优联前端

        武汉优联前端科技有限公司由一批从事前端10余年的专业人才创办,是一家致力于H5前端技术研究的科技创新型公司,为合作伙伴提供专业高效的前端解决方案,合作伙伴遍布中国及东南亚地区,行业涵盖广告,教育, 医疗,餐饮等。有效的解决了合作伙伴的前端技术难题,节约了成本,实现合作共赢。承接开发Web前端,微信小程序、小游戏,2D/3D游戏,动画交互与UI广告设计等各种技术研发。

 


http://www.ppmy.cn/server/160718.html

相关文章

arkime和elasticsearch 安装方法三

Ubuntu新机 sudo apt upgrade sudo apt install open-vm-tools-desktop -y sudo reboot 然后换源 cp /etc/apt/source.list /etc/apt/source.list.bak sudo apt update nano /etc/apt/source.list deb https://mirrors.aliyun.com/ubuntu/ jammy main restricted unive…

工业相机 SDK 二次开发-Halcon 插件

本文介绍了 Halcon 连接相机时插件的使用。通过本套插件可连接海康 的工业相机。 一. 环境配置 1. 拷贝动态库 在 用 户 安 装 MVS 目 录 下 按 照 如 下 路 径 Development\ThirdPartyPlatformAdapter 找到目录为 HalconHDevelop 的文 件夹&#xff0c;根据 Halcon 版本找到对…

喜报!华普微荣获威星智能“优秀供应商奖”

近日&#xff0c;华普微凭借着过硬的产品质量和优秀的服务品质&#xff0c;成功荣获了浙江威星智能仪表股份有限公司&#xff08;以下简称“威星智能”&#xff09;授予的“2024年度优秀供应商”奖。这项荣誉不仅代表着威星智能对华普微过往辛勤付出与卓越贡献的高度认可&#…

【Python运维】Python与Terraform结合:实现云基础设施的自动化部署

《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 随着云计算的普及,自动化部署云基础设施成为提升运维效率和降低人为错误的重要手段。本文深入探讨了如何结合Python与Terraform实现云基础…

Android核心组件——Activity

Activity是一种Android应用组件&#xff0c;它为用户提供一种交互窗口&#xff0c;例如拨打电话&#xff0c;照相&#xff0c;发送电子邮件或者浏览地图等。在Android应用中&#xff0c;交互窗口内显示什么样的信息&#xff0c;支持用户进行什么方式的交互操作&#xff0c;以及…

专业130+总分410+西安交通大学815/869原909信号与系统考研电子信息与通信工程。真题,大纲,参考书。

read-normal-img 考研成功上岸西安交通大学&#xff0c;总分410&#xff0c;专业课815/909-现在的869信号与系统&#xff08;含DSP&#xff09;130&#xff0c;总结一下自己的复习经历&#xff0c;希望给大家有些帮助。 专业课&#xff1a;815/869原909信号与系统和dsp 教材&…

Autosar CP中SWC收发LIN消息的函数调用流程原理解析

Part 1&#xff1a;SWC发送 在AUTOSAR架构中&#xff0c;软件组件&#xff08;SWC&#xff0c;Software Component&#xff09;要发送LIN消息时&#xff0c;通常通过COM模块的接口来发起请求。这是因为COM模块是AUTOSAR架构中负责信号和数据传输的核心模块&#xff0c;它为SWC提…

WPF-系统资源

引用资源方法 单个资源 <Window.Resources><ResourceDictionarySource"Res.xaml"></ResourceDictionary> </Window.Resources> <Grid Width"{StaticResource value}" />多个资源 <Window.Resources><ResourceDi…