Tio Boot DocsTio Boot Docs
Home
  • java-db
  • api-table
  • Enjoy
  • Tio Boot Admin
  • ai_agent
  • translator
  • knowlege_base
  • ai-search
  • 案例
Abount
  • Github
  • Gitee
Home
  • java-db
  • api-table
  • Enjoy
  • Tio Boot Admin
  • ai_agent
  • translator
  • knowlege_base
  • ai-search
  • 案例
Abount
  • Github
  • Gitee
  • 01_tio-boot 简介

    • tio-boot:新一代高性能 Java Web 开发框架
    • tio-boot 入门示例
    • Tio-Boot 配置 : 现代化的配置方案
    • tio-boot 整合 Logback
    • tio-boot 整合 hotswap-classloader 实现热加载
    • 自行编译 tio-boot
    • 最新版本
    • 开发规范
  • 02_部署

    • 使用 Maven Profile 实现分环境打包 tio-boot 项目
    • Maven 项目配置详解:依赖与 Profiles 配置
    • tio-boot 打包成 FastJar
    • 使用 GraalVM 构建 tio-boot Native 程序
    • 使用 Docker 部署 tio-boot
    • 部署到 Fly.io
    • 部署到 AWS Lambda
    • 到阿里云云函数
    • 使用 Deploy 工具部署
    • 胖包与瘦包的打包与部署
    • 使用 Jenkins 部署 Tio-Boot 项目
    • 使用 Nginx 反向代理 Tio-Boot
    • 使用 Supervisor 管理 Java 应用
  • 03_配置

    • 配置参数
    • 服务器监听器
    • 内置缓存系统 AbsCache
    • 使用 Redis 作为内部 Cache
    • 静态文件处理器
    • 基于域名的静态资源隔离
    • DecodeExceptionHandler
  • 04_原理

    • 生命周期
    • 请求处理流程
    • 重要的类
  • 05_json

    • Json
    • 接受 JSON 和响应 JSON
    • 响应实体类
  • 06_web

    • 概述
    • 文件上传
    • 接收请求参数
    • 接收日期参数
    • 接收数组参数
    • 返回字符串
    • 返回文本数据
    • 返回网页
    • 请求和响应字节
    • 文件下载
    • 返回视频文件并支持断点续传
    • http Session
    • Cookie
    • HttpRequest
    • HttpResponse
    • Resps
    • RespBodyVo
    • /zh/06_web/19.html
    • 全局异常处理器
    • 异步
    • 动态 返回 CSS 实现
    • 返回图片
    • Transfer-Encoding: chunked 实时音频播放
    • Server-Sent Events (SSE)
    • 接口访问统计
    • 接口请求和响应数据记录
    • 自定义 Handler 转发请求
    • 使用 HttpForwardHandler 转发所有请求
    • 跨域
    • 添加 Controller
    • 常用工具类
    • HTTP Basic 认证
    • WebJars
    • JProtobuf
  • 07_validate

    • 数据紧校验规范
    • 参数校验
  • 08_websocket

    • 使用 tio-boot 搭建 WebSocket 服务
    • WebSocket 聊天室项目示例
  • 09_java-db

    • java‑db
    • 操作数据库入门示例
    • SQL 模板
    • 数据源配置与使用
    • ActiveRecord
    • Model
    • 生成器与 Model
    • Db 工具类
    • 批量操作
    • 数据库事务处理
    • Cache 缓存
    • Dialect 多数据库支持
    • 表关联操作
    • 复合主键
    • Oracle 支持
    • Enjoy SQL 模板
    • Java-DB 整合 Enjoy 模板最佳实践
    • 多数据源支持
    • 独立使用 ActiveRecord
    • 调用存储过程
    • java-db 整合 Guava 的 Striped 锁优化
    • 生成 SQL
    • 通过实体类操作数据库
    • java-db 读写分离
    • Spring Boot 整合 Java-DB
    • like 查询
    • 常用操作示例
    • Druid 监控集成指南
    • SQL 统计
  • 10_api-table

    • ApiTable 概述
    • 使用 ApiTable 连接 SQLite
    • 使用 ApiTable 连接 Mysql
    • 使用 ApiTable 连接 Postgres
    • 使用 ApiTable 连接 TDEngine
    • 使用 api-table 连接 oracle
    • 使用 api-table 连接 mysql and tdengine 多数据源
    • EasyExcel 导出
    • EasyExcel 导入
    • TQL(Table SQL)前端输入规范
    • ApiTable 实现增删改查
    • 数组类型
    • 单独使用 ApiTable
  • 11_aop

    • JFinal-aop
    • Aop 工具类
    • 配置
    • 配置
    • 独立使用 JFinal Aop
    • @AImport
    • 原理解析
  • 12_cache

    • Caffine
    • Jedis-redis
    • hutool RedisDS
    • Redisson
    • Caffeine and redis
    • CacheUtils 工具类
    • 使用 CacheUtils 整合 caffeine 和 redis 实现的两级缓存
    • 使用 java-db 整合 ehcache
    • 使用 java-db 整合 redis
    • Java DB Redis 相关 Api
    • redis 使用示例
  • 13_认证和权限

    • hutool-JWT
    • FixedTokenInterceptor
    • 使用内置 TokenManager 实现登录
    • 用户系统
    • 重置密码
    • 匿名登录
    • Google 登录
    • 权限校验注解
    • Sa-Token
    • sa-token 登录注册
    • StpUtil.isLogin() 源码解析
    • 短信登录
    • 移动端微信登录实现指南
    • 移动端重置密码
  • 14_i18n

    • i18n
  • 15_enjoy

    • tio-boot 整合 Enjoy 模版引擎文档
    • 引擎配置
    • 表达式
    • 指令
    • 注释
    • 原样输出
    • Shared Method 扩展
    • Shared Object 扩展
    • Extension Method 扩展
    • Spring boot 整合
    • 独立使用 Enjoy
    • tio-boot enjoy 自定义指令 localeDate
    • PromptEngine
    • Enjoy 入门示例-擎渲染大模型请求体
    • Enjoy 使用示例
  • 16_定时任务

    • Quartz 定时任务集成指南
    • 分布式定时任务 xxl-jb
    • cron4j 使用指南
  • 17_tests

    • TioBootTest 类
  • 18_tio

    • TioBootServer
    • tio-core
    • 内置 TCP 处理器
    • 独立启动 UDPServer
    • 使用内置 UDPServer
    • t-io 消息处理流程
    • tio-运行原理详解
    • TioConfig
    • ChannelContext
    • Tio 工具类
    • 业务数据绑定
    • 业务数据解绑
    • 发送数据
    • 关闭连接
    • Packet
    • 监控: 心跳
    • 监控: 客户端的流量数据
    • 监控: 单条 TCP 连接的流量数据
    • 监控: 端口的流量数据
    • 单条通道统计: ChannelStat
    • 所有通道统计: GroupStat
    • 资源共享
    • 成员排序
    • ssl
    • DecodeRunnable
    • 使用 AsynchronousSocketChannel 响应数据
    • 拉黑 IP
    • 深入解析 Tio 源码:构建高性能 Java 网络应用
  • 19_aio

    • ByteBuffer
    • AIO HTTP 服务器
    • 自定义和线程池和池化 ByteBuffer
    • AioHttpServer 应用示例 IP 属地查询
    • 手写 AIO Http 服务器
  • 20_netty

    • Netty TCP Server
    • Netty Web Socket Server
    • 使用 protoc 生成 Java 包文件
    • Netty WebSocket Server 二进制数据传输
    • Netty 组件详解
  • 21_netty-boot

    • Netty-Boot
    • 原理解析
    • 整合 Hot Reload
    • 整合 数据库
    • 整合 Redis
    • 整合 Elasticsearch
    • 整合 Dubbo
    • Listener
    • 文件上传
    • 拦截器
    • Spring Boot 整合 Netty-Boot
    • SSL 配置指南
    • ChannelInitializer
    • Reserve
  • 22_MQ

    • Mica-mqtt
    • EMQX
    • Disruptor
  • 23_tio-utils

    • tio-utils
    • HttpUtils
    • Notification
    • 邮箱
    • JSON
    • 读取文件
    • Base64
    • 上传和下载
    • Http
    • Telegram
    • RsaUtils
    • EnvUtils 使用文档
    • 系统监控
    • 毫秒并发 ID (MCID) 生成方案
  • 24_tio-http-server

    • 使用 Tio-Http-Server 搭建简单的 HTTP 服务
    • tio-boot 添加 HttpRequestHandler
    • 在 Android 上使用 tio-boot 运行 HTTP 服务
    • tio-http-server-native
    • handler 常用操作
  • 25_tio-websocket

    • WebSocket 服务器
    • WebSocket Client
  • 26_tio-im

    • 通讯协议文档
    • ChatPacket.proto 文档
    • java protobuf
    • 数据表设计
    • 创建工程
    • 登录
    • 历史消息
    • 发消息
  • 27_mybatis

    • Tio-Boot 整合 MyBatis
    • 使用配置类方式整合 MyBatis
    • 整合数据源
    • 使用 mybatis-plus 整合 tdengine
    • 整合 mybatis-plus
  • 28_mongodb

    • tio-boot 使用 mongo-java-driver 操作 mongodb
  • 29_elastic-search

    • Elasticsearch
    • JavaDB 整合 ElasticSearch
    • Elastic 工具类使用指南
    • Elastic-search 注意事项
    • ES 课程示例文档
  • 30_magic-script

    • tio-boot 整合 magic-script
  • 31_groovy

    • tio-boot 整合 Groovy
  • 32_firebase

    • 整合 google firebase
    • Firebase Storage
    • Firebase Authentication
    • 使用 Firebase Admin SDK 进行匿名用户管理与自定义状态标记
    • 导出用户
    • 注册回调
    • 登录注册
  • 33_文件存储

    • 文件上传数据表
    • 本地存储
    • 使用 AWS S3 存储文件并整合到 Tio-Boot 项目中
    • 存储文件到 腾讯 COS
  • 34_spider

    • jsoup
    • 爬取 z-lib.io 数据
    • 整合 WebMagic
    • WebMagic 示例:爬取学校课程数据
    • Playwright
    • Flexmark (Markdown 处理器)
    • tio-boot 整合 Playwright
    • 缓存网页数据
  • 36_integration_thirty_party

    • tio-boot 整合 okhttp
    • 整合 GrpahQL
    • 集成 Mailjet
    • 整合 ip2region
    • 整合 GeoLite 离线库
    • 整合 Lark 机器人指南
    • 集成 Lark Mail 实现邮件发送
    • Thymeleaf
    • Swagger
    • Clerk 验证
  • 37_dubbo

    • 概述
    • dubbo 2.6.0
    • dubbo 2.6.0 调用过程
    • dubbo 3.2.0
  • 38_spring

    • Spring Boot Web 整合 Tio Boot
    • spring-boot-starter-webflux 整合 tio-boot
    • Tio Boot 整合 Spring Boot Starter
    • Tio Boot 整合 Spring Boot Starter Data Redis 指南
  • 39_spring-cloud

    • tio-boot spring-cloud
  • 40_mysql

    • 使用 Docker 运行 MySQL
    • /zh/42_mysql/02.html
  • 41_postgresql

    • PostgreSQL 安装
    • PostgreSQL 主键自增
    • PostgreSQL 日期类型
    • Postgresql 金融类型
    • PostgreSQL 数组类型
    • PostgreSQL 全文检索
    • PostgreSQL 查询优化
    • 获取字段类型
    • PostgreSQL 向量
    • PostgreSQL 优化向量查询
    • PostgreSQL 其他
  • 43_oceanbase

    • 快速体验 OceanBase 社区版
    • 快速上手 OceanBase 数据库单机部署与管理
    • 诊断集群性能
    • 优化 SQL 性能指南
    • /zh/43_oceanbase/05.html
  • 50_media

    • JAVE 提取视频中的声音
    • Jave 提取视频中的图片
    • /zh/50_media/03.html
  • 51_asr

    • Whisper-JNI
  • 54_native-media

    • java-native-media
    • JNI 入门示例
    • mp3 拆分
    • mp4 转 mp3
    • 使用 libmp3lame 实现高质量 MP3 编码
    • Linux 编译
    • macOS 编译
    • 从 JAR 包中加载本地库文件
    • 支持的音频和视频格式
    • 任意格式转为 mp3
    • 通用格式转换
    • 通用格式拆分
    • 视频合并
    • VideoToHLS
    • split_video_to_hls 支持其他语言
    • 持久化 HLS 会话
  • 55_telegram4j

    • 数据库设计
    • /zh/55_telegram4j/02.html
    • 基于 MTProto 协议开发 Telegram 翻译机器人
    • 过滤旧消息
    • 保存机器人消息
    • 定时推送
    • 增加命令菜单
    • 使用 telegram-Client
    • 使用自定义 StoreLayout
    • 延迟测试
    • Reactor 错误处理
    • Telegram4J 常见错误处理指南
  • 56_telegram-bots

    • TelegramBots 入门指南
    • 使用工具库 telegram-bot-base 开发翻译机器人
  • 60_LLM

    • 简介
    • AI 问答
    • /zh/60_LLM/03.html
    • /zh/60_LLM/04.html
    • 增强检索(RAG)
    • 结构化数据检索
    • 搜索+AI
    • 集成第三方 API
    • 后置处理
    • 推荐问题生成
    • 连接代码执行器
    • 避免 GPT 混乱
    • /zh/60_LLM/13.html
  • 61_ai_agent

    • 数据库设计
    • 示例问题管理
    • 会话管理
    • 历史记录
    • 对接 Perplexity API
    • 意图识别与生成提示词
    • 智能问答模块设计与实现
    • 文件上传与解析文档
    • 翻译
    • 名人搜索功能实现
    • Ai studio gemini youbue 问答使用说明
    • 自建 YouTube 字幕问答系统
    • 自建 获取 youtube 字幕服务
    • 通用搜索
    • /zh/61_ai_agent/15.html
    • 16
    • 17
    • 18
    • 在 tio-boot 应用中整合 ai-agent
    • 16
  • 62_translator

    • 简介
  • 63_knowlege_base

    • 数据库设计
    • 用户登录实现
    • 模型管理
    • 知识库管理
    • 文档拆分
    • 片段向量
    • 命中测试
    • 文档管理
    • 片段管理
    • 问题管理
    • 应用管理
    • 向量检索
    • 推理问答
    • 问答模块
    • 统计分析
    • 用户管理
    • api 管理
    • 存储文件到 S3
    • 文档解析优化
    • 片段汇总
    • 段落分块与检索
    • 多文档解析
    • 对话日志
    • 检索性能优化
    • Milvus
    • 文档解析方案和费用对比
    • 离线运行向量模型
  • 64_ai-search

    • ai-search 项目简介
    • ai-search 数据库文档
    • ai-search SearxNG 搜索引擎
    • ai-search Jina Reader API
    • ai-search Jina Search API
    • ai-search 搜索、重排与读取内容
    • ai-search PDF 文件处理
    • ai-search 推理问答
    • Google Custom Search JSON API
    • ai-search 意图识别
    • ai-search 问题重写
    • ai-search 系统 API 接口 WebSocket 版本
    • ai-search 搜索代码实现 WebSocket 版本
    • ai-search 生成建议问
    • ai-search 生成问题标题
    • ai-search 历史记录
    • Discover API
    • 翻译
    • Tavily Search API 文档
    • 对接 Tavily Search
    • 火山引擎 DeepSeek
    • 对接 火山引擎 DeepSeek
    • ai-search 搜索代码实现 SSE 版本
    • jar 包部署
    • Docker 部署
    • 爬取一个静态网站的所有数据
    • 网页数据预处理
    • 网页数据检索与问答流程整合
  • 65_java-linux

    • Java 执行 python 代码
    • 通过大模型执行 Python 代码
    • MCP 协议
    • Cline 提示词
    • Cline 提示词-中文版本
  • 66_manim

    • 简介
    • Manim 开发环境搭建
    • 生成场景提示词
    • 生成代码
    • 完整脚本示例
    • 语音合成系统
    • Fish.audio TTS 接口说明文档与 Java 客户端封装
    • 整合 fishaudio 到 java-uni-ai-server 项目
    • 执行 Python (Manim) 代码
    • 使用 SSE 流式传输生成进度的实现文档
    • 整合全流程完整文档
    • HLS 动态推流技术文档
    • manim 分场景生成代码
    • 分场景运行代码及流式播放支持
    • 分场景业务端完整实现流程
    • Maiim布局管理器
    • 仅仅生成场景代码
    • 使用 modal 运行 manim 代码
    • Python 使用 Modal GPU 加速渲染
    • Modal 平台 GPU 环境下运行 Manim
    • Modal Manim OpenGL 安装与使用
    • 优化 GPU 加速
    • 生成视频封面流程
    • Java 调用 manim 命令 执行代码 生成封面
    • Manim 图像生成服务客户端文档
    • manim render help
    • 显示 中文公式
    • manimgl
    • EGL
    • /zh/66_manim/30.html
    • /zh/66_manim/31.html
    • 成本核算
    • /zh/66_manim/33.html
  • 70_tio-boot-admin

    • 入门指南
    • 初始化数据
    • token 存储
    • 与前端集成
    • 文件上传
    • 网络请求
    • 图片管理
    • /zh/70_tio-boot-admin/08.html
    • Word 管理
    • PDF 管理
    • 文章管理
    • 富文本编辑器
  • 71_tio-boot

    • /zh/71_tio-boot/01.html
    • Swagger 整合到 Tio-Boot 中的指南
    • HTTP/1.1 Pipelining 性能测试报告
  • 80_性能测试

    • 压力测试 - tio-http-serer
    • 压力测试 - tio-boot
    • 压力测试 - tio-boot-native
    • 压力测试 - netty-boot
    • 性能测试对比
    • TechEmpower FrameworkBenchmarks
    • 压力测试 - tio-boot 12 C 32G
  • 99_案例

    • 封装 IP 查询服务
    • tio-boot 案例 - 全局异常捕获与企业微信群通知
    • tio-boot 案例 - 文件上传和下载
    • tio-boot 案例 - 整合 ant design pro 增删改查
    • tio-boot 案例 - 流失响应
    • tio-boot 案例 - 增强检索
    • tio-boot 案例 - 整合 function call
    • tio-boot 案例 - 定时任务 监控 PostgreSQL、Redis 和 Elasticsearch
    • Tio-Boot 案例:使用 SQLite 整合到登录注册系统
    • tio-boot 案例 - 执行 shell 命令

