当前位置: 首页 > 科技观察

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

时间:2023-03-13 00:16:37 科技观察

本文转载自微信公众号《微信大学前端技术》,作者翁家瑞。转载本文请联系微一大学前端技术公众号。前言前几天偶然看到一篇关于webassembly的文章,对这门技术还是很感兴趣的。在了解一些相关知识的基础上,看能不能稍微实践一下。什么是网络?WebAssembly(wasm)是一种可移植、体积小、加载速度快且与Web兼容的新格式。用C、C++等语言编写的模块,可以通过编译器创建wasm格式的文件。这个模块以二进制形式发送给浏览器,然后js可以通过wasm调用其中的方法函数。WebAssembly的优点网上应该有很多相关介绍。WebAssembly的优点是性能好,运行速度比Js快很多。对于计算量大、性能要求高的应用场景,如图像/视频解码、图像处理、3D/WebVR/AR等,优势明显。我们可以直接将已有的用C、C++等语言编写的库编译成WebAssembly运行在浏览器上,并且可以作为库被JavaScript引用。也就是说我们可以把很多后端的工作转移到前端,减轻服务器的压力。WebAssembly最简单的做法叫我们写最简单的c文件intadd(inta,intb){returna+b;}然后安装Emscripten编译器Emscripten安装指南emcctest.c-Os-sWASM=1-sSIDE_MODULE=1-otest.wasm然后我们就可以在html中引入并使用了结果=>{constadd=results.instance.exports.addconsole.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]-ccopy[output]start是开始时间end是结束时间input是要操作的视频源文件output是输出文件的位置名这行代码是我们需要获取相关的视频编辑命令FFmpegwasm通过Emscripten将ffmpeg编译成wasm环境问题比较多,所以我们直接使用这个成熟的库https://github.com/ffmpegwasm/ffmpeg.wasm直接使用网上编译好的CDN资源为了方便本地调试,我下载了所有相关资源,一共4个资源文件ffmpeg.min.jsffmpeg-core.jsffmpeg-core.wasmffmpeg-core.worker.js我们在使用的时候只需要导入第一个文件即可,其他文件会使用fetch方法调用时拉取资源的最小函数,实现前置函数实现:我们需要imp在本地添加一个node服务,因为如果server端没有设置responseheader,会出现ffmpeg的模块,会报错SharedArrayBufferisnotdefined。这是因为系统中存在安全漏洞。浏览器默认禁用此api。要启用它,您需要在标头上设置Cross-Origin-Opener-Policy:same-originCross-Origin-Embedder-Policy:require-corp我们启动一个简单的节点服务constKoa=require('koa');constpath=require('path')constfs=require('fs')constrouter=require('koa-router')();conststatic=require('koa-static')conststaticPath='./static'constapp=newKoa();app.use(static(path.join(__dirname,staticPath)))//logrequestURL:app.use(async(ctx,next)=>{console.log(`处理${ctx.request.method}${ctx.request.url}...`);ctx.set('Cross-Origin-Opener-Policy','same-origin')ctx.set('Cross-Origin-Embedder-Policy','require-corp')awaitnext();});router.get('/',async(ctx,next)=>{ctx.response.body='

Index

