目录

VAP (Kotlin Multiplatform) — 中文

面向 Android 与 Desktop(JVM)的 VAP 透明视频 Compose 播放库,并带 Compose Desktop 制作工具。

English

简介

VAP 是带真实 alpha 通道的短动画容器。本仓库用 Compose 的 加载 → 驱动 → 渲染 来播:解析 vapc 头、保持热解码会话,Android 走 Surface / Vulkan,Desktop 走 Canvas / Bitmap。

容器与编解码思路参考 Tencent VAP (Video Animation Player)。

Android Surface 路径要求 API 29+ 且设备支持 Vulkan 1.1。 Desktop 为 FFmpeg + Skiko。

模块

模块 作用 Maven
vap-log 进程级日志(级别 / 限频 / sink) com.kaus-io:vap-log
vap-core VapSource / VapConfig / MP4 box(vapc、vapp) com.kaus-io:vap-core
vap-decode-api VapcParser、VapFrameDecoder com.kaus-io:vap-decode-api
vap-decode-android MediaCodec + Vulkan;对外类型 VapSurfaceFrameDecoder com.kaus-io:vap-decode-android
vap-decode-jvm Desktop 解码(FFmpeg + Skiko) com.kaus-io:vap-decode-jvm
vap-vk-android Native AHB 合成(libvap_vk.so,C++) com.kaus-io:vap-vk-android
vap-encode PNG 序列 → VAP com.kaus-io:vap-encode
vap-compose VapAnimation / VapRequest / VapAnimationState com.kaus-io:vap-compose
vap-coil Coil 3 静图海报解码 com.kaus-io:vap-coil
example-vap-shared 演示 UI + 基准 —
example-vap-android Android 宿主 —
example-vap-desktop Desktop 宿主 —
app-vap-tool GUI + CLI 制作 —

播放只依赖一个工件;vap-compose 会带上对应平台解码器:

implementation("com.kaus-io:vap-compose:<version>")

从 PNG 制作:

implementation("com.kaus-io:vap-encode:<version>")

Android 上用 Coil 抽静图:

implementation("com.kaus-io:vap-coil:<version>")

Compose 播放

三层:加载 → 驱动(解码会话) → 渲染。

写法 适用
提升 state Feed / Pager / 浮窗:把 VapAnimationState 挂在不会随页面 dispose 的祖先上,只切 isPlaying。
封装重载 一次性页面:VapAnimation(composition, request)。dispose 会拆掉热解码器。

生命周期: VapRequest.presentFirstFrameEagerly 默认 true,composition 一就绪 就打开解码器,把打包第 0 帧画到同一个 Surface / Canvas。暂停期间会话保持热状态, 只在 composition 对象变更或 state 离开组合树时释放。切换 isPlaying 不会冷启动 MediaCodec / Vulkan。

1. 推荐:提升 state

val composition = remember(path) {
    loadVapComposition(VapCompositionSpec.File(path))
}

val anim = animateVapCompositionAsState(
    composition = composition,
    request = VapRequest.Builder()
        .playing(pageVisible)
        .iterations(VapConstants.IterateForever)
        .build(),
)

VapAnimation(anim, modifier = Modifier.fillMaxWidth().height(320.dp))

异步加载用 rememberVapComposition(spec)(IO 线程,可取消)。 animateVapCompositionAsState(isPlaying, iterations, fps, …) 的散落参数重载仍在, 新代码优先 VapRequest。

来源:

VapCompositionSpec.File("/abs/clip.mp4")
VapCompositionSpec.Asset("demo_cat.mp4")   // Android assets/,不拷磁盘
VapCompositionSpec.Bytes(bytes)            // 会拷贝;已持有的缓冲用 Bytes.wrap

2. Surface 与 Canvas

Android 上默认走 AndroidEmbeddedExternalSurface: MediaCodec → AHardwareBuffer → Vulkan → 窗口。高帧率或大 alpha 素材走这条。