分场景运行代码及流式播放支持

  • 1. 运行过程说明
    • 1.1 首次场景渲染
    • 1.2 后续场景代码提交
    • 1.3 场景合并与结束
  • 2. 代码实现
    • 2.1 ManimHanlder.java
    • 2.2 ManimService.java 实现
    • 2.3 ManimService.java
  • 3. 总结

本篇文档旨在详细说明如何分场景运行代码,以及如何利用流式传输(Stream)技术实现视频播放。文档内容包含运行过程说明、接口调用示例以及完整的 Java 代码实现,并对各个模块进行了详细解释,帮助读者全面理解整个流程。


1. 运行过程说明

在之前的工作中,我们已经实现了分场景生成代码,本节主要介绍如何分场景运行代码以及如何实现流式播放。

1.1 首次场景渲染

调用接口 /manim/start 后,会返回如下 JSON 格式响应:

{
  "sessionId": "500972768364683264",
  "sessionIdPrt": "105553151428672",
  "exitCode": 0,
  "executeCode": null,
  "viode_length": 0.0,
  "taskId": "0",
  "stdOut": null,
  "stdErr": null,
  "output": "/data/session/500972768364683264/main.m3u8",
  "images": null,
  "videos": null
}

在首次场景渲染成功后,生成的 main.m3u8 文件即可用于播放,播放地址例如:

http://xxx/data/session/500972768364683264/main.m3u8

1.2 后续场景代码提交

后续的场景执行需要通过提交代码的方式运行,调用示例为:

POST /manim?session_prt=105553141509952&m3u8_path=./data/hls/500960829722333184/main.m3u8
{CODE}

每次提交代码执行后,成功返回对应场景的地址,例如:

  • Sence 01
    /data/hls/500972882755936256/main.m3u8
  • Sence 02
    /data/hls/500973222591029248/main.m3u8
  • Sence 03
    /data/hls/500973399972339712/main.m3u8
  • Sence 04
    /data/hls/500973577777274880/main.m3u8
  • Sence 05
    /data/hls/500973739706769408/main.m3u8

1.3 场景合并与结束

在所有场景提交并执行完毕后,调用 /manim/finish 接口完成整个视频的合并工作。调用示例如下:

/manim/finish?session_prt=105553151428672&m3u8_path=./data/session/500972768364683264/main.m3u8&videos=/data/hls/500972882755936256/main.m3u8,/data/hls/500973222591029248/main.m3u8,/data/hls/500973399972339712/main.m3u8,/data/hls/500973577777274880/main.m3u8,/data/hls/500973739706769408/main.m3u8

响应示例如下:

{
  "sessionId": "0",
  "sessionIdPrt": "0",
  "exitCode": 0,
  "executeCode": null,
  "viode_length": 87.666667,
  "taskId": "0",
  "stdOut": null,
  "stdErr": null,
  "output": null,
  "images": null,
  "videos": null
}

