百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 博客教程 > 正文

120 行代码实现纯 Web 剪辑视频

connygpt 2024-08-21 14:44 8 浏览


作者: 翁佳瑞 来源:微医大前端技术

前言

前几天偶尔看到一篇 webassembly 的相关文章,对这个技术还是挺感兴趣的,在了解一些相关知识的基础上,看下自己能否小小的实践下。

什么是 webasembly?

WebAssembly(wasm)就是一个可移植、体积小、加载快并且兼容 Web 的全新格式。可以将 C,C++等语言编写的模块通过编译器来创建 wasm 格式的文件,此模块通过二进制的方式发给浏览器,然后 js 可以通过 wasm 调用其中的方法功能。

WebAssembly 的优势

网上对于这个相关的介绍应该有很多了,WebAssembly 优势性能好,运行速度远高于 Js,对于需要高计算量、对性能要求高的应用场景如图像/视频解码、图像处理、3D/WebVR/AR 等,优势非常明显,们可以将现有的用 C、C++等语言编写的库直接编译成 WebAssembly 运行到浏览器上,并且可以作为库被 JavaScript 引用。那就意味着我们可以将很多后端的工作转移到前端,减轻服务器的压力。.........

WebAssembly 最简单的实践调用

我们编写一个最简单的 c 文件

int add(int a,int b) { 
  return a + b; 
}

然后安装对于的 Emscripten 编译器Emscripten 安装指南

emcc test.c -Os -s WASM=1 -s SIDE_MODULE=1 -o test.wasm

然后我们在 html 中引入使用即可

fetch('./test.wasm').then(response => 
  response.arrayBuffer() 
).then(bytes => 
  WebAssembly.instantiate(bytes) 
).then(results => { 
  const add = results.instance.exports.add 
  console.log(add(11,33)) 
});

这时我们即可在控制台看到对应的打印日志,成功调用我们编译的代码啦

正式开动

既然我们已经知道如何能快速的调用到一些已经成熟的 C,C++的类库,那我们离在线剪辑视频预期目标更进一步了。

最终 demo 演示

由于录制操作的电脑 cpu 不太行,所以可能耗时比较久,但整体的效果还是能看到滴

