Cutter - Web视频剪辑工具原理浅析

2023-11-17

大厂技术  坚持周更  精选好文

最近一直在开发 web视频剪辑工具(cutter),这个工具可以方便老师们编辑拍摄好的视频。

这是一个挺有意思的项目,预计分多章和大家分享介绍。

本期主要介绍下其大体流程,方便大家对其原理有一个简单认知

Cutter 剪辑工具效果演示:

web编辑器

6d975bcca8687d8ab81aecfe50b75a44.gif

导出效果6341b230102e9be6b55e35508d216116.gif

阅读完本文,预计可以收获的知识点:

  • 了解web视频剪辑工具 基本原理

  • 实战一个ffmpeg+wasm+offscreen canvas demo

技术链路

全链路可以简单分为

  • 页面依赖的底层:vesdk

  • 页面核心交互:web剪辑工具

  • 后端:视频架构-视频合成

82bb0a499adadb7d264b989d15b8caeb.png

问题抛出

为了更好的理解全链路,这里我们抛出两个问题,带着问题来看整体链路,增强我们的理解:

Q1:视频是怎么在网页端实现编辑预览的效果?

Q2:怎么保证预览效果和合成效果一致性?

Q1:视频是怎么在网页端实现编辑预览的效果

目前web视频编辑主要有两个方向

  • 一种是使用原生JS,基于浏览器提供的

  • 多媒体前端技术入门指南 - TeqNG[1]

  • 腾讯云剪 - web多媒体技术在视频编辑场景的应用[2]

  • 爱奇艺云剪辑Web端的技术实现[3]

  • 另一种是直接使用WebAssembly将现有基于C/C++等代码的视频编辑框架编译到浏览器上运行

  • 《VESDK技术演进之Web音视频编辑技术》

二者的对比,可以参考如下:

图片来源 《VESDK技术演进之Web音视频编辑技术》

f69b2d5233171f02d3c43595e72d84b7.png

流程图

vesdk采用的是第二种方式(ffmpeg+wasm),大体流程转换图可参考如下:

1061d1ff66daf5a435ae7e11ab41eac7.png

调度逻辑:

解码、绘制时尽力出帧frame,将frame放入缓存池中,上屏时结合raf 自行根据fps计算下一帧渲染时间点

剪辑中 音频、文字 是怎么绘制的:

音频:在主线程中,基于Web Audio的OpenAL API来构建

文字、特效:基于webgl shader等直接绘制

为了更好的理解上面的流程图,介绍下里面的一些关键名词

YUV

YUV是一种颜色编码格式,能够通过公式计算还原为RGB,相比于RGB编码体积占用更小。

R = Y + 1.140*V

G = Y - 0.394U - 0.581V

B = Y + 2.032*U

“Y”表示明亮度(Luminance或Luma),也就是灰阶值

“U”和“V”表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色

例如:通过YUV预览工具,我们可以看到各个分量单独显示时的成像

7b3beb731a2103432ab1d6f5a65ae65e.png

FFmpeg

  • 媒体内容源文件都是比较大的,为了便于传输和存储,需要对原视频通过编码来压缩,再通过容器封装将压缩后的视频、音频、字幕等组合到一个容器内,这就是 编码容器封装 的过程(例如可以用 压缩饼干封袋包装 来理解,会出现很多不同的压缩工艺和包装规格)

  • 在播放端进行播放时,进行相应的 解封装解码,得到原文件数据

在上述过程中,FFmpeg就是这样一款领先的多媒体框架,几乎实现了当下所有常见的数据封装格式、多媒体传输协议、音视频编解码器。

FFmpeg提供了两种调用姿势,可以面向不同场景需求:

  • 调用方法一:应用层可以调用 ffmpeg[4] \ffprobe等命令行 cli 工具 来读写媒体文件;

// 例如:
ffmpeg -i tempalte.mp4 -pix_fmt yuv420p tempalte.yuv

可以看到解封装后,原文件体积是远大于1.1MB的

3958fdea7d0a0a378feda5e6b21db981.png
  • 调用方法二:c层面可以调用 FFmpeg 下层编解码器等外部库用来实现编解码,获取原生图像和音频数据

// 可以参考该文章Mp4读取yuv数据
https://www.jianshu.com/p/f4516e6df9f1
// 几个关键api
av_read_frame    读取视频流的h264帧数据
avcodec_send_packet     将h264帧数据发送给解码器
avcodec_receive_frame    从解码器中读出解码后的yuv数据

