在上一篇文章中,我们探讨了动效平台中的「多种序列帧格式自动转换」功能;本期,我们将介绍动效多格式转换能力背后的技术实现。涵盖了大前端复杂Node服务系统设计,涉及FFmpeg、透明视频、APNG、Webp、Avif等多动效格式的转换和压缩、优化策略。
在现代C端页面中,动效的应用越来越广泛,不同的机型和业务场景对动效的表现力和性能有着不同的要求。设计师通常关注动效的视觉效果,而不关心具体的动效形式,这就需要研发团队来判断或通过测试验证,找到既符合性能要求又能保证动效质量的最佳解决方案。为了简化这一复杂流程,我们希望设计师只需导出一种最通用且低成本的格式,如序列帧。通过高性能的动效转换服务,支持对各种目标格式进行处理和转换,并应用各种压缩和优化策略,确保符合研发的性能标准,同时保证动效质量。动效转换服务支持批量异步转换,具有高展性,能够高效处理大量动效文件,并提供参数修改以满足业务自定义需求。
本文将深入的介绍动效转换服务的技术架构与相关格式产物的优化策略,揭示动效转换服务背后的故事。
我们对当前Web端常用的动效类型及格式进行了系统梳理,包括序列帧图片、Lottie、Apng、透明视频、普通视频、Webp动图、Avif动画、SVG、SPINE、FBX模型文件等,并制定了以下目标:
1. 序列帧格式转换:序列帧图片是大多数设计软件支持导出的格式,具有较低的导出成本。因此,我们将序列帧作为通用格式,基于此实现向其他动图格式的转换,如Apng、Webp、Avif、透明视频和普通视频等。2. 服务调度与容器化能力:我们的服务目标之一是实现多种动效格式的互相转换,并具备调度能力和容器化特性,以支持动态服务伸缩和故障恢复,确保服务的高扩展性和高效性。3. 批量转换支持:确保动效平台和CLI等大规模服务的稳定性和效率。4. 一站式压缩能力:提供适合各种平台格式的压缩质量参数和等级,业务可以根据需求灵活选择,以平衡效果与性能。在大前端的背景下,前端工程师对动效格式的接触和熟悉程度更高。然而,提供批量化工具或服务需要更稳定的系统支持。在这种情况下,Node.js成为前端开发中更优的选择。
该项目的挑战还在于涉及大量图像领域的编解码及底层技术细节,需要去编译适合NodeJS运行的二进制文件比如FFmpeg、libavif等优秀的开源C++库。这不仅要求具备足够的音视频知识储备,还需具备服务架构思维。对此,我们对大前端工程师提出了更高的要求:- 服务架构思维,设计稳定、高效的系统服务,处理批量图像和视频任务。
- 流量控制与调度,确保服务在高并发和大流量下的稳定性和性能。
- 音视频处理知识储备,了解音视频的基本原理和处理方法。
- 编解码技术理解,深入掌握图片、视频相关的编解码技术,熟练使用和编译FFmpeg、libavif等工具。
- 扩展工程思维,以解决问题为导向,灵活运用各种工具和技术,创造业务价值。
通过应对这些技术挑战,我们成功设计并实现了以下服务系统。
动效转换服务架构总体分为六层,从上到下依次是应用层、接入层、领域层、消息队列层、数据层和基础服务层。以下是每一层的详细介绍:
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
应用层
应用层提供接口服务给平台和CLI工具,主要用于平台上的动效资源进行自由格式的选择。用户可以根据我们提供的预设压缩参数导出各种格式的资源,也可以自定义更精细的压缩参数。CLI工具会自动将序列帧资源按我们的压缩预设值批量请求转换各种类型格式。接入层主要提供RPC和HTTP的服务协议,确保不同的客户端能够方便的接入我们的动效转换服务。领域层从上到下分为流控服务、调度服务、动效转换微服务集群、日志服务和监控服务。1. 流控服务:提供外部API接口,进行请求的权限校验,使用Redis缓存进行QPS节流控制,请求任务拆分批次和任务数量等操作。2. 调度服务:接收流控服务转发的请求,进行批次和任务的创建,通过任务调度逻辑将任务转发到对应的Kafka队列中。调度服务还负责部分业务逻辑的预处理和逻辑调用的编排,以及任务状态流转和任务重试逻辑。3. 动效转换微服务:处理具体的转换任务。该服务主要分为业务处理层、基础服务层和多媒体处理层。- 业务处理层:进行各种格式的消息转发、压缩命令封装、编解码命令组装、异常处理等。
- 基础服务层:提供统一的FFmpeg调用封装、子进程管理、文件处理、Redis连接池、异步存储上下文等。
- 多媒体处理层:将编译好的FFmpeg、Libavif、FFprobe、Fbx2gltf等二进制执行文件通过子进程启动,接受执行命令进行执行。
4. 日志服务和监控服务:对全链路进行trace串联和监控,确保服务的可观测性和稳定性。在消息队列层,我们通过Kafka进行队列的调度和存储,确保任务的高效分发和处理。数据层包括MySQL、Redis和分布式文件存储。用来存储任务的各种状态信息、流量控制以及最终生产的转换结果。使用快手内部基础服务,包括K8s容器和KNode监控等,提供服务的运行环境和容器化支持及基础监控。- 提供服务的方式灵活:支持HTTP、RPC等多种服务协议,既可以满足平台调用的需求,也可以作为微服务的原子能力,支持下游服务的调用。
- 高度模块化:各个层级解耦设计,将流量入口层和数据库操作、调度、转换处理等功能拆分,避免单点故障导致整体系统奔溃,提高系统的稳定性和可靠性。
- 最大化利用机器性能:转换任务属于CPU密集型任务,将其拆分为独立的微服务集群可以最大化利用机器性能来提高CPU的利用率。而调度和流控等其它任务则属于非CPU密集型,通过拆分出来可以有效节省机器资源。
- 可扩展性强:如果需要支持或调用音视频等相关能力,可以在调度层轻松的添加实现,满足不断变化的业务需求。
- 排查问题链路清晰:各个服务集群的功能拆分明确,便于快速定位和解决问题,提高系统的可维护性和可观测性。
动效转换的主流程分为以下6步:2. 请求处理:业务方通过RPC接口请求服务,API服务进行权限校验和流控控制,并拆分批次和任务。3. 任务调度:API服务将请求转发至调度服务,调度服务创建批次和任务表,并通过任务执行器进行业务逻辑编排和队列调度,发送Kafka生产消息。4. 任务执行:转换服务接收Kafka消费信息,执行转换任务,完成后将结果上传至OSS,并通过Redis发布订阅回传结果。如果是异步交互任务,则会发送Kafka生产消息通知下游。5. 状态流转:调度服务接收Redis订阅消息,处理任务状态流转和错误重试。6. 结果查询:业务方通过同步轮询或异步Kafka消费方式查询和获取任务结果。通过以上步骤,动效转换服务实现了从权限申请到结果获取的全流程管理,确保了任务的高效执行和结果的及时反馈。
在动效转换服务中,我们使用了多种编码解码器来处理不同格式的动效文件,下面将进行编码解码器相关的介绍和使用。FFmpeg是一个开源的多媒体框架,可以用来录制、转换和流式传输音视频。FFprobe是FFmpeg的一部分,用于分析多媒体流并输出相关信息。我们通过fluent-ffmpeg作为垫片,在Node.js中可以方便地使用FFmpeg和FFprobe:import Ffmpeg, { FfprobeStream, FfmpegCommand, Formats } from 'fluent-ffmpeg';
@Injectable()
export class FfmpegService implements OnApplicationBootstrap {
onApplicationBootstrap() {
// ffmpegPath和ffprobePath指定ffmpeg和ffprobe的二进制文件路径
Ffmpeg.setFfmpegPath(ffmpegPath);
Ffmpeg.setFfprobePath(ffprobePath);
}
public buildCommand(options?: Ffmpeg.FfmpegCommandOptions) {
return Ffmpeg(options);
}
public toPromise(input: FfmpegCommand) {
return new Promise((res, rej) => {
input
.on('start', (commandLine) => {
// ffmpeg输出命令
logger.log('render: ', commandLine);
})
.on('end', (p) => {
res(p);
})
.on('error', (err) => {
rej(err);
})
.run();
});
}
}
在FFmpeg中,内置了大量的编解码器,可以通过以下命令查看当前编译使用的FFmpeg支持哪些可用的编解码器:// 查看编码器
ffmpeg -encoders
// 查看解码器
ffmpeg -decoders
- libx264:libx264是一个用于H.264编码的开源库,是一种广泛使用的视频压缩标准,兼容性较好,适用于各种视频应用,但压缩效率较低,需要更大的文件大小来达到相同的质量。
- libx265:libx265是一个用于H.265编码的开源库,它提供了更高的压缩率,适合高质量视频的传输,但部分低端机型可能不支持该格式的解码。
- lavfi:lavfi是FFmpeg中的一个滤镜库,用于处理视频和音频滤镜,可以用于生成黑色背景,抽取alpha通道等操作。
- libwebp_anim:libwebp_anim是用于Webp动画的编码和解码库,支持高效的图像压缩。
uPNG.js是一个用于处理PNG图像的js库,它提供了对PNG图像的编码和解码功能,支持无损压缩和透明度处理。1. 读取PNG文件:从指定文件夹中读取所有PNG文件,并按顺序排序。2. 解码PNG图像:使用uPNG.js解码器,获取图像数据。在解码的过程中,会逐个解析PNG文件中的数据块(如IHDR、IDAT、IEND等),提取图像信息和数据。// 解码 PNG 图像
const image = PNG.decode(buffer);
// 获取图像数据
const rgbaData = PNG.toRGBA8(image)[0];
3. 编码PNG图像:通过传入FPS以及图像数据,编码器会根据图像数据生成相应的PNG数据块,再使用zlib压缩图像数据,最终完成PNG数据块的写入。// 编码 APNG 动画
const out = PNG.encode(filesBuffer, width, height, color, delays, { loop });
// 保存文件
writeFileSync('res.png', Buffer.from(out));
Libavif
libavif是一个用于AVIF图像格式的开源库,AVIF是一种基于AV1视频编码技术的图像格式,提供了高效的图像压缩和优异的图像质量。libavif提供了对AVIF图像的编码和解码功能,适用于各种图像处理应用。为了在NodeJs上运行,我们交叉编译了Libavif的完全静态的二进制文件,通过npm包进行二进制文件的发布。对于libavif的简单使用,可以通过Node.js的child_process创建一个子进程,用来执行avifenc,并监听其标准输出以获取输出数据。const avifenc = spawn(binaryPath, args);
// 监听输出数据
avifenc.stdout.on('data', (data) => {
logger.log(`avif stdout: ${data}`);
});
// 监听执行结束信号
avifenc.on('close', (code) => {
if (code !== 0) {
reject(new Error(`avifenc process exited with code ${code}`))
}
resolve(outPath);
}
FBX2GLTF
fbx2gltf是一个用于将FBX文件转换为glTF或GLB格式的工具。glTF是一种用于高效传输和加载3D模型的文件格式,而GLB是glTF的二进制版本,fbx2gltf可以将常见的3D模型格式FBX转换为glTF或GLB格式,以便在WebGL、Crab等3D渲染引擎中使用。1. 几何数据转换:将3D模型的几何数据(顶点、法线、UV坐标等)转换为glTF格式。2. 材质转换:将材质信息转换为glTF材质,包括颜色、纹理、透明度等。3. 动画转换:将动画数据转换为glTF动画,包括骨骼动画、关键帧动画等。4. 封装glTF/GLB文件:将转换后的数据封装为glTF或GLB文件,包含所有的3D模型数据、材质和动画。下面将会详细介绍部分动效转换的核心技术以及原理。分别是透明视频的原理和实现以及多格式处理。透明视频是一种在视频中包含透明通道(alpha通道)的技术,使得视频中的某些部分可以完全透明或半透明。在制作的过程中,通常会将视频的颜色信息(RGB)和透明度信息(Alpha)分开处理。为了便于编码和播放,常见的做法是将RGB和Alpha通道分别放置在视频帧的左右两部分。 视频加载失败,请刷新页面再试
刷新 
这种布局方式有助于在编码和解码过程中更好地处理透明度信息,同时保持视频的整体质量。下面是实现透明视频转换的关键步骤:1. 输入文件准备:从指定文件夹中读取PNG序列帧文件,这些文件包含了视频的每一帧图像。2. 编码器选择:根据业务场景和参数选择合适的编码器。常见的编码器包括H.264和H.265,可以针对不同的编码器配置特定的参数优化编码性能和质量。3. 透明度处理:透明度处理可以分为预透明处理和非预透明处理。当需要在视频编码之前对图像进行预处理以便进行更复杂的操作和优化时,可以进行预透明处理。预透明处理的关键步骤包括:- 使用lavfi滤镜生成一个与输入图像尺寸相同的黑色背景。
- 抽取alpha通道并进行叠加和堆叠,将处理后的RGB和Alpha通道分别放置在视频帧的左右两部分。
4、复杂滤镜处理:使用FFmpeg中的complexFilter进行图像处理,包括格式转换、填充、拆分、叠加和堆叠等操作。下面会简单介绍这部分复杂滤镜的实现步骤。'[1:v]format=pix_fmts=rgba,pad=iw:ceil(ih/2)*2:0:0:black[padv1]
其中,[1:v]表示第二个输入视频流(第一个输入是黑色背景),然后通过format参数将视频流的像素格式转换为RGBA,pad=iw:ceil(ih/2)*2:0:0:black对视频进行填充,使其高度为偶数(必要时填充黑色),以确保后续处理的正确性。
(2)RGB和Alpha通道的拆分,可以使用split[rgb][alpha]将视频流拆分为两个视频流。'[0:v][rgb]overlay=shortest=1,pad=iw:ih:0:0:black,format=pix_fmts=yuv420p[left]'
这段代码中,overlay=shortest=1将RGB视频流叠加到黑色背景上,使用最短的输入流长度,然后通过pad=iw:ih:0:0:black对叠加后的结果进行填充,确保尺寸一致。最后再将像素格式转换为YUV420P。
(4)处理Alpha通道,与第三步操作不同的是,需要使用alphaextract将Alpha通道提取出来,然后对Alpha通道进行填充,使其宽度变为偶数。'[alpha]format=pix_fmts=rgba,alphaextract,pad=ceil(iw/2)*2:ih:mod(iw\\,2):0:black,format=pix_fmts=yuv420p[right]'
(5)将两个左右视频流进行堆叠,上述的视频流我们起一个别名为[left]和[right],然后hstack进行水平堆叠,并且调整帧率。`[left][right]hstack[out];[out]setpts=N/(${fps}*TB)[final]`
至此,我们就完成了图像编码的处理工作,最后将结果保存到指定路径,由FFmpeg完成最终的输出。针对APNG、Webp以及Avif,我们分别使用了PNG解码器、libwebp_anim解码器和libavif编码器进行了相应的解码和转换工作,这部分主要涉及大量的压缩和性能优化的指令,下面我们会详细的介绍压缩与优化策略。为了满足转换产物在端上的性能表现和动效的质量,除了常见的帧率码率调整,我们在编码阶段为不同的格式均提供了更多的压缩和优化策略。下表是常用四种格式的优化策略和参数:为了方便业务方使用,我们通过两种方式计算和衡量出每种格式压缩参数的预设值,然后给业务方提供简单的高、中、低档位,使用起来方便、便捷。下面是部分预设的参考值配置:预设值需要同时满足质量和性能,因此我们通过以下两种手段进行多格式质量衡量。质量衡量
我们制定了四个指标,分别是帧数、体积、PSNR峰值信噪比和SSIM相似度。其中,帧数指动画中的图像帧的数量,帧数越多,流畅度越高,文件体积也会相应的增加。体积指的是文件的大小,体积越小,表示压缩效果越好,但可能会影响图像或动画的质量。我们可以通过控制变量法,以相同的帧数,对比不同参数及格式之间的体积。
PSNR峰值信噪比是衡量图像或视频质量的指标。PSNR值越高,表示压缩后的图像或动画质量越接近原始图像或动画。我们可以通过以下FFmpeg命令计算PSNR值。
ffmpeg -i original_file -i compressed_file -lavfi psnr="stats_file=psnr_result.txt" -f null -
SSIM相似度也是衡量质量的指标,主要关注图像的结构信息。SSIM值越接近1,表示压缩后的图像或动画质量越接近原始图像或动画。我们可以通过以下FFmpeg命令计算SSIM值。ffmpeg -i original_file -i compressed_file -lavfi ssim="stats_file=ssim_result.txt" -f null -
下面是APNG格式,原输出产物与预设值为无损的质量衡量对比:' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
PSNR与SSIM值的报告如下,接近无穷大,代表文件质量相似。真机检测
我们还会通过云真机,批量的进行动效的准出和准入检测,尤其是内存和CPU等等指标,本系列的下一篇文章将会详细介绍有关动效准入准出检测。
本文详细介绍了有关动效转换服务的技术架构和优化策略。我们分享了多种动效格式的转换和压缩方案,以及在实际业务中的应用和性能优化策略。希望这些内容能够为您提供一些启示和支持。如有任何疑问或建议,欢迎随时留言讨论,期待您的宝贵意见。