Desktop 没有 Surface 宿主,始终 Canvas / Bitmap。

contentScale 默认 ContentScale.Fit(letterbox)。Crop 为覆盖;其余拉伸填满。

封装重载

state 绑在当前 Composable。不要用在需要切页保活解码器的场景。

val composition by rememberVapComposition(VapCompositionSpec.File(path))
VapAnimation(
    composition = composition,
    request = VapRequest.Builder()
        .iterations(VapConstants.IterateForever)
        .fps(30)
        .build(),
    modifier = Modifier.fillMaxWidth().height(320.dp),
)

叠加层读 VapAnimationState.status(Idle / Preparing / Ready / Failed)。 请始终挂着 VapAnimation——等到 Ready 再换播放器会拆掉 Surface,把第 0 帧 eager present 抵消掉。

API 速查

API 作用
loadVapComposition / loadVapCompositionAsync 解析 vapc、包装 VapSource、预热打包第 0 帧海报(vapp 或 retriever)
rememberVapComposition(spec, onError) 跨重组记忆;spec 变化时后台重载,不闪 null
VapRequest / VapRequest.Builder 播放门、次数、fps、GPU 后端、eager 第 0 帧、Coil 风格 Listener
animateVapCompositionAsState(composition, request) 持有热解码器;保活的关键
VapAnimation(state) / VapAnimation(composition, request) 渲染(Android Surface / Desktop Canvas)
VapPlayerStatus Idle → Preparing → Ready / Failed
VapConstants.IterateForever iterations 用的 Int.MAX_VALUE

VapRequest.Listener:onStart / onSuccess / onError / onCancel / onCompleted(全部迭代自然结束)/ onPresented(逐帧,须无分配)。 进程级默认可在 Application.onCreate 设一次 VapRequest.processDefaults。

海报(vapp)

加载时预热打包第 0 帧静图,让 Surface / Coil 在首个 MediaCodec 帧之前有画面。

  1. MP4 尾部已有 vapp box(打包第 0 帧 PNG)则直接用。
  2. 否则 Android 回退 MediaMetadataRetriever。

编码后再嵌入海报:

./gradlew :app-vap-tool:run --args="poster <clip.mp4> --in-place"

制作工具(app-vap-tool)

Compose Desktop。无参是 GUI,带参是 CLI。编码器与 vap-encode 同一份。 本机需要 ffmpeg(PATH、--ffmpeg 或表单)。

输入

目录里 PNG,文件名必须是从 0 起的连续十进制序号:

frames/
  0.png
  1.png
  299.png

遇到首个缺号即停。带非数字前缀(frame_001.png)会被忽略。打包后最长边超过 1504 时,部分 MediaCodec 设备可能绿屏。

GUI

./gradlew :app-vap-tool:run

设置 fps、alpha 蒙版缩放、编码(H.264 / H.265)、质量(bitrate / vbr / crf)、 alpha(on / off / auto)、ffmpeg 路径。点 Create VAP 后弹出循环预览。

CLI

./gradlew :app-vap-tool:run --args="<输入目录> --alpha on --fps 30"
./gradlew :app-vap-tool:run --args="poster <input.mp4> --in-place"
Usage: vap-tool <input-dir> [options]
       vap-tool poster <input.mp4> [options]

Options:
  -o, --output <dir>      输出目录(默认 <input-dir>/output)
      --fps <int>         帧率(默认 60)
      --scale <float>     alpha 蒙版缩放,0.5-1.0(默认 0.5)
      --codec <h264|h265> 视频编解码器(默认 h265)
      --quality <mode>    bitrate | vbr | crf(默认 bitrate)
      --bitrate <kbps>    bitrate 模式目标码率(默认 3000)
      --vbr-target <kbps> vbr 模式目标码率(默认 3000)
      --vbr-max <kbps>    vbr 模式最大码率(默认 4500)
      --crf <0-51>        crf 模式 CRF(默认 29)
      --ffmpeg <path>     ffmpeg 可执行文件(默认 ffmpeg)
      --alpha <mode>      on | off | auto(必填);auto 会扫描所有帧
      --vapc-version <int>  vapc box 版本(默认 3)
      --keep-frames       保留中间产物 <output>/frames PNG 序列
      --no-progress       关闭逐帧进度
  -h, --help              帮助