';});router.get('/:filename',async(ctx,next)=>{console.log(ctx.request.url)constfilePath=path.join(__dirname,ctx.request.url);console.log(filePath)consthtmlContent=fs.readFileSync(filePath);ctx.type="html";ctx.body=htmlContent;});app.use(router.routes());app.listen(3000);console.log('appstartedatport3000...');下面做一个最小的demo来实现剪辑功能,在剪辑视频前一秒新建一个demo.html文件,导入相关资源选择原始视频文件:开始编辑视频
原始视频
处理后的视频
letoriginFile$(document).ready(function(){$('#select_origin_file').on('change',(e)=>{constfile=e.target.files[0]originFile=fileconsturl=window.webkitURL.createObjectURL(文件)$('#origin-video').attr('src',url)})$('#start_clip').on('click',asyncfunction(){const{fetchFile,createFFmpeg}=FFmpeg;ffmpeg=createFFmpeg({log:true,corePath:'./assets/ffmpeg-core.js',});constfile=originFileconst{name}=file;if(!ffmpeg.isLoaded()){awaitffmpeg.load();}ffmpeg.FS('writeFile',name,awaitfetchFile(file));awaitffmpeg.run('-i',name,'-ss','00:00:00','-to','00:00:01','output.mp4');constdata=ffmpeg.FS('readFile','output.mp4');consttempURL=URL.createObjectURL(newBlob([data.buffer],{type:'video/mp4'}));$('#handle-video').attr('src',tempURL)})});代码的意思也很简单,通过导入的FFmpeg创建一个实例,然后使用ffmpeg.load()方法加载对应的wasm和worker资源,没有优化wasm资源比较大,本地文件是23MB。如果这个需要投入生产,就必须通过emcc调整打包参数,去掉无用的模块,然后通过fetchFile方法将选中的输入文件加载到内存中。然后就可以通过ffmpeg.run运行与本地命令行相同的ffmpeg命令行参数,参数基本一致。至此我们的核心功能已经实现。如果你想做一点优化和编辑,最好选择时间段。为了方便,我直接导入元素的cdn,使用滑块截取视频区间。我这里只贴js相关的代码。具体代码大家可以去github仓库仔细看看:classClipVideo{constructor(){this.ffmpeg=nullthis.originFile=nullthis.handleFile=nullthis.vueInstance=nullthis.currentSliderValue=[0,0]this.init()}init(){console.log('init')this.initFfmpeg()this.bindSelectOriginFile()this.bindOriginVideoLoad()this.bindClipBtn()this.initVueSlider()}initVueSlider(maxSliderValue=100){控制台。log(`maxSliderValue${maxSliderValue}`)if(!this.vueInstance){const_this=thisconstMain={data(){return{value:[0,0],maxSliderValue:maxSliderValue}},watch:{value(){_this.currentSliderValue=this.value}},方法:{formatTooltip(val){return_this.transformSecondToVideoFormat(val);}}}constCtor=Vue.extend(Main)this.vueInstance=newCtor().$mount('#app')}否则{这个。vueInstance.maxSliderValue=maxSliderValuethis.vueInstance.value=[0,0]}}transformSecondToVideoFormat(value=0){consttotalSecond=Number(value)lethours=Math.floor(totalSecondd/(60*60))letminutes=Math.floor(totalSecond/60)%60letsecond=totalSecond%60lethoursText=''letminutesText=''letsecondText=''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;这。ffmpeg=createFFmpeg({log:true,corePath:'./assets/ffmpeg-core.js',});}bindSelectOriginFile(){$('#select_origin_file').on('change',(e)=>{constfile=e.target.files[0]this.originFile=fileconsturl=window.webkitURL.createObjectURL(file)$('#origin-video').attr('src',url)})}bindOriginVideoLoad(){$('#origin-video').on('loadedmetadata',(e)=>{constduration=Math.floor(e.target.duration)this.initVueSlider(duration)})}bindClipBtn(){$('#start_clip').on('click',()=>{console.log('startclip')this.clipFile(this.originFile)})}asynclipFile(file){const{ffmpeg,currentSliderValue}=thisconst{fetchFile}=FFmpeg;const{name}=file;conststartTime=this.transformSecondToVideoFormat(currentSliderValue[0])constendTime=this.transformSecondToVideoFormat(currentSliderValue[1])console.log('clipRange',startTime,endTime)if(!ffmpeg.isLoaded()){awaitffmpeg.load();}ffmpeg.FS('writeFile',name,awaitfetchFile(file));awaitffmpeg.run('-i',name,'-ss',startTime,'-to',endTime,'output.mp4');constdata=ffmpeg.FS('readFile','output.mp4');consttempURL=URL.createObjectURL(newBlob([data.buffer],{type:'video/mp4'}));$('#handle-video').attr('src',tempURL)}}$(document).ready(function(){constinstance=newClipVideo()});这样一来,文章开头的效果就是这样实现的。总结webassbembly还是一个比较新的技术。我这里只应用了它的一小部分功能。还有很多值得我们去探索的地方。欢迎与我们交流。参考资料WebAssembly完整介绍——了解wasm的前世今生(https://juejin.cn/post/6844903709806182413)使用FFmpeg和WebAssembly实现纯前端视频抓帧(https://toutiao.io/posts/7as4kva/preview)前端视频帧提取ffmpeg+Webassembly(https://juejin.cn/post/6854573219454844935)