在该步骤中,后端会合并各个场景的视频片段,生成最终的 MP4 文件,并通过 HLS 分段生成新的 .m3u8 文件,同时获取视频的时长信息。


2. 代码实现

接下来的代码实现主要分为两部分:HTTP 请求处理的 ManimHanlder 和具体业务逻辑处理的 ManimService。下面详细介绍各个模块的实现细节。

2.1 ManimHanlder.java

该类用于处理 HTTP 请求,并调用业务服务层执行相关操作。主要包含以下几个方法:

  • start
    用于初始化 HLS 渲染场景,创建新的会话目录,并调用 NativeMedia.initPersistentHls 初始化持久化 HLS 播放。返回的 JSON 中包含 sessionId、sessionIdPrt(对应持久化会话的标识)以及生成的 m3u8 文件路径。

  • finish
    用于结束会话。根据传入的 m3u8_path 判断对应的 HLS 文件是否存在,如果存在则调用 NativeMedia.finishPersistentHls 完成会话合并,否则调用 NativeMedia.freeHlsSession 释放会话。
    同时,在传入 videos 参数时会对视频文件进行合并操作,利用 NativeMedia.merge 方法合并各个分场景生成的 MP4 文件,并计算视频时长。

  • index
    用于接受代码执行请求。当请求带有 stream 参数时,会添加 Server-Sent Events(SSE)的响应头,实现流式数据返回;否则直接执行代码并返回最终结果。