poster:
  -o, --output <file>     输出文件(默认 <input-name>.poster.mp4)
      --in-place          覆盖输入文件

退出码:0 成功,1 编码/海报失败,2 参数错误。

拿不准用 --alpha auto:逐帧扫描,只有真有透明像素才写蒙版。

输出

<输出目录>/(默认 <输入目录>/output/):

文件 作用
video.mp4 已插入 vapc 的 H.264 / H.265,这才是可播的 VAP
vapc.json 同一布局的旁路 JSON(调试用)

编码成功后中间产物 <输出目录>/frames/ PNG 序列会被删除;用 --keep-frames 可保留以便检查。

vap-tool poster 再追加 vapp box,不改写 stco / co64。

构建 / 运行

./gradlew :example-vap-android:assembleDebug
./gradlew :example-vap-desktop:run
./gradlew :app-vap-tool:run
./gradlew :app-vap-tool:run --args="<输入目录> --alpha on"
./gradlew printPublishableModules

演示素材

Android / Desktop 演示会在 example-vap-android/src/main/assets/ 下找下列文件名。 二进制不在 git 里(体积大);请自行放入许可兼容的文件,或用 app-vap-tool 编码。 缺文件时选择器为空。拟定 Wikimedia 来源:

期望文件 透明度 拟定源 许可
demo_cat.mp4 透明 Transparent Cartoon Running Cat CC BY-SA 2.0
demo_home.mp4 不透明 A Boy and His Dragon CC BY-SA 2.0
demo_about.mp4 不透明 A Frolic With Felix (1920) excerpt 公有领域
demo_idle.mp4 不透明 Clay Day CC BY 3.0
demo_think.mp4 不透明 Bounce-ball CC BY-SA 4.0
demo_speech.mp4 不透明 BouncingBallAnimation CC BY-SA 4.0
demo_touch_wake.mp4 不透明 CarAnimation1 CC BY-SA 4.0

详见 example-vap-android/src/main/assets/README.md。 PAG 基准还需要同名 .pag(同样未内置)。

Android 性能对比(相对 libpag)

快照,不是产品承诺。约 2026-07,高通 Bengal(arm64)车机,libpag PAGView vs VAP Vulkan Surface(VapAnimation,内容 30fps / UI 60fps)。预热 1s,播放 10s, 只看最后 5s。CPU 为 Process.getElapsedCpuTime() / 墙钟(多核可超 100%)。 Δ 为 VAP − PAG。

此后合成器又改过(批次 present、去掉 AHB 导入缓存、opening / vapp 海报),数字只作方向参考。

布局 内容 fps CPU Δ PSS Δ
870 dp 矩形 30.1 vs 30.1 **−37.5%**(283% → 177%) −3.2%
全屏铺满 30.1 vs 29.9 −1.8%(噪声) −7.5%
~259 dp 圆形裁剪 30.1 vs 29.9 −2.0%(噪声) −3.6%

稳态锁在约 30 / 60。真正拉开的是大画幅 CPU和略低的 PSS。小图标换引擎换不来 CPU。870 dp 上 UI 掉帧略多(相对 60Hz:1.3 vs 0.3),内容帧率相同。

开源协议

MIT License。

关于
12.4 MB
邀请码
    Gitlink(确实开源)
  • 加入我们
  • 官网邮箱:gitlink@ccf.org.cn
  • QQ群
  • QQ群
  • 公众号
  • 公众号

版权所有:中国计算机学会技术支持:开源发展技术委员会
京ICP备13000930号-9 京公网安备 11010802047560号