找回密码
 会员注册
查看: 19|回复: 0

使用FFmpeg与WebAssembly实现纯前端视频截帧

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
64454
发表于 2024-9-20 21:30:15 | 显示全部楼层 |阅读模式
|导语  随着短视频兴起,音视频技术已经越来越火热,或许你之前有了解过如何在前端处理音视频,但随着视频文件的逐渐增大、用户体验要求的不断提高,纯前端处理音视频的技术也推成出新。下面将结合实际案例,讲解如何使用FFmpeg和WebAssembly实现前端视频截帧。文章较长,也非常硬核,建议先收藏再慢慢看。背景腾讯课堂涨知识创作者后台,目前主要通过邀请合作老师来平台上发布视频。上传视频的同时,需要对视频进行截帧生成推荐封面,生成规则比较简单,根据视频总时长,平均截取8帧。用户可以从其中选择一张作为视频封面。前期调研视频截帧,首先想到的是video+canvas方案,毕竟接触最多的就是它了,不过后面的深入分析,可以发现他们的局限性还是挺多的。下面主要对比了不同截帧方案,每种方案都是可以走通的,也有不同的问题。1.腾讯云视频上传转换能力腾讯云“数据万象”,上传和存储服务都基于对象存储服务(COS),同时官网上提供了媒体截图接口GenerateSnapshot,可以获取某个时刻的截图,输出的截图统一为jpeg格式,同时在我们的内部库也封装基础的JS操作。视频上传和每个时刻的截图处理分成多个异步任务,上传任务返回结果后才能执行下一个截图处理。但是目前这种方案需要服务端配合实现鉴权,比较麻烦,而且只有在上传视频后再进行截图,整个耗时会非常长。2.video+canvas视频截图可以看下网上的demo: demo地址:https://zzarcon.github.io/video-snapshot/主体实现代码如下:async takeSnapshot(time?: VideoTime): romise {    // 首先通过createElement,创建video,    // 在video上设置src后,通过currentTime方法,将视频设置到指定时间戳    const video = await this.loadVideo(time);    const canvas = document.createElement('canvas');    // 获取video标签的尺寸,作为画布的长宽    canvas.width = video.videoWidth;    canvas.height = video.videoHeight;    const context = canvas.getContext('2d');    if (!context) {        throw new Error('error creating canvas context');    }    // 当前时间戳下的video作为图像源,在canvas上绘制图像    context.drawImage(video, 0, 0, canvas.width, canvas.height);    const dataURL = canvas.toDataURL();    return dataURL;}首先利用video标签播放视频,再设置 videoObject.currentTime 指定时刻播放,最后放canvas中进行截图,也可以同上面的demo类似,提供一个操作界面,让用户选择截图时刻。缺点主要在video支持视频封装格式和编码有限,而且只支持下面几种:H.264编码的MP4视频(MPEG-LA公司)VP8编码的webm格式的视频(Google公司)Theora编码的ogg格式的视频(iTouch开发)用户制作上传的其它封装格式和编码组合的视频没法播放,平台上传支持4和flv格式。3.wasm+FFfmpeg实现截取视频截帧主要看到这篇文章wasm+FFmpeg实现前端截取视频帧功能,直接利用FFmpeg提供的lib 库,用c语言写好视频截帧功能,最后通过Emscripten编译器打包成wasm+JS的形式,在浏览器里面跑截图任务。FFmpeg是功能强大的开源软件,能够运行音视频多种格式,几乎包括了现存所有的视音频编码标准。至于wasm的浏览器支持情况,对比看了下大概在90%左右,有不支持的情况以手动上传兜底,最后跟产品讨论可以接受。4.FFmpeg截图任务队列了解到我们服务端已经有一套FFmpeg截图方案,不过是异步任务队列的形式,耗时也在分钟级别,可能在视频上传完成后,也没法得到截图结果,所以没法满足需求。结论从这次需求出发,主要想实现的功能点是上传视频过程中能快速截帧,提供给用户选择,不阻塞流程,同时需要支持MP4,FLV格式,以及WMV3,H.264等常见的编码格式截图。上面的几种方案里面FFmpeg才能满足。另一方面,b站使用这套方案已经在线上运行,具有可行性,所以最后决定用wasm+FFmpeg方案。开发踩坑开发编译 FFmpeg 到后面实现截帧功能,遇到的问题挺多,网上资料相对比较少,这里尽量还原整个实践过程。基础概念解释wasm+FFmpeg的方案里面涉及到很多之前没有接触过的概念,下面一一介绍。FFmpeg:优秀的音视频处理库,可以实现视频截图,没有JS版本。webAssembly:体积小且加载快的全新二进制格式,已经得到了主流浏览器厂商的支持。Emscripten:用来把c/c++代码编译成asm.js和WebAssembly的工具链。编译流程先把c/c++代码编译成LLVM字节码,然后根据不同的目标语言编译成asm.js或者wasm。下面我们从如何安装Emscripten开始讲起,到编译FFmpeg,构建出ffmpeg.wasm,从而可以在浏览器执行。文章整体篇幅比较长,而且整体构建也有比较简单的方式,如果你已经了解到网上有很多现成的构建包,可以直接拿来用,那么你就不用太关注整个编译过程及最后的C语言方案如何实现,直接跳转到部署上线部分。但是,如果想追求极致,根据自己的业务需求,来调整包大小,或者用新版本的FFmpeg来打包,就需要看完C语言部分。安装Emscripten编译之前需要手动安装Emscripten编译器,安装提供了两种方式:1.根据官网指导安装官方文档:https://emscripten.org/docs/getting_started/downloads.html#download-and-installFetch the latest version of the emsdk (not needed the first time you clone)git pull# Download and install the latest SDK tools../emsdk install latest# Make the "latest" SDK "active" for the current user. (writes .emscripten file)./emsdk activate latest# Activate ATH and other environment variables in the current terminalsource ./emsdk_env.sh上面的latest可以替换成指定的版本号进行安装,需要安装Python,make等依赖环境。而且会通过googlesource.com源下载依赖,需要保证访问外网。最后安装成功,运行 emcc-v查看结果。2.安装Emscripten的docker镜像不用安装其它的依赖环境,通过运行容器的方式使用别人已经搭建好的Emscripten环境。Emscripten镜像地址:https://hub.docker.com/r/trzeci/emscripten也可以设置cachewasm缓存,加速第二次运行速度。#!/bin/bash -x# 指定emscripten版本号EM_VERSION=1.39.18-upstream# 运行成功后,开始执行./build.sh里面的脚本编译ffmpeg。docker pull trzeci/emscriptenEM_VERSIONdocker run \  -v $PWD:/src \  -v $PWD/cache-wasm:/emsdk_portable/.data/cache/wasm \  trzeci/emscriptenEM_VERSION \  sh -c 'bash ./build.sh'编译FFmpeg编译过程跟gcc编译类似,后面的编译推荐使用ubuntu系统,其它系统遇到问题比较多。1.配置FFmpeg参数,生成MakeFile等配置文件运行命令emconfigure ./configure ...后面加上配置参数,可以运行 ./configure--help 查看所有可以用的配置。下面列出了配置示例,我们的需求是要支持MP4,FLV视频格式,及常见的H.264,HEVC,WMV3编码。具体每个配置含义:https://cloud.tencent.com/developer/article/13939722.构建依赖emmake make -j4后面 -j设置启用多个内核并行去构建,如果在配置中没有传递参数 --disable-programs,在这一步就会把安装依赖和构建产物走完,所以如果要构建阶段加上一些额外的参数,或者自己写c方案去引入ffmpeglib库自定义构建,可以在配置时加上 --disable-programs3.构建ffmpeg.wasm通过Emscripten构建FFmpeg.wasm,目前主流的方案有两种:(1)整体编译FFmpeg,加上pre.jspost.js包裹胶水代码,跟wasm通信具体方案是把上面第二步编译得到的二进制产物FFmpeg,重命名为ffmpeg.bc,然后经过emcc构建出ffmpeg.wasm+ffmpeg.js胶水代码。在网上搜索一下ffmpeg.js,也可以发现已经有现成的库:ffmpeg.js:https://github.com/Kagami/ffmpeg.js videoconverter.js:https://github.com/bgrins/videoconverter.js不过该方案目前尝试只在Emscripten@1.39.15之前的版本可以实现,在之后的版本产物只有libavcodec.alibswscale.alibavutil.aetc…,生成的 FFmpeg 文件也是可执行的 FFmpeg 文件,无法作为emcc的输入内容。具体解释可以看:https://github.com/emscripten-core/emscripten/issues/11977如果想走通整体编译方案,需要使用 Emscripten@1.39.15之前的版本,对应ffmpeg@3.x老版本进行编译,或者直接找现成编译好的库。知道构建出来的产物是什么,那如何跟它进行通信?可以想到应该是胶水代码ffmpeg.js内部会导出函数或者全局变量,供外部使用,结果放在回调函数中。其实可以利用Emscripten提供的 --pre-js和 --post-js两个可选参数。用户传入自定义的pre.js和post.js,包裹住最后生成的胶水代码ffmpeg.js,在wasm被执行之前,运行pre.js中的代码,方便在pre.js中导出自定义函数(后面提到的ffmpeg_run函数)供外部使用,完成通信。代码示例可以参考videoconverter中的文件:ffmpeg_post.js:https://github.com/bgrins/videoconverter.js/blob/master/build/ffmpeg_post.jsffmpeg_pre.js:https://github.com/bgrins/videoconverter.js/blob/master/build/ffmpeg_pre.js外部调用方式是:js代码通过postmessage传递截帧任务参数和File实例对象,参数经过处理后,执行pre.js中定义的ffmpeg_run函数,截帧任务成功后执行回调返回结果。// 外部js业务代码workers[i].postMessage({  fps,  files,  // 这里的截帧任务参数,跟ffmpeg命令行用法参数一致  arguments: [      '-ss', '1',      '-i', '/input/' + files[i].name,      '-vframes', '1',      '-q:v', '2',      '/output/01.jpg'  ],})ommessage接受到任务,传递给内部函数ffmpeg_run执行任务。// web worker中运行截帧任务,引入ffmpeg.jsonmessage = function (e) {  const { fps, files, arguments } = e.data;  let params = [];   ...// ffmpeg_run在ffmpeg.js里面是全局函数,引入后可以直接用  ffmpeg_run({      outputDir: '/output',      inputDir: '/input',      arguments: arguments,      files,  }, (res) => {    // 返回所有的arrayBuffe二进制数据数组,    // 二进制转换为base64格式,展示在页面中展示      self.postMessage(res);  })}最后总结一下整体的命令:# 配置emconfigure ./configure \    --prefix=./lib/ffmpeg-emcc \  ...# 构建依赖,生成ffmpeg.bc二进制产物emmake make -j4# 构建ffmpeg.wasmemcc   -O2   -s ASSERTIONS=1   -s VERBOSE=1   -s TOTAL_MEMORY=33554432   -lworkerfs.js \  -s ALLOW_MEMORY_GROWTH=1   -s WASM=1   -v ffmpeg.bc \ # 上一步生成产物,重命名后作为emcc的输入内容  -o ./ffmpeg.js --pre-js ./ffmpeg_pre.js --post-js ./ffmpeg_post.js实际上这种方案跟FFmpeg没有特别复杂通信,整体的调用方法都封装到了ffmpeg_run里面了,不用关注FFmpeg内部的实现细节,唯一的缺点是体积太大12M以上,里面的功能不可控,偶现截图失败,浏览器崩溃的问题,也没法快速定位。我们线上主要用后面c方案实现,大小在3.7M(可以根据实际业务需求变化),相比整体编译更加灵活,所以这里主要介绍c方案实现。(2)引入自定义的c文件,暴露出接口函数供JS 调用FFmpeg内部分别有不同的库文件,提供不同功能。可以自己写一份c代码,通过头文件引入的方式,用FFmpeg提供的内部库,实现截帧功能。这种方式非常考验对 FFmpeg 的理解,而且 FFmpeg 里面很多功能库没有提供完备的文档,不过有一篇教程非常详细的讲述每一步怎么做 AnffmpegandSDLTutorial,文章里面用的api在2015年更新过一遍,但是相比现在的FFmpeg 版本,还是有很多api废弃了。AnffmpegandSDLTutorial:http://dranger.com/ffmpeg/tutorial01.html最新的文章可以看:https://zhuanlan.zhihu.com/p/40786748这两篇在原文章的基础上更新了api,其中最后一篇应该算是比较新的版本,用到了ffmpeg@3.4.8+emscripten@1.39.18可以编译成功。在前面第二步编译make基础上,再执行 makeinstall,将 FFmpeg 构建到prefix参数指定的目录下,然后执行emcc,引入c文件和 FFmpeg 的库文件,生成最终产物。所以整体命令总结一下# 配置ffmpeg参数emconfigure ./configure \    --prefix=/data/web-catch-picture/lib/ffmpeg-emcc \    ...# 构建make,安装依赖make  # 或者emmake make -j4,# 安装ffmpeg及相关lib到指定目录make install# 构建目标产物# capture.c是我们自定义的c代码# libavformat.a libavcodec.a libswscale.a... 是前一步编译安装ffmpeg后生成的库文件emcc ${CLIB_PATH}/capture.c ${FFMPEG_PATH}/lib/libavformat.a ${FFMPEG_PATH}/lib/libavcodec.a ${FFMPEG_PATH}/lib/libswscale.a ${FFMPEG_PATH}/lib/libavutil.a \    -O3 \    -I "${FFMPEG_PATH}/include" \    -s WASM=1 \    -s TOTAL_MEMORY=${TOTAL_MEMORY} \    -s EXPORTED_FUNCTIONS='["_main", "_free", "_capture", "_setFile"]' \    -s ASSERTIONS=0 \    -s ALLOW_MEMORY_GROWTH=1 \    -s MAXIMUM_MEMORY=-1 \    -lworkerfs.js    -o /data/web-capture/wasm/capture.js最后编译用到的参数不多,这里简单解释一下:WASM=1:指定我们想要的wasm输出形式。如果我们不指定这个选项,Emscripten默认将只会生成asm.js。TOTAL_MEMORY=33554432:可以通过TOTAL_MEMORY参数控制内存容量,值必须为64KB的整数倍EXPORTED_FUNCTIONSEmscripten:为了减少代码体积,会删除无用的函数,类似treeshaking的DCE,我们自定义的函数暴露给外部使用,需要同通过 EXPORTED_FUNCTIONS:保证不被删除,参数的命名形式为'_funcName'ASSERTIONS=1:用于为内存分配错误启用运行时检查,ASSERTIONS默认是开启的,在存在编译优化参数(-O1+)的时候会被关闭ALLOW_MEMORY_GROWTH=1:设置可变内存,初始化后内存容量固定,在可变内存模式下,空间不足可以实现自动扩容MAXIMUM_MEMORY=-1:设置成-1,意味着没有额外的内存限制,浏览器会尽可能的允许内存增加。从这篇文章看https://v8.dev/blog/4gb-wasm-memory,v8目前允许WebAssembly应用的最大内存也是4GB,这里也可以设置成4G。-I"${FFMPEG_PATH}/include":指定了引用的头文件涉及到的FFmpeg库libavcodec:音视频各种格式的编解码libavformat:用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文和读取音视频帧等功能libavutil:包含一些公共的工具函数的使用库,包括算数运算,字符操作等。libswscale:提供原始视频的比例缩放、色彩映射转换、图像颜色空间或格式转换的功能。libswscale常用的函数数量很少,一般情况下就3个:sws_getContext():初始化一个SwsContext。sws_scale():处理图像数据。sws_freeContext:释放一个SwsContext。常用FFmpeg数据结构AVFormatContext:描述了媒体文件的构成及基本信息,是统领全局的基本结构体,贯穿程序始终,很多函数都要用它作为参数;AVCodecContext:描述编解码器上下文的数据结构,包含了众多编解码器需要的参数信息;AVCodec:编解码器对象,每种编解码格式(例如H.264、AAC等)对应一个该结构体,如libavcodec/aacdec.c的ff_aac_decoder。每个AVCodecContext中含有一个AVCodec;AVPacket:存放编码后、解码前的压缩数据,即ES数据;AVFrame:存放编码前、解码后的原始数据,如YUV格式的视频数据或PCM格式的音频数据等;C代码逻辑梳理截帧功能的实现,重点在解封装和解码,先从下面的代码流程图看下整个过程:对照上面的流程图,进行具体解释:1.main主函数注册所有可用的文件格式和编解码器,在后面打开相应的格式文件时会自动使用。#include #include #include #include int main(int argc, charg *argv[]) {  av_register_all();}2.读取视频文件文件读取主要通过读取文件到内存,然后传递首地址指针到c文件中,完成内存文件传递。具体实现拆解:JS部分实现设置type="file"属性的input标签,触发change事件,获取File对象检查如果file文件之前没有缓存过,则newFileReader(),利用readAsArrayBuffer方法,转换为ArryaBufferconstfilePtr=Module._malloc(fileBuffer.length):建立视图,方便插入和读取内存中的数据Module是emscripten编译出的ffmpeg.js中暴露出来的全局变量,接着通过Module._malloc分配同等大小的内存空间,Module.HEAP8.set(fileBuffer,filePtr),将数据填充进去,最后将内存首地址指针,及长度传给c文件暴露出来的方法。C部分实现到c文件里面全局变量定义数据结构BufferData存放文件位置指针和长度,保存前面JS部分传入的变量typedef struct {  uint8_t *ptr;  // 文件中对应位置指针  size_t size;   // 内存长度} BufferData;分配更视频文件同等大小的内存区域,后面在av_read_frame读取数据包时,会调用avio_alloc_context中的read_packet方法读取流数据,readPacket里面主要根据前面传入的size,拷贝BufferData结构体中的数据,  uint8_t *avioCtxBuffer = (uint8_t *)av_malloc(avioCtxBufferSize);    //avio_alloc_context开头会读取部分数据探测流的信息,不会全部读取,除非设置的缓存过大。  // av_read_frame 会在读帧的时候,调用avio_alloc_context中的read_packet方法读取流数据,  // 每隔avioCtxBufferSize调用一次,直到读完。  avioCtx = avio_alloc_context(avioCtxBuffer, avioCtxBufferSize, 0, NULL, readPacket, NULL, NULL);指定数据获取的方式,表示把媒体数据当作流来读写    // ->pb 指向有效实例,pb是用来读写数据的,它把媒体数据当做流来读写    pFormatCtx->pb = avioCtx;    // AVFMT_FLAG_CUSTOM_IO,表示调用者已指定了pb(数据获取的方式)    pFormatCtx->flags = AVFMT_FLAG_CUSTOM_IO;打开文件,读取文件头,同时存储文件信息到pFormatCtx机构中,后面的三个参数,描述了文件格式,缓冲区大小和格式参数,简单指明NULL和0,告诉libavformat去自动探测文件格式并使用默认的缓冲区大小。avformat_open_input(&pFormatCtx, "", NULL, NULL)3.解封装和解码大部分音视频格式的原始流的数据中,不同类型的流会按时序先后交错在一起,形成多路复用,这样的数据分布,既有利于播放器打开本地文件,读取某一时段的音视频;也有利于网络在线观看视频,从某一刻开始播放视频。视频文件中包含数个音频和视频流,并且他们各自被分开存储不同的数据包里面,我们要做的是使用libavformat依次读取这些包,只提取出我们需要的视频流,并把它们交给libavcodec进行解码处理解码整体流程,再对比看下这张流程图:大体的实现思路基本一致。获取文件主体的流信息,保存到pFormatCtx结构体中,遍历pFormatCtx->streams数组类型的指针,大小为pFormatCtx->nb_streams,找到视频流AVMEDIA_TYPE_VIDEO:if (avformat_find_stream_info(pFormatCtx, NULL) nb_streams; i++) {    // 找出视频流    if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {        videoStream = i;        break;    }}通过pFormatCtx->streams[videoStream]->codec,获取编解码器上下文,后面读取视频流,解码数据包,获取原始的帧数据需要用到。这里可以通过上下文拿到解码器id(pCodecCtx->codec_id枚举类型),通过id获取解码器:pCodec = avcodec_find_decoder(pCodecCtx -> codec_id);打开解码器,开始循环读取视频流:avcodec_open2av_read_frame原始流中读取的每一个packet的流可能是不一样的,需要判断packet的流索引,按类型处理,找到视频流:if (packet.stream_index == videoStream)解码数据包,获取原始的YUV格式帧数据,大多数编码器返回YUV420格式的,然后使用sws_scale将YUV格式帧数据转换成RGB24格式数据:avcodec_send_packetavcodec_receive_framesws_scale4.错误信息捕获FFmpeg错误管理是在C运行时库的基础上扩展,根据函数的返回值int进行判断,成功返回值大于或等于0(>=0),错误的返回值为负数,错误值继承c运行时库的错误值,扩展自己的错误值定义在libavcodec/error.h或者libavutil/error.h(较新版本位置)头文件中。需要使用FFmpeg提供的函数:int av_strerror(int errnum, char *errbuf, size_t errbuf_size);对int类型的返回值翻译成字符串,比如: ret = avcodec_receive_frame(dec, frame); fprintf(stderr, "Error during decoding (%s)\n", av_err2str(ret));5.读取视频文件优化文件传递本来是将原始的视频数据,通过js的readAsArrayBuffer方法文件转换为ArrayBuffer,传递内存地址进去,占用了很大空间,同时在读取数据包时,又会额外开辟空间,截帧过程中,内存占用可以达到文件本身大小的3倍多。测试上传一个1.8G左右的视频文件,运行任务时内存占用达到了5.4G。需要修改文件的传递方式,利用Emscripten提供的FileSystemAPI。默认支持MEMFS模式,所有文件存在内存中,显然不满足我们在需求。WORKERFS模式必须运行在worker中,在worker中提供对File和Blob对象的只读访问,不会将整个数据复制到内存中,可以用于大型文件,加上参数 -lworkerfs.js才能包括进来。而且在FFmpeg配置需要加上--enable-protocol=file,输入的文件也属于协议,不加入file的支持是不能读入文件的。C文件修改:ImageData *capture(int ms, const char* path) {  // 文件路径作为avformat_open_input函数第二个参数,文件流读取交给ffmpeg完成,  // 不用再设置pFormatCtx->pb读取方式。  int ret = avformat_open_input(&pFormatCtx, path, NULL, NULL);  ...JS入口文件修改:const MOUNT_DIR  = '/working';// createFolder只需要在初始化执行一次FS.createFolder('/', MOUNT_DIR.slice(1), true, true);...// 这里直接传入视频文件的File对象实例。不需要做其他读buffer内存操作。FS.mount(WORKERFS, { files: [file] }, MOUNT_DIR)// JavaScript调用C/C++时只能使用Number作为参数, 这里的虚拟路径字符串传递要用Module.cwrap包裹一层var c_capture = Module.cwrap('capture', 'number', ['number', 'string']);c_capture(timeStamp, `${MOUNT_DIR}/${file.name}`);FS.unmount(MOUNT_DIR)修改后运行任务时,无论视频文件的体积多大,内存占用基本稳定在200M-400M。看到这里,整个需求中最困难的阶段已经结束了,编译构建过程可能在实际操作时非常曲折,后面讲到的错误捕获及内存优化方案对于实现截帧的帮助会非常大。接下来会讲一下比较简单的部署及线上情况。读者可以根据一些线上数据,来权衡是否能应用到自己的业务场景中。部署上线本地开发可以跑通,接下来进行部署上线,项目使用webpack打包,假设项目中相关的目录结构如下:src├─ffmpeg │  ├─wasm│  │ ├─ffmpeg.wasm│  │ ├─ffmpeg.min.js     │  ├─ffmpeg.worker.js  // 封装截帧功能,同时引入并初始化ffmpeg.min.js,并引入ffmpeg.wasm│  ├─index.js          // 截图功能入口文件,初始化web worker并引入ffmpeg.worker,    ...需要结合两个loader使用:file-loader:通过webpack默认加载方式,没法在worker中引入wasm文件,而且我们得到的ffmpeg.js经过了压缩,不需要其它loader再次处理,可以直接利用file-loader得到文件路径,加载ffmpeg.wasm,ffmpeg.js文件worker-loader:专门用来处理webworker文件引入和初始化操作的loader,可以直接引入worker文件,不用担心路径问题。最后看下webpack.config:  configChain.module    .rule('worker')    .test(/\.worker\.js/)    .use('worker-loader')    .loader('worker-loader')    configChain.module    .rule('wasm')    .test(/\.wasm$|ffmpeg.js$/)    .type("javascript/auto") /** this disabled webpacks default handling of wasm */    .use('file-loader')    .loader('file-loader')    .options({      name: 'assets/wasm/[name].[hash].[ext]'    })另外,通常我们线上的JS,css等资源都放在cdn上面,如果不进行特殊处理,这里配置打包出来的worker.js文件引入的路径也是cdn域名,但是webworker严格限制了worker初始化时引入的worker.js必须跟当前页面同源,所以需要重写__webpack_public_path__的路径。index.jsimport './rewritePublicPath';import ffmpegWork from './ffmpeg.worker';const worker = new ffmpegWork();rewritePublicPath.js// 这里需要跟页面的url保持一致__webpack_public_path__ = "//ke.qq.com/admin/";worker-loader的相关配置里面也提供了publicPath参数,不过跟我们理解的不一样,worker-loader.options.publicPath只会影响在worker代码里面,再次import其他文件的情况,而我们在初始化worker.js时,webpack默认会使用外部的__webpack_public_path__去替换路径,所以需要重写path。现网效果数据上报到elk,通过Grafana查看整体数据情况,从不同维度收集了线上情况,下面对比过去7天的数据:1.整体支持FFmpeg截图的情况,必须同时支持Webassembly和WebWorker,整体支持情况达到90.87%,对于不支持截帧的情况,我们会引导用户进行手动上传并提供裁剪功能。2.首帧耗时平均在467ms,整体截取8帧耗时在2.47s左右,主要在window上的qq浏览器截帧耗时明显慢很多,偶现最长到了36.56s。3.截帧成功率达到99.86%,设置了首帧任务超时18s,出现超时及失败的情况目前看非常少。总结最开始对音视频相关技术了解几乎为零,所以整个方案从前期调研,到后面落地,上线部署,遇到的问题还是挺多。目前的c方案根据视频总时长,平均截取8帧实际上是串行执行,这块需要优化,在c代码中支持同时截帧多次,返回结果数组。Webassembly是由主流浏览器厂商制定的规范,目前来看支持情况还可以(除了IE),很大程度增强了浏览器的功能,把c/c++等功能库搬到浏览器上面跑,减轻了服务器压力。应用场景非常广泛,除了FFmpeg解析视频,还有很多算法模型训练,文件MD5计算等功能都可以借助Webassembly在浏览器里面去做。紧追技术前沿,深挖专业领域扫码关注我们吧!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-27 14:54 , Processed in 0.386918 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表