拓冰建站拓冰建站
首页 / 资讯中心 / 正文

AI视频中台源码级交付实战:Spring Boot + 低代码编排

从第一次被客户质问“你们这个平台我们想加一个视频转GIF的功能要等你们排期多久”开始我就意识到传统的“交付一个黑盒系统”模式在AI视频这条赛道上已经走不通了。项目做的是基于 Spring Boot 的 AI 视频中台最终选择了“源码级交付 低代码编排”的路线。这篇博文就把整个二次开发实战过程掰开揉碎聊聊为什么这么选、工程上怎么落地、以及源码交付时那些不亲历根本发现不了的坑。现在做 AI 视频相关的基础能力技术上已经不算什么秘密无非是拉流、转码、抽帧、切片、调用模型推理、再合成。真正的难点在两个地方一是业务接入方的需求永远在变今天要抽封面明天要贴字幕后天要横转竖二是交付之后客户/内部团队的二次开发能力参差不齐你丢给他一个“能用”的黑盒他改不了一行代码出了问题全找你。这两点叠加在一起逼着我重新思考中台的建设方式能不能把视频处理能力沉淀成可编排的积木同时把所有积木的源码、构建脚本、部署文档一起交出去让接手的团队能自己拼、自己改、自己扩展。1. 中台的“中”到底在哪AI视频任务拆解与架构分层先说一个很多人容易搞混的概念。视频中台不等于一个视频处理工具而是一套把分散的视频能力统一收口、对外提供标准化服务的体系。“中”字的关键在于它不是某个业务线的专属后端而是多个业务场景共享的基础设施。我在设计这个项目时第一步不是写代码而是把AI视频相关的需求全部拆开找共性。1.1 拆解出来的三类共性能力把接触到的需求列个清单大致能归成三类媒体处理能力转码、压缩、抽帧、切片、拼接、加水印、生成GIF、横竖屏转换。这类能力特点是耗时、吃CPU/GPU、无状态。AI 分析能力语音转写、字幕生成、人脸检测、场景识别、画面增强、智能摘要、文本转语音 TTS 。这类能力依赖具体模型耗时更长可能需要 GPU 或特定推理服务。业务适配能力把上面两类能力按业务规则串起来比如“上传视频 → 转码成 mp4 → 抽帧 → 调用人脸检测 → 生成封面 → 推送给业务系统”。业务适配层是变化最频繁的也应该是配置化程度最高的。以前的架构容易乱就是因为把这三类东西混在一个服务里写死每次需求变更都牵一发动全身。中台的做法是让前两类能力变成基础原子服务让第三类能力通过配置编排去实现。1.2 分层架构原子、引擎、接入三层我最终落地的是三层架构每一层职责单一边界很清楚。层级职责关键组件接入层对外 API、回调通知、WebSocket 推送Spring Boot Controller、Netty WebSocket编排层读取配置、构建流水线、调度任务、状态管理Pipeline Engine、State Machine、分布式任务队列原子层封装具体处理能力保证可复用、可替换FFmpeg Wrapper、模型推理 Client、OSS SDK接入层负责和外部系统打交道解决“用什么协议进来、结果怎么通知出去”编排层是整个中台的大脑解决“一个任务要做哪几步、每一步依赖什么、失败怎么办”原子层是手脚解决“每一步具体怎么执行”。这样的分层带来一个直接好处原子层扩展一个能力编排层和接入层基本不用动。这就是二次开发最友好的形态。关于技术选型Spring Boot 在这方面优势很明显生态成熟招人容易和大部分企业现有技术栈一致内置依赖注入和自动配置非常适合做“可插拔”的功能模块对高并发异步任务支持好相关整合方案多得是。相比 Node.js 胶水层和 Python 重算法层Spring Boot 做中台编排层是很顺手的选择。2. 低代码的本质不是拖拽而是可编排的视频流水线引擎深入聊之前先把话说透“低代码”在视频中台这个场景里绝不是搞一个可视化画布让业务去拖拖拽拽。那东西看着酷实际交付了没人用业务看不懂节点研发觉得拖拽比写代码还麻烦。我这里说的低代码是指任何一条视频处理流程都可以通过修改一份 JSON 配置文件来定义而不需要改动 Java 代码。2.1 用 JSON 定义一条视频处理流水线把一条业务需求转成 JSON 配置长这样{ pipelineId: portrait_to_landscape_01, name: 竖屏转横屏并生成字幕, nodes: [ { id: input, type: source, params: { sourceField: videoUrl } }, { id: transcode, type: ffmpeg, params: { vf: scale1920:1080:force_original_aspect_ratiodecrease,pad1920:1080:(ow-iw)/2:(oh-ih)/2, codec: h264 } }, { id: audio_extract, type: ffmpeg, params: { outputFormat: wav } }, { id: asr, type: ai_asr, params: { model: whisper-base, language: zh } }, { id: subtitle_burn, type: ffmpeg, params: { subtitleType: ass } }, { id: output, type: sink, params: { targetField: resultUrl } } ], edges: [ { from: input, to: transcode }, { from: transcode, to: audio_extract }, { from: transcode, to: subtitle_burn }, { from: audio_extract, to: asr }, { from: asr, to: subtitle_burn } ], globalParams: { callbackUrl: https://api.example.com/callback, priority: 5 } }你没看错核心就是一个 DAG有向无环图。节点nodes定义做什么边edges定义先后和依赖关系。引擎负责解析这张图、按拓扑序执行、把上一个节点的输出作为下一个节点的输入。2.2 引擎如何“读图”并执行写 Java 代码去执行一张 JSON 图没有想象中那么复杂。关键是三步解析定义、构建执行上下文、按依赖执行。Component public class PipelineEngine { private final MapString, NodeProcessor processorMap; public PipelineEngine(ListNodeProcessor processors) { this.processorMap processors.stream() .collect(Collectors.toMap(NodeProcessor::getType, Function.identity())); } public PipelineResult execute(PipelineDefinition definition, MapString, Object input) { // 1. 构建 DAG DirectedAcyclicGraph graph buildGraph(definition); // 2. 拓扑排序 ListString sortedNodeIds graph.topologicalSort(); // 3. 遍历执行结果存入上下文 ExecutionContext context new ExecutionContext(input); for (String nodeId : sortedNodeIds) { NodeDefinition nodeDef definition.getNode(nodeId); NodeProcessor processor processorMap.get(nodeDef.getType()); if (processor null) { throw new UnsupportedOperationException(不支持的节点类型: nodeDef.getType()); } // 依赖节点的输出已经放进上下文直接取用 Object result processor.process(nodeDef.getParams(), context); context.setOutput(nodeId, result); } return new PipelineResult(context.getAllOutputs()); } }这里每类节点对应一个NodeProcessor实现类。加一种能力就新增一个处理器、注册进去老代码一行不用改。这就是低代码和第二开发生态能共存的原因。顺带说一句为什么选 JSON 而不是 YAMLJSON 在 Java 生态里解析性能更好做 schema 校验的工具更成熟。更关键的是前端如果有需要JSON 可以直接映射成树形组件将来就算要做可视化编辑也有基础。2.3 配置模型里暗含的状态机一个视频处理任务从创建到结束状态必须严格可控。我在PipelineDefinition里加上status字段配合一个轻量级状态机管理PENDING排队中RUNNING执行中SUCCEEDED成功FAILED失败CANCELLED取消状态迁移全靠事件驱动。任务完成后发PipelineFinishedEvent监听器负责回调业务系统、推送 WebSocket、清理临时文件。这样编排引擎不需要关心“任务结束之后干什么”扩展性一下就好起来了。3. Spring Boot 工程化落地模块划分、异步任务与关键代码解读架构模型聊清楚了落到 Spring Boot 工程里有很多细节值得单独说一说。这一章节全是实操中验证过的东西直接抄作业问题不大。3.1 多模块 Maven 工程从一坨变成积木盒单体应用最大的问题不是代码多而是边界模糊。我按前面说的三层架构优化了工程结构用 Maven 多模块拆开video-platform ├── video-api/ # 对外暴露的 DTO、枚举、接口定义 ├── video-common/ # 工具类无业务逻辑 ├── video-atomic/ # 原子能力层FFmpeg封装、AI Client ├── video-orchestration/ # 编排引擎、状态机、配置模型 ├── video-server/ # Spring Boot 启动入口、Controller、WebSocket └── video-bootstrap/ # 配置文件、启动脚本、Dockerfile这套结构最核心的原则是依赖方向必须单向。video-server可以依赖video-orchestration和video-atomic但video-atomic绝对不能反向依赖上层。这样底层能力完全独立将来单独抽成微服务也很轻松。交付源码时接手的团队看video-api就知道有哪些接口看video-atomic就知道有哪些能力根本不用翻完整个工程。3.2 异步任务与线程池视频处理不能堵住接口线程视频处理一定是异步的。接口提交任务后立刻返回taskId真正处理放到线程池/消息队列里跑。这里最容易踩的坑是用 Spring 默认的线程池跑长任务会把线程耗尽影响其他接口响应。我单独配置了一个视频任务线程池Configuration public class VideoExecutorConfig { Bean(videoTaskExecutor) public Executor videoTaskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(1000); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix(video-task-); // 关键拒绝策略不能在极端情况下把任务丢掉 executor.setRejectedExecutionHandler(new CallerRunsPolicy()); executor.initialize(); return executor; } }CallerRunsPolicy的意思是队列满的时候新提交的任务由提交线程自己执行。虽然会阻塞接口线程但总比任务悄悄丢失要好。对视频中台来说任务宁可慢不能丢。实际跑下来单机 8 线程处理 1080p 转码抽帧OCR 的任务一天能扛住上千条任务量对于初期阶段完全够用。等量大了再把任务队列换成 RocketMQ 或 RabbitMQ编排引擎的接口是现成的改动成本可控。3.3 文件上传与断点续传的几个细节视频文件动辄几十上百 MB上传是个让人头疼的问题。我最终用的是预签名 URL 直传对象存储服务端不经过文件流客户端请求POST /api/v1/upload/token传入文件哈希、大小。服务端校验权限生成一个有效的对象存储预签名 URL 返回。客户端直接把文件通过 HTTP PUT 上传到对象存储不走应用服务器。传完调用POST /api/v1/tasks提交处理任务参数里带上文件 Key。这套方案的好处是应用服务器完全不用处理文件流内存和带宽都省了也不容易超时。断点续传由对象存储的分片上传能力解决客户端 SDK 基本都自带了。提示大文件上传一定要做哈希校验推荐SHA-256。最开始偷懒只对比文件大小结果有客户传了个同样大小的损坏文件转码直接失败排了半天才发现是文件问题。3.4 进度上报用 WebSocket 把过程交给用户视频处理动辄几十秒甚至几分钟接口请求-响应模型搞不定。我给中台加了一个轻量的 WebSocket 推送通道。任务执行时处理器每完成一个节点就发布一个进度事件public record ProgressEvent(String taskId, int percentage, String currentNode, String message) {}WebSocket 服务端按taskId维度推送事件。前端拿到进度后渲染进度条。需要说明的是这里我刻意没有用 Spring Messaging 那套复杂抽象直接用了WebSocketHandler接口加一个ConcurrentHashMap维护 session简单直接。考虑到视频中台的推送频率并不高几秒一条这套方案完全够稳定。4. 源码级交付的扩展性设计SPI 插件机制与 AI 算法接入实战这个章节是整个项目最有技术含量也最容易被低估的部分。“源码级交付”不是把代码打包给客户就完事了而是要让客户/接手的团队真的能自己加需求、自己修 Bug、自己接算法。为了做到这一点光有开源心态不够要有硬性的工程机制托底。4.1 为什么“源码级交付”必须配套 SPI 机制想象一下这个场景客户拿到源码他想接入自己训练的一个人脸识别模型。如果没有扩展点设计他得从 Controller 层开始把一次调用的链路整个捋一遍改完还不知道改了哪里会不会影响别的功能。有了 SPI 扩展点他的改动被限制在一个独立模块里风险大幅下降。我在项目里定义了几个核心 SPI 接口。其中一个是最关键的public interface VideoAnalysisProvider { String name(); boolean supports(String type); AnalysisResult analyze(InputStream videoStream, MapString, String params); }AI 算法接入通过 Spring Boot 的自动配置机制实现。接手的团队只要在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports里注册自己的实现类中台启动时就能自动发现新能力。这种方式的优点是完全不侵入原有代码。4.2 一个实操案例接入一个新的 AI 算法以接入一个“吸烟行为检测”算法为例。接手的团队只需要做三件事第一步新建一个 Maven 模块比如叫video-extension-smoking引入video-api依赖。第二步实现VideoAnalysisProvider接口Component public class SmokingDetectionProvider implements VideoAnalysisProvider { Override public String name() { return smokingDetect; } Override public boolean supports(String type) { return smoking.equals(type); } Override public AnalysisResult analyze(InputStream videoStream, MapString, String params) { // 调用你们自己的算法服务 float confidence yourAlgorithmClient.detect(videoStream); return new AnalysisResult().put(smoking, confidence 0.8f); } }第三步在视频流水线配置里用这个节点类型{ id: smoke_check, type: smoking, params: { threshold: 0.8 } }完事了。没有改任何中台核心代码新能力直接可以被编排引擎调度。这才是源码级交付应该有的样子给你全套源码也给你同步扩展能力的方法论你既看得懂也改得动。后来客户的对接团队照着这个模式一周内接了三个自研算法进来全程没找过我。4.3 交付文档不是 README而是“扩展手册”源码级交付代码之外最重要的资产是说明文档。我不写长篇小说式的 README我写一份《二次开发扩展指南》里面只回答四个问题怎么在本地把工程跑起来环境要求、配置文件位置、常见启动问题在哪加一个新的原子处理能力SPI 接口说明 一个完整示例怎么加一个新的 HTTP 接口Controller 的规范、返回值约定怎么改前端页面而不影响整体打包发布这份指南的价值很多时候比源码本身还大。它能极大地降低接盘侠的恐惧感和上手成本也减少了后续支持的工作量。5. 部署、监控和掉过的坑从上传超时到 Actuator 未授权访问说实话这章节才是从“能用”到“好用”的关键。项目上线几个月踩过的坑比写代码的时间还多。挑几个有代表性的说说。5.1 FFmpeg 内存溢出的真相最早跑转码任务的时候遇到过一个特别诡异的现象任务执行到一半整个应用内存报警有时候直接 OOM。一开始以为是视频文件太大加内存、限制并发都没用。最终还是靠压式排查法定位到问题。执行 FFmpeg 命令时用的是ProcessBuilder但没有正确处理子进程的输出流和错误流。子进程只要往标准错误流里写内容缓冲区满了之后子进程就会阻塞等待读取而 Java 主进程又没在读结果两边僵住再加上后续任务排队内存就爆了。解决办法不复杂却非常关键Process process processBuilder.start(); // 如果标准错误不读子进程会阻塞在这里 // 必须用异步方式持续消费 CompletableFuture.runAsync(() - { BufferedReader reader new BufferedReader(new InputStreamReader(process.getErrorStream())); String line; while ((line reader.readLine()) ! null) { log.info([ffmpeg] {}, line); } });这里给所有做外部进程调用的同行提个醒只要用了ProcessBuilder无论你觉得那个程序会有多少输出都得立刻消费它的输出流和错误流。不要有这个侥幸心理。5.2 大文件上传的客户端“假死”对象存储直传解决了服务端压力但客户端在上传大文件时出过一个问题上传进度条走到 100% 后前端界面卡死任务列表迟迟不刷新。排查结果是直传完成后客户端回调提交任务接口时接口响应中包含了一个非常大的 JSON内嵌了完整的上传凭证和签名信息解析超时。后来把任务提交接口的响应精简为只返回 taskId 和几个必要字段这个问题就消失了。这也是个经典教训接口返回的内容不是越多越好尤其在高频调用场景下少即是多。5.3 Spring Boot Actuator 未授权访问风险这个坑是真真正正的“在热词里看到自己”系列。项目一开始为了方便排查线上问题开了 Actuator 的绝大多数端点包括/actuator/env和/actuator/heapdump。结果安全扫描一出来直接报高危漏洞。原因也简单heapdump 端点会把堆内存里的所有对象导出来明文密码、Token 全都裸露在外。修复方案分两步走第一只暴露必要的端点第二通过management.server.port把 Actuator 放到独立端口不对外网开放。最终贴合生产环境的配置如下management: endpoints: web: exposure: include: health,info endpoint: health: show-details: never server: port: 9191不要让“方便排查”成为引入重大安全隐患的借口。该封的端口一定要封该收的端点一定要收。5.4 回调风暴与幂等处理视频任务处理完成后会回调业务系统。在一次压测中出现过极端情况几百个任务同时完成回调请求像风暴一样打出去把客户业务系统打挂了。加一个简单的信号量限流之外更重要的是回调接口必须做幂等。我在回调逻辑里加了本地缓存去重同一taskId只回调一次public void callbackTaskResult(String taskId, TaskResult result) { if (okHttpClient null) { return; } if (callbackCache.add(taskId)) { retryCallback(taskId, result); } }注意这是分布式环境下会存在问题多个实例之间需改用 Redis 去重。对这个项目而言早期部署单实例时本地缓存够用上了多实例再换 Redis 也不迟。5.5 配置热加载改 JSON 不想重启低代码的价值很大程度取决于改配置的快慢。如果改一个 JSON 都要重新打包重启那这套体系就名存实亡了。我用 Spring Boot 的ConfigurationProperties 定时刷新实现了轻量级方案配置存储在数据库或独立的 JSON 文件里引擎缓存 pipelineId 与定义对象每 30 秒检查版本号版本变了自动重新加载。实际使用中这个设计帮了大忙。有一次客户凌晨提需求要调整某个视频任务的参数群里收到消息后远程更新了配置内容两分钟后新任务就走的新流程不带重启的。这体验对业务方来说就意味着“平台是活的”。6. 实战验证用一套配置跑通一个竖屏转横屏并带字幕的完整流程讲到这里上一堆抽象机制不如直接来一个端到端案例。就以“某平台运营要发布一条短视频原始视频是竖屏想变成横屏发布同时要自动添加上字幕”为例看一下中台完整处理链路。6.1 流水线配置与核心参数解析以 2.1 节的 JSON 配置为例这里逐段说明关键参数的设计意图sourceField: videoUrl告诉引擎任务的原始视频地址从提交参数的videoUrl字段读取。这是引擎与外部输入对齐的约定。scale1920:1080:force_original_aspect_ratiodecrease,pad1920:1080...FFmpeg 滤镜。竖屏视频被等比缩小到高为 1080宽度不足的部分用黑色填充到两边。视频处理里最常用且稳定的方案。音频提取节点把转码视频中的音轨提取为 wav 文件供 ASR 识别使用。模型的输入与视频文件分离可以避免不必要的大文件传输。字幕烧录节点它依赖 ASR 节点的输出字幕文件同时依赖转码节点的输出横屏视频。这种多入边的依赖关系是 DAG 比线性管道灵活的地方。6.2 实际执行日志与耗时分布跑完一个 60 秒的竖屏短视频日志大致如下单机、CPU 8 核配置节点耗时说明transcode横屏化8s大部分 CPU 花费在像素格式转换和编码上audio_extract1s提取音轨很快asr语音识别4s依赖推理服务GPU 会更佳subtitle_burn字幕烧录5s重新编码一次总耗时 18 秒左右。这个性能表现和客户“提交任务后 30 秒内看到结果”的需求是对得上的。对接方在任务提交后的 WebSocket 推送里可以实时看到“正在转码”“正在识别字幕”“正在烧录”等阶段状态体验上的反馈非常不错。6.3 这条流程给团队带来的直接收益同一个配置上线后两周内被三个不同业务方复用了A 部门拿来处理课程视频只需把横竖屏参数改掉加了一个 AI 摘要节点B 部门拿来处理用户 UGC 视频在尾部加了一个水印节点C 部门直接把 ASR 结果输出到数据库用于内容审核的数据源。这就是中台的价值——能力沉淀下来以后新业务并不是从 0 开始做而是从 80% 开始。后续做几个新业务接入这对交付方和被交付方来说节约的成本都是肉眼可见的。7. 关于这套路线的反思与心得做完整个项目回头看有些话不吐不快。第一低代码不是降维打击而是降低协作成本。在视频中台这个场景里业务方并不关心你用的是什么设计模式他关心的是“我提一个需求要等多久”。配置化编排把等待时间从“等一个开发排期”变成“改一段 JSON 内容”这是质的变化。第二源码级交付不是终点需要交付的是一套扩展心智。交源码简单让对方团队建立“这东西我能改”的信心才更难。SPI 机制、模块化结构、傻瓜式文档都是在降低这个心理门槛。源码本身可以读但如果没有好的扩展设计接手的团队大概率还是不敢动核心代码最后还是把所有需求抛回到你这边。第三任何技术选型都要考虑团队的真实水平。为什么不选 Serverless 架构为什么不直接用云厂商的视频处理服务因为这些方案虽然省事但对很多团队来说是个黑盒他们无法掌控细节和成本。Spring Boot FFmpeg 对象存储的组合足够成熟、足够透明、也足够让一个中等水平的 Java 开发能上手二次开发。在工程领域“够用、开放、好接手”往往比“高精尖、酷炫”更可持续。如果要给后来者一个最实在的建议那就是先把一个最简单的“转码 回调”流程完整跑通再逐步加编排、加 AI 能力。别一开始就指望 DAG 引擎一步到位。视频处理中最容易出问题的是底层命令执行、文件生命周期管理、异常恢复这些“脏活累活”这些稳定了上层再怎么扩展都比较稳。这一次迭代下来最大的收获不是代码量而是让我对“交付”这件事有了新的理解真正高质量的项目交付是让对方不再需要你也能持续往前走而源码级交付加低代码编排恰好就是一条可以实现这个目标的稳妥路径。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门