以下是 ManimHanlder.java 的完整代码实现:

package com.litongjava.linux.handler;

import java.io.File;

import com.litongjava.jfinal.aop.Aop;
import com.litongjava.linux.ProcessResult;
import com.litongjava.linux.service.ManimService;
import com.litongjava.media.NativeMedia;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.core.ChannelContext;
import com.litongjava.tio.core.Tio;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.server.util.CORSUtils;
import com.litongjava.tio.utils.hutool.FilenameUtils;
import com.litongjava.tio.utils.json.JsonUtils;
import com.litongjava.tio.utils.snowflake.SnowflakeIdUtils;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ManimHanlder {
  ManimService manimService = Aop.get(ManimService.class);

  public HttpResponse start(HttpRequest request) {
    HttpResponse response = TioRequestContext.getResponse();
    CORSUtils.enableCORS(response);
    long sessionId = SnowflakeIdUtils.id();
    String subPath = "./data/session/" + sessionId;
    new File(subPath).mkdirs();
    String m3u8Path = subPath + "/main.m3u8";
    String tsPattern = subPath + "/segment_video_%03d.ts";
    int startNumber = 0;
    int segmentDuration = 10; // 每个分段时长(秒)
    long initPersistentHls = NativeMedia.initPersistentHls(m3u8Path, tsPattern, startNumber, segmentDuration);

    ProcessResult processResult = new ProcessResult();
    processResult.setSessionId(sessionId);
    processResult.setSessionIdPrt(initPersistentHls);
    processResult.setOutput(m3u8Path.replace("./", "/"));
    response.setJson(processResult);
    return response;
  }

  public HttpResponse finish(HttpRequest request) {
    HttpResponse response = TioRequestContext.getResponse();
    CORSUtils.enableCORS(response);

    Long session_prt = request.getLong("session_prt");
    ProcessResult processResult = new ProcessResult();
    if (session_prt == null) {
      processResult.setStdErr("m3u8_path can not be empty");
      return response.setJson(processResult);
    }
    String m3u8Path = request.getString("m3u8_path");
    if (m3u8Path == null) {
      processResult.setStdErr("m3u8_path can not be empty");
      return response.setJson(processResult);
    } else {
      m3u8Path = "." + m3u8Path;
    }
    String videos = request.getString("videos");
    log.info("session_prt:{},m3u8Path:{}", session_prt, m3u8Path, videos);
    File file = new File(m3u8Path);
    if (file.exists()) {
      log.info("finishPersistentHls");
      NativeMedia.finishPersistentHls(session_prt, m3u8Path);
    } else {
      log.info("freeHlsSession");
      NativeMedia.freeHlsSession(session_prt);
    }

    if (videos != null) {
      String subPath = FilenameUtils.getSubPath(m3u8Path);
      String outputPath = subPath + "/main.mp4";
      String[] split = videos.split(",");
      String[] mp4FileList = new String[split.length];
      for (int i = 0; i < split.length; i++) {
        String string = "." + split[i].replace(".m3u8", ".mp4");
        mp4FileList[i] = string;
      }
      log.info("merge:{}", JsonUtils.toJson(mp4FileList));
      boolean merged = NativeMedia.merge(mp4FileList, outputPath);
      log.info("merged:{}", merged);
      if (merged) {
        double videoLength = NativeMedia.getVideoLength(outputPath);
        processResult.setViode_length(videoLength);
      }
    }

    return response.setJson(processResult);
  }

  public HttpResponse index(HttpRequest request) {
    HttpResponse response = TioRequestContext.getResponse();
    CORSUtils.enableCORS(response);

    ChannelContext channelContext = request.getChannelContext();
    Boolean stream = request.getBoolean("stream");
    Long session_prt = request.getLong("session_prt");
    String m3u8Path = request.getString("m3u8_path");
    log.info("{},{}", session_prt, m3u8Path);
    if (stream == null) {
      stream = false;
    }
    String code = request.getBodyString();

    if (stream) {
      response.addServerSentEventsHeader();
      Tio.send(channelContext, response);
      response.setSend(false);
    }
    try {
      ProcessResult executeScript = manimService.executeCode(code, stream, session_prt, m3u8Path, channelContext);
      if (executeScript != null) {
        response.setJson(executeScript);
      }

    } catch (Exception e) {
      log.error(e.getMessage(), e);
      response.setStatus(500);
      response.setString(e.getMessage());
    }
    return response;
  }

}