demo 仓库地址(https://github.com/Dseekers/clip-video-by-webassembly)

FFmpeg

在这个之前你得稍微的了解下啥是 FFmpeg? 以下根据维基百科的目录解释

FFmpeg 是一个开放源代码的自由软件,可以运行音频和视频多种格式的录影、转换、流功能[1],包含了 libavcodec——这是一个用于多个项目中音频和视频的解码器库,以及 libavformat——一个音频与视频格式转换库。

简单的说这个就是由 C 语言编写的视频处理软件,它的用法也是相当滴简单

我主要将这次需要用到的命令给调了出来,如果你还可能用到别的命令,可以根据他的官方文档查看 ,还可以了解下阮一峰大佬的文章 (https://www.ruanyifeng.com/blog/2020/01/ffmpeg.html)

ffmpeg -ss [start] -i [input] -to [end] -c copy [output]

start 为开始时间 end 为结束时间 input 为需要操作的视频源文件 output 为输出文件的位置名称

这一行代码就是我们需要用到的剪辑视频的命令了

获取相关的FFmpeg的wasm

由于通过 Emscripten 编译 ffmpeg 成 wasm 存在较多的环境问题,所以我们这次直接使用在线已经编译好的 CDN 资源

这边就直接使用了这个比较成熟的库 https://github.com/ffmpegwasm/ffmpeg.wasm

为了本地调试方便,我把其相关的资源都下了下来 一共 4 个资源文件

ffmpeg.min.js 
ffmpeg-core.js 
ffmpeg-core.wasm 
ffmpeg-core.worker.js

我们使用的时候只需引入第一个文件即可,其它文件会在调用时通过 fetch 方式去拉取资源

最小的功能实现

前置功能实现: 在我们本地需要实现一个 node 服务,因为使用 ffmpeg 这个模块会出现如果没在服务器端设置响应头, 会报错 SharedArrayBuffer is not defined,这个是因为系统的安全漏洞,浏览器默认禁用了该 api,若要启用则需要在 header 头上设置

Cross-Origin-Opener-Policy: same-origin 
Cross-Origin-Embedder-Policy: require-corp

我们启动一个简易的 node 服务

const Koa = require('koa'); 
const path = require('path') 
const fs = require('fs') 
const router = require('koa-router')(); 
const static = require('koa-static') 
const staticPath = './static' 
const app = new Koa(); 
app.use(static( 
    path.join(__dirname, staticPath) 
)) 
// log request URL: 
app.use(async (ctx, next) => { 
    console.log(`Process ${ctx.request.method} ${ctx.request.url}...`); 
    ctx.set('Cross-Origin-Opener-Policy', 'same-origin') 
    ctx.set('Cross-Origin-Embedder-Policy', 'require-corp') 
    await next(); 
}); 
 
router.get('/', async (ctx, next) => { 
    ctx.response.body = '<h1>Index</h1>'; 
}); 
router.get('/:filename', async (ctx, next) => { 
    console.log(ctx.request.url) 
    const filePath = path.join(__dirname, ctx.request.url); 
    console.log(filePath) 
    const htmlContent = fs.readFileSync(filePath); 
    ctx.type = "html"; 
    ctx.body = htmlContent; 
}); 
app.use(router.routes()); 
app.listen(3000); 
console.log('app started at port 3000...');

我们做一个最小化的 demo 来实现下这个剪辑功能,剪辑视频的前一秒钟 新建一个 demo.html 文件,引入相关资源

<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script> 
<script src="./assets/ffmpeg.min.js"></script> 
 
<div class="container"> 
  <div class="operate"> 
    选择原始视频文件: 
    <input type="file" id="select_origin_file"> 
    <button id="start_clip">开始剪辑视频</button> 
  </div> 
  <div class="video-container"> 
    <div class="label">原视频</div> 
    <video class="my-video" id="origin-video" controls></video> 
  </div> 
  <div class="video-container"> 
    <div class="label">处理后的视频</div> 
    <video class="my-video" id="handle-video" controls></video> 
  </div> 
</div>
let originFile 
$(document).ready(function () { 
  $('#select_origin_file').on('change', (e) => { 
    const file = e.target.files[0] 
    originFile = file 
    const url = window.webkitURL.createObjectURL(file) 
    $('#origin-video').attr('src', url) 
  }) 
  $('#start_clip').on('click', async function () { 
    const { fetchFile, createFFmpeg } = FFmpeg; 
    ffmpeg = createFFmpeg({ 
      log: true, 
      corePath: './assets/ffmpeg-core.js', 
    }); 
    const file = originFile 
    const { name } = file; 
    if (!ffmpeg.isLoaded()) { 
      await ffmpeg.load(); 
    } 
    ffmpeg.FS('writeFile', name, await fetchFile(file)); 
    await ffmpeg.run('-i', name, '-ss', '00:00:00', '-to', '00:00:01', 'output.mp4'); 
    const data = ffmpeg.FS('readFile', 'output.mp4'); 
    const tempURL = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' })); 
    $('#handle-video').attr('src', tempURL) 
  }) 
});

其代码的含义也是相当简单,通过引入的 FFmpeg 去创建一个实例,然后通过 ffmpeg.load()方法去加载对应的 wasm 和 worker 资源 没有进行优化的 wasm 的资源是相当滴大,本地文件竟有 23MB,这个若是需要投入生产的可是必须通过 emcc 调节打包参数的方式去掉无用模块。然后通 fetchFile 方法将选中的 input file 加载到内存中去,接下来就可以通过 ffmpeg.run 运行和 本地命令行一样的 ffmpeg 命令行参数了参数基本一致

这时我们的核心功能已经实现完毕了。

做一点小小的优化

剪辑的话最好是可以选择时间段,我这为了方便直接把 element 的以 cdn 方式引入使用 通过 slider 来截取视频区间,我这边就只贴 js 相关的代码了,具体代码可以去 github 仓库里面仔细看下

class ClipVideo {
constructor() {
this.ffmpeg = null
this.originFile = null
this.handleFile = null
this.vueInstance = null
this.currentSliderValue = [0, 0]
this.init()
}
init() {
console.log('init')
this.initFfmpeg()
this.bindSelectOriginFile()
this.bindOriginVideoLoad()
this.bindClipBtn()
this.initVueSlider()
}
initVueSlider(maxSliderValue = 100) {
console.log(`maxSliderValue ${maxSliderValue}`)
if (!this.vueInstance) {
const _this = this
const Main = {
data() {
return {
value: [0, 0],
maxSliderValue: maxSliderValue
}
},
watch: {
value() {
_this.currentSliderValue = this.value
}
},
methods: {
formatTooltip(val) {
return _this.transformSecondToVideoFormat(val);
}
}
}
const Ctor = Vue.extend(Main)
this.vueInstance = new Ctor().$mount('#app')
} else {
this.vueInstance.maxSliderValue = maxSliderValue
this.vueInstance.value = [0, 0]
}
}
transformSecondToVideoFormat(value = 0) {
const totalSecond = Number(value)
let hours = Math.floor(totalSecond / (60 * 60))
let minutes = Math.floor(totalSecond / 60) % 60
let second = totalSecond % 60
let hoursText = ''
let minutesText = ''
let secondText = ''
if (hours < 10) {
hoursText = `0${hours}`
} else {
hoursText = `${hours}`
}
if (minutes < 10) {
minutesText = `0${minutes}`
} else {
minutesText = `${minutes}`
}
if (second < 10) {
secondText = `0${second}`
} else {
secondText = `${second}`
}
return `${hoursText}:${minutesText}:${secondText}`
}
initFfmpeg() {
const { createFFmpeg } = FFmpeg;
this.ffmpeg = createFFmpeg({
log: true,
corePath: './assets/ffmpeg-core.js',
});
}
bindSelectOriginFile() {
$('#select_origin_file').on('change', (e) => {
const file = e.target.files[0]
this.originFile = file
const url = window.webkitURL.createObjectURL(file)
$('#origin-video').attr('src', url)

})
}
bindOriginVideoLoad() {
$('#origin-video').on('loadedmetadata', (e) => {
const duration = Math.floor(e.target.duration)
this.initVueSlider(duration)
})
}
bindClipBtn() {
$('#start_clip').on('click', () => {
console.log('start clip')
this.clipFile(this.originFile)
})
}
async clipFile(file) {
const { ffmpeg, currentSliderValue } = this
const { fetchFile } = FFmpeg;
const { name } = file;
const startTime = this.transformSecondToVideoFormat(currentSliderValue[0])
const endTime = this.transformSecondToVideoFormat(currentSliderValue[1])
console.log('clipRange', startTime, endTime)
if (!ffmpeg.isLoaded()) {
await ffmpeg.load();
}
ffmpeg.FS('writeFile', name, await fetchFile(file));
await ffmpeg.run('-i', name, '-ss', startTime, '-to', endTime, 'output.mp4');
const data = ffmpeg.FS('readFile', 'output.mp4');
const tempURL = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
$('#handle-video').attr('src', tempURL)
}
}
$(document).ready(function () {
const instance = new ClipVideo()
});

这样文章开头的效果就这样实现啦

小结

webassbembly 还是比较新的一项技术,我这边只是应用了其中一小部分功能,值得我们探索的地方还有很多,欢迎大家多多交流哈

参考资料

WebAssembly 完全入门——了解 wasm 的前世今生

(https://juejin.cn/post/6844903709806182413)

使用 FFmpeg 与 WebAssembly 实现纯前端视频截帧 (https://toutiao.io/posts/7as4kva/preview)

前端视频帧提取 ffmpeg + Webassembly (https://juejin.cn/post/6854573219454844935)

相关推荐

自学Python,写一个挨打的游戏代码来初识While循环

自学Python的第11天。旋转~跳跃~,我~闭着眼!学完循环,沐浴着while的光芒,闲来无事和同事一起扯皮,我说:“编程语言好神奇,一个小小的循环,竟然在生活中也可以找到原理和例子”,同事也...

常用的 Python 工具与资源,你知道几个?

最近几年你会发现,越来越多的人开始学习Python,工欲善其事必先利其器,今天纬软小编就跟大家分享一些常用的Python工具与资源,记得收藏哦!不然下次就找不到我了。1、PycharmPychar...

一张思维导图概括Python的基本语法, 一周的学习成果都在里面了

一周总结不知不觉已经自学Python一周的时间了,这一周,从认识Python到安装Python,再到基本语法和基本数据类型,对于小白的我来说无比艰辛的,充满坎坷。最主要的是每天学习时间有限。只...

三日速成python?打工人,小心钱包,别当韭菜

随着人工智能的热度越来越高,许多非计算机专业的同学们也都纷纷投入到学习编程的道路上来。而Python,作为一种相对比较容易上手的语言,也越来越受欢迎。网络上各类网课层出不穷,各式广告令人眼花缭乱。某些...

Python自动化软件测试怎么学?路线和方法都在这里了

Python自动化测试是指使用Python编程语言和相关工具,对软件系统进行自动化测试的过程。学习Python自动化测试需要掌握以下技术:Python编程语言:学习Python自动化测试需要先掌握Py...

Python从放弃到入门:公众号历史文章爬取为例谈快速学习技能

这篇文章不谈江流所专研的营销与运营,而聊一聊技能学习之路,聊一聊Python这门最简单的编程语言该如何学习,我完成的第一个Python项目,将任意公众号的所有历史文章导出成PDF电子书。或许我这个Py...

【黑客必会】python学习计划

阅读Python文档从Python官方网站上下载并阅读Python最新版本的文档(中文版),这是学习Python的最好方式。对于每个新概念和想法,请尝试运行一些代码片段,并检查生成的输出。这将帮助您更...

公布了!2025CDA考试安排

CDA数据分析师报考流程数据分析师是指在不同行业中专门从事行业数据搜集、整理、分析依据数据作出行业研究评估的专业人员CDA证书分为1-3级,中英文双证就业面广,含金量高!!?报考条件:满18...

一文搞懂全排列、组合、子集问题(经典回溯递归)

原创公众号:【bigsai】头条号:程序员bigsai前言Hello,大家好,我是bigsai,longtimenosee!在刷题和面试过程中,我们经常遇到一些排列组合类的问题,而全排列、组合...

「西法带你学算法」一次搞定前缀和

我花了几天时间,从力扣中精选了五道相同思想的题目,来帮助大家解套,如果觉得文章对你有用,记得点赞分享,让我看到你的认可,有动力继续做下去。467.环绕字符串中唯一的子字符串[1](中等)795.区...

平均数的5种方法,你用过几种方法?

平均数,看似很简单的东西,其实里面包含着很多学问。今天,分享5种经常会用到的平均数方法。1.算术平均法用到最多的莫过于算术平均法,考试平均分、平均工资等等,都是用到这个。=AVERAGE(B2:B11...

【干货收藏】如何最简单、通俗地理解决策树分类算法?

决策树(Decisiontree)是基于已知各种情况(特征取值)的基础上,通过构建树型决策结构来进行分析的一种方式,是常用的有监督的分类算法。决策树算法是机器学习中的一种经典算法,它通过一系列的规则...

面试必备:回溯算法详解

我们刷leetcode的时候,经常会遇到回溯算法类型题目。回溯算法是五大基本算法之一,一般大厂也喜欢问。今天跟大家一起来学习回溯算法的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~什么是回溯...

「机器学习」决策树——ID3、C4.5、CART(非常详细)

决策树是一个非常常见并且优秀的机器学习算法,它易于理解、可解释性强,其可作为分类算法,也可用于回归模型。本文将分三篇介绍决策树,第一篇介绍基本树(包括ID3、C4.5、CART),第二篇介绍Ran...

大话AI算法: 决策树

所谓的决策树算法,通俗的说就是建立一个树形的结构,通过这个结构去一层一层的筛选判断问题是否好坏的算法。比如判断一个西瓜是否好瓜,有20条西瓜的样本提供给你,让你根据这20条(通过机器学习)建立起...