// demo
void decode() {
    char *path = "/template.mp4";
    ...
    while(true) {
        av_read_frame(avformat_context, packet);//读取文件中的h264帧
        if (packet->stream_index == videoStream) {
            int ret = avcodec_send_packet(avcodec_context, packet);//将h264帧发送到解码器
            if (ret < 0) {
                break;
            }
            while (true) {
                int ret = avcodec_receive_frame(avcodec_context, frame);//从解码器获取帧
                sws_scale(sws_context,
                            (uint8_t const * const *) frame->data,
                            frame->linesize, 0, avcodec_context->height, pFrameYUV->data,
                            pFrameYUV->linesize);//将帧数据转为yuv420p
                fwrite(pFrameYUV->data[0], sizeof( uint8_t ), avcodec_context->width * avcodec_context->height, pFile);//将y数据写入文件中
                fwrite(pFrameYUV->data[1], sizeof( uint8_t ), avcodec_context->width * avcodec_context->height / 4, pFile);//将u数据写入文件中
                fwrite(pFrameYUV->data[2], sizeof( uint8_t ), avcodec_context->width * avcodec_context->height / 4, pFile);//将v数据写入文件中
            }
        }
    }
    ...
}

WASM

WebAssembly是一种安全、可移植、效率高、文件小的格式,其提供的命令行工具wasm可以将高级语言(如 C++)编写的代码转换为浏览器可理解的机器码,所以实现了在浏览器中直接运行。

例如c代码 经过如下步骤,可以被浏览器直接执行

0e58f46197f70dcf3ca15cf0210fec06.png

加法 demo小例子:

step1、可利用在线工具(https://mbebenita.github.io/WasmExplorer/)编写一个加法例子,然后下载得到 编译好的 test.wasm 文件

f69d1ea081174a189300bf3828b8d951.png

step2、加载test.wasm

d5edba2c47d6f16ca750f0647f353084.png

step3、浏览器中直接运行

1fab763a78c07c835a43351d50c749f8.png

OffscreenCanvas

OffscreenCanvas[5]非常有意思,这是一个离前端开发人员比较近的概念,它是一个可以脱离屏幕渲染的canvas对象,在主线程环境和web worker环境均有效。

OffscreenCanvas 一般搭配worker使用,目前主要用于两种不同的使用场景:

ed95fec4f1d88aa2dc15a77f9498594b.png
image.png

流程 优点 劣势
模式一:同步显示offscrrenCanvas中的帧 step1、在 Worker 线程创建一个 OffscreenCanvas 做后台渲染step2、再把渲染好的缓冲区 Transfer 回主线程显示 主线程可以直接控制渲染内容 canvas渲染受主线程影响
模式二:异步显示offscrrenCanvas中的帧 step1、将主线程中 Canvas 转换为 OffscreenCanvas,并发送给worker线程step2、worker线程获取到OffscreenCanvas后,进行绘制计算操作,最后把绘制结果直接 Commit 到浏览器的 Display Compositor (相当于在 Worker 线程直接更新 Canvas 元素的内容,不走常规的渲染流程)(参考表格下面的图) canvas渲染不受主线程影响-   避免绘制过程中的大量的计算阻塞主线程-   避免主线程的耗时任务阻塞渲染 主线程无法控制绘制内容
a751014413de0785ed1190d0b876a330.png

模式一:

// 主线程 进行渲染  
const ctx = renderCanvas.getContext( '2d' ); 
const worker = new Worker( 'worker.js' ); 
worker.onmessage = function ( msg ) {  
    if (msg.data.method === 'transfer' ) { 
    ctx.drawImage(msg.data.buffer, 0 , 0 ); 
  } 
};   

    // worker线程
onmessage = async (event) => {     
    const offscreenCanvas = new OffscreenCanvas( 480 , 270 );    
    const ctx = offscreenCanvas.getContext( "2d" );    
    // ctx绘制工作     
    ...    
    const imageBitmap = await self.createImageBitmap( new Blob([data.buffer]));    
    ctx.drawImage(imageBitmap, 0 , 0 );
    let imageBitmap = offscreenCanvas.transferToImageBitmap();
    // bitmap发送给主线程     
    postMessage({ method : "transfer" , buffer : imageBitmap}, [imageBitmap])
}

备注:postMessage 常规传递是通过拷贝的方式;对此postMessage提供了第二个参数,可以传入实现了Transferable[6]接口的数据(例如 ImageBitmap),这些数据的控制权会被转移到子线程,转移后主线程无法使用这些数据(会抛错)

备注:postMessage 常规传递是通过拷贝的方式;对此postMessage提供了第二个参数,可以传入实现了Transferable[7]接口的数据(例如 ImageBitmap),这些数据的控制权会被转移到子线程,转移后主线程无法使用这些数据(会抛错)

模式二:

// 主线程
const worker = new Worker('worker.js');
const offscreenCanvas = canvas.transferControlToOffscreen();
worker.postMessage({
  canvas: offscreenCanvas,
}, [offscreenCanvas])


// worker
onmessage = async (event) => {
  const canvas = event.data.canvas;
  const ctx = canvas.getContext("2d");
  // ctx绘制工作   ...
  const imageBitmap = await self.createImageBitmap(new Blob([data.buffer]));
  // 开始渲染
  ctx.drawImage(imageBitmap, 0, 0);
}

一个对照试验:

对照 主线程解码+主线程渲染(参考动图 1)
实验组 demo1:主线程解码 + 主线程渲染-   解码被卡住 ❌-   渲染被卡住 ❌(参考动图 2) demo2:work 线程 解码 + 主线程canvas渲染-   解码不被卡住 ✅-   渲染被卡住 ❌(参考动图 3)
实验组 demo3:worker 线程 解码 + offscreenCanvas(同步模式)-   解码不被卡住 ✅-   渲染被卡住 ❌(参考动图 4) demo4:worker 线程 解码 + offscreenCanvas( 异步模式 )-   解码不被卡住 ✅-   渲染不被卡住 ✅(参考动图 5)

动图 14b9ce8bdb8a77040fc49074c4a16761c.gif

动图 2

f56739feae37c2413fc99e6db13e63b0.gif


动图 3

af3445ad341bfcc498422fd044411b48.gif动图 4

e12e0cda269575bbc4d269fcaa9ca8e9.gif

动图 5

0a5c007564bde5f86cf428cd35502c3d.gif


通过实验,我们可以发现:

解码任务放在worker线程,不会被主线程打断;渲染任务放在offscreenCanvas,不会被主线程打断

fd9b175b91aa648110fee9468f69ab94.png

Q2:怎么保证预览效果和合成效果一致性?

这个问题比较容易理解,受限于浏览器自身的性能和限制,前端合成问题较多,稳定性和性能不足,所以是采用服务端合成的方式。

为了保证服务端导出和前端编辑预览一致,约定一个草稿协议,云端合成时基于草稿做类似前端合成操作即可

0e5572e68ba75fb555800a231ecf360b.png

51f06905e491bc9c1561964f1886e003.png

可以看到 ffmpeg + wasm + worker + offscreenCanvas搭配起来后,还是能做出一款性能不错的有意思的音视频小工具

我们进入一个小实战环节

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Cutter - Web视频剪辑工具原理浅析 的相关文章

随机推荐

  • 变参函数的学习

    定义 可变参数函数又称参数个数可变函数 也可以称为变参函数 int printf const char format 其中printf就为典型的变参函数 其中 参数可分为两部分 数目确定的固定参数和数目可变的可选参数 函数至少需要一个固定参
  • = 1.0.1ubuntu2.13)' is not installed' aria-label='The required dependency 'apt (>= 1.0.1ubuntu2.13)' is not installed'> The required dependency 'apt (>= 1.0.1ubuntu2.13)' is not installed

    使用Ubuntu系统的时候 系统提示升级 从14 04升级到16 04时 提示 The required dependency apt gt 1 0 1ubuntu2 13 is not installed 该提示指的是没有安装所需的依赖
  • Django实现教育平台---个人用户中心管理

    修改用户头像 django文件 上传 下面的html的样式省去 上传文件一定要定义enctype
  • ElasticSearch6.x 之IK 分词

    IK分词器介绍 elasticsearch analysis ik git地址 https github com medcl elasticsearch analysis ik 分词方式 Analyzer ik smart ik max w
  • pycharm 有些库(函数)没有代码提示的解决办法

    问题描述 如图 输入变量im 后没有关于第三方库相应的函数或其他提示 当然 此文档的前提是有相关的函数说明以及已有相关设置等 解决方案 python是动态强类型语言 IDE无法判断Image open Me jpg 的返回值类型 无法根据参
  • Git密钥配置

    一 下载并安装Git 官网下载地址点击这里 二 打开git bash 选择一个空文件夹 右键选择 Git Bash Here 三 配置密钥 在Git Bash界面输入git命令 初始化自己的用户名和邮箱 git config global
  • C# string类型(引用类型)

    C string类型 引用类型 2016年03月31日 10 34 45 阅读数 966 sing类型 引用类型 名称 CTS类型 说明 string System String Unicode字符串 string str1 hello s
  • Python教程:类的继承——深入理解继承的概念和用法

    Python教程 类的继承 深入理解继承的概念和用法 类的继承是面向对象编程中的重要概念 它允许我们定义一个新的类 并从现有的类中继承属性和方法 这种继承关系可以让我们在代码中实现代码重用 提高代码的可维护性和可扩展性 在本文中 我们将深入
  • k8s非高可用环境搭建

    k8s非高可用环境搭建 文章目录 k8s非高可用环境搭建 环境准备 集群信息 1 节点规划 2 修改hostname 3 添加hosts解析 4 调整系统配置 5 安装docker 部署kubernetes 1 安装kubernetes k
  • Python 一篇入门

    目录 Python 的简介与特点 Python支持多种编程风格 解释运行 跨平台 可扩展强 可嵌入 丰富的库 Python版本选择 Python开发环境搭建 认识Python解释器 快速入门 变量和赋值 动态类型 变量命名规则 认识 数字
  • Android DataStore 使用详解

    转载请标明出处 http blog csdn net zhaoyanjun6 article details 127358235 本文出自 赵彦军的博客 文章目录 概述 使用 DataStore 本地数据 查看DataStore 文件 Ke
  • Eclipse中JUnit的安装及初始使用

    JUnit的下载 安装 1 下载 http www junit org JUnit软件包 版本很多 可以自行选择 2 在eclipse中添加junit jar包 打开eclipse gt 菜单栏点击project gt properties
  • ubuntu pip intall出现“设备上没有空间”的解决办法

    原因 空间问题呗 东西太多了 tmp盘不够大 pip install的时候文件包会预先下载到tmp盘 步骤1 在home目录下新建一个tmp文件夹 用来取代系统根目录的tmp文件夹 步骤2 设置环境变量TMPDIR export TMPDI
  • [LeetCode-02]-Add Two Numbers-性能极好

    文章目录 题目相关 Solution 1 错误的解法 2 正确解法 3 几个用例 后记 每周完成一个ARTS Algorithm Review Tip Share ARTS Algorithm 每周至少做一个 leetcode 的算法题 R
  • 强化学习打卡班第四五章

    强化学习打卡班第四五章 第四章 Policy Gradient梯度策略 例子 贝叶斯公式补充 奖励函数 reward function 最大化方法 梯度上升 PPO算法 从 On policy 到 Off policy 第四章 Policy
  • Linux 使用wget 命令下载JDK的方法

    Oracle官网上下载jdk 需要点击accept licence的才能下载 使用下面的命令 直接可以下载 wget no check certificate no cookies header Cookie oraclelicense a
  • DeBlurGANv2图像去模糊 训练自己的数据集

    之前在有位博主的DeblurGANv2教程的页面下留了言 很多小伙伴来私信我 config yaml怎么调参数 predict py和train py需要怎么修改 之前只跑了predict 有些问题也没办法解答 最近自己跑了一下train
  • MTU 和 MSS 区别

    MTU Maximum Transmit Unit 最大传输单元 即物理接口 数据链路层 提供给其上层 通常是IP层 最大一次传输数据的大小 以普遍使用的以太网接口为例 缺省MTU 1500 Byte 这是以太网接口对IP层的约束 如果IP
  • HPE Microserver GEN10升级BIOS

    到手的机子BIOS版本还是ZA10A290 非常有必要升级 便从HPE官网下载了最新的版本 ZA10A360 选择UEFI Shell方式更新 官网下载地址 https support hpe com hpesc public km pro
  • Cutter - Web视频剪辑工具原理浅析

    大厂技术 坚持周更 精选好文 最近一直在开发 web视频剪辑工具 cutter 这个工具可以方便老师们编辑拍摄好的视频 这是一个挺有意思的项目 预计分多章和大家分享介绍 本期主要介绍下其大体流程 方便大家对其原理有一个简单认知 Cutter
Powered by Hwhale