2.2 ManimService.java 实现

该服务类负责处理脚本代码的执行、文件监控以及视频合并等核心逻辑。下面对主要方法和功能做详细介绍:

2.2.1 executeCode 方法

  • 目录创建与代码预处理
    在 executeCode 方法中,首先会创建缓存目录 cache,并为每次提交生成唯一 ID 作为子文件夹的标识;同时将传入的代码字符串中占位符 #(output_path) 替换为实际的输出目录。

  • 脚本文件生成
    在 scripts 目录下创建对应的子目录,并将预处理后的 Python 脚本保存为 script.py 文件。

  • 视频渲染与目录监控
    根据是否需要流式返回结果:

    • 流式发送(stream 为 true)
      创建需要监控的文件夹(存放部分视频片段),启动一个独立线程来监控目录中文件的创建(使用 Java WatchService),以便实时将新生成的文件路径通知客户端。接下来通过异步线程执行 Python 脚本,执行结束后将结果(包括最终生成的视频文件路径)以 SSE 形式发送回客户端。

    • 非流式执行
      直接调用 execute 方法执行 Python 脚本,等待脚本执行结束后获取结果,并进一步调用 NativeMedia.splitVideoToHLS 将生成的视频文件切分为 HLS 格式,再调用 NativeMedia.appendVideoSegmentToHls 将当前场景的视频追加到整体会话中。

  • 视频合并与 HLS 分段
    若脚本执行生成的视频存在,则将视频文件拷贝至目标文件夹,并进行 HLS 分段处理,生成新的 m3u8 播放列表供播放使用。

2.2.2 execute 方法

该方法利用 Java 的 ProcessBuilder 来调用 Python 解释器执行脚本,并将标准输出和错误输出重定向到日志文件中。待脚本执行完毕后,读取日志内容,并封装在 ProcessResult 对象中返回。

2.2.3 watchFolder 方法

该方法使用 WatchService 监控指定目录中新创建的文件,并通过 Tio 的 SSE(Server-Sent Events)功能,将每个新文件的全路径发送给客户端,实现实时流式数据传输。

下面分别展示 ManimService.java 的完整代码实现,以下代码块均原样保留,未做任何省略。


2.3 ManimService.java

package com.litongjava.linux.service;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.concurrent.TimeUnit;

import com.litongjava.linux.ProcessResult;
import com.litongjava.media.NativeMedia;
import com.litongjava.tio.core.ChannelContext;
import com.litongjava.tio.core.Tio;
import com.litongjava.tio.http.common.sse.SsePacket;
import com.litongjava.tio.http.server.util.SseEmitter;
import com.litongjava.tio.utils.hutool.FileUtil;
import com.litongjava.tio.utils.json.JsonUtils;
import com.litongjava.tio.utils.snowflake.SnowflakeIdUtils;
import com.litongjava.tio.utils.thread.TioThreadUtils;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ManimService {

  public ProcessResult executeCode(String code, Boolean stream, Long sessionId, String m3u8Path, ChannelContext channelContext) throws IOException, InterruptedException {
    new File("cache").mkdirs();
    long id = SnowflakeIdUtils.id();
    String subFolder = "cache" + File.separator + id;
    code = code.replace("#(output_path)", subFolder);

    String folder = "scripts" + File.separator + id;
    File fileFolder = new File(folder);
    if (!fileFolder.exists()) {
      fileFolder.mkdirs();
    }
    String scriptPath = folder + File.separator + "script.py";
    FileUtil.writeString(code, scriptPath, StandardCharsets.UTF_8.toString());
    // 执行脚本
    String videoFolder = subFolder + File.separator + "videos" + File.separator + "1080p30";
    log.info("videoFolder:{}", videoFolder);
    // 定义需要监控的文件夹,注意此处为绝对路径或根据实际情况调整
    String partVideoFolder = videoFolder + File.separator + "partial_movie_files" + File.separator + "CombinedScene";
    // 如果需要流式发送,则启动文件夹监控线程

    ProcessResult execute = null;
    if (stream) {
      File file = new File(partVideoFolder);
      file.mkdirs();
      log.info("watch:{}", file.getAbsolutePath());
      Thread watcherThread = new Thread(() -> watchFolder(partVideoFolder, channelContext));
      watcherThread.start();
      TioThreadUtils.execute(() -> {
        try {
          ProcessResult execute2 = execute(scriptPath);
          execute2.setTaskId(id);
          String filePath = videoFolder + File.separator + "CombinedScene.mp4";
          File combinedScenefile = new File(filePath);
          if (combinedScenefile.exists()) {
            execute2.setOutput(filePath.replace("\\", "/"));
          } else {
            log.info("file is not exists:{}", filePath);
          }
          // 可以等待一段时间,以确保监控期间捕获到文件创建事件
          Thread.sleep(2000);
          if (watcherThread != null && watcherThread.isAlive()) {
            watcherThread.interrupt();
          }
          String skipNullJson = JsonUtils.toSkipNullJson(execute2);
          Tio.send(channelContext, new SsePacket("result", skipNullJson));
          SseEmitter.closeSeeConnection(channelContext);

        } catch (IOException e) {
          e.printStackTrace();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      });

    } else {
      execute = execute(scriptPath);
      execute.setTaskId(id);
      String filePath = videoFolder + File.separator + "CombinedScene.mp4";
      File file = new File(filePath);
      if (file.exists()) {
        execute.setOutput(filePath.replace("\\", "/"));

        String subPath = "./data/hls/" + id + "/";
        String name = "main";

        String relPath = subPath + name + ".mp4";
        File relPathFile = new File(relPath);
        relPathFile.getParentFile().mkdirs();

        Files.copy(file.toPath(), relPathFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
        String hlsPath = subPath + name + ".m3u8";
        log.info("to hls:{}", hlsPath);
        NativeMedia.splitVideoToHLS(hlsPath, relPath, subPath + "/" + name + "_%03d.ts", 10);

        execute.setOutput(hlsPath.replace("./", "/"));
        if (sessionId != null) {
          log.info("merge into:{},{}", sessionId, m3u8Path);
          String appendVideoSegmentToHls = NativeMedia.appendVideoSegmentToHls(sessionId, filePath);
          log.info("merge result:{}", appendVideoSegmentToHls);
        } else {
          log.info("skip merge to hls");
        }

      } else {
        log.info("file is not exists:{}", filePath);
      }

    }
    return execute;
  }

  public static ProcessResult execute(String scriptPath) throws IOException, InterruptedException {
    String osName = System.getProperty("os.name").toLowerCase();
    log.info("osName: {} scriptPath: {}", osName, scriptPath);
    ProcessBuilder pb;
    if (osName.contains("windows") || osName.startsWith("mac")) {
      pb = new ProcessBuilder("python", scriptPath);
    } else {
      pb = new ProcessBuilder("python3", scriptPath);
    }
    pb.environment().put("PYTHONIOENCODING", "utf-8");

    // 获取脚本所在目录
    File scriptFile = new File(scriptPath);
    File scriptDir = scriptFile.getParentFile();
    if (scriptDir != null && !scriptDir.exists()) {
      scriptDir.mkdirs();
    }

    // 定义日志文件路径,存放在与 scriptPath 相同的目录
    File stdoutFile = new File(scriptDir, "stdout.log");
    File stderrFile = new File(scriptDir, "stderr.log");

    // 将输出和错误流重定向到对应的日志文件
    pb.redirectOutput(stdoutFile);
    pb.redirectError(stderrFile);

    Process process = pb.start();
    int exitCode = process.waitFor();

    // 读取日志文件内容,返回给客户端(如果需要实时返回,可用其他方案监控文件变化)
    String stdoutContent = new String(Files.readAllBytes(stdoutFile.toPath()), StandardCharsets.UTF_8);
    String stderrContent = new String(Files.readAllBytes(stderrFile.toPath()), StandardCharsets.UTF_8);

    ProcessResult result = new ProcessResult();
    result.setExitCode(exitCode);
    result.setStdOut(stdoutContent);
    result.setStdErr(stderrContent);

    return result;
  }

  /**
   * 监控指定目录中新建文件的变化,并发送给客户端
   */
  private void watchFolder(String folderPath, ChannelContext channelContext) {
    Path path = Paths.get(folderPath);
    try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
      path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
      while (!Thread.currentThread().isInterrupted()) {
        WatchKey key = null;
        try {
          key = watchService.poll(1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
          e.printStackTrace();
          return;
        }
        if (key == null) {
          continue;
        }
        for (WatchEvent<?> event : key.pollEvents()) {
          if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
            String newFileName = event.context().toString();
            String fullPath = folderPath + File.separator + newFileName;
            // 通过 Tio.send 发送新文件路径到客户端
            Tio.send(channelContext, new SsePacket("part", fullPath));
          }
        }
        key.reset();
      }
    } catch (IOException e) {
      log.error("Error watching folder {}", folderPath, e);
    }
  }
}


3. 总结

本文档详细描述了如何通过分场景提交代码执行和流式监控视频生成过程。

  • 运行流程:先通过 /manim/start 初始化场景,再依次提交场景代码,通过接口 /manim/index 实时返回部分生成的视频文件(利用 SSE),最终利用 /manim/finish 接口合并生成完整的视频。
  • 代码实现:文档中包含了 HTTP 请求处理(ManimHanlder)以及核心业务逻辑(ManimService)的完整 Java 源码。
  • 关键技术点:
    • 使用 Java WatchService 实现文件目录实时监控。
    • 利用 ProcessBuilder 执行 Python 脚本,并重定向脚本输出。
    • 通过 HLS 分段生成流媒体播放列表,实现流式播放。

通过对代码实现与运行流程的详细解释,可以帮助开发者快速理解并扩展基于此方案的视频分场景运行与流式播放系统。

Edit this page
Last Updated:
Contributors: Tong Li
Prev
manim 分场景生成代码
Next
分场景业务端完整实现流程