sync: VAP 当前进度(会话/暂停竞态与 codec 启动异常处理;统一构建版本 AGP 9.4.1 / Kotlin 2.4.20 / Gradle 9.7.1)
面向 Android 与 Desktop(JVM)的 VAP 透明视频 Compose 播放库,并带 Compose Desktop 制作工具。
English
VAP 是带真实 alpha 通道的短动画容器。本仓库用 Compose 的 加载 → 驱动 → 渲染 来播:解析 vapc 头、保持热解码会话,Android 走 Surface / Vulkan,Desktop 走 Canvas / Bitmap。
vapc
容器与编解码思路参考 Tencent VAP (Video Animation Player)。
Android Surface 路径要求 API 29+ 且设备支持 Vulkan 1.1。 Desktop 为 FFmpeg + Skiko。
vap-log
com.kaus-io:vap-log
vap-core
VapSource
VapConfig
vapp
com.kaus-io:vap-core
vap-decode-api
VapcParser
VapFrameDecoder
com.kaus-io:vap-decode-api
vap-decode-android
VapSurfaceFrameDecoder
com.kaus-io:vap-decode-android
vap-decode-jvm
com.kaus-io:vap-decode-jvm
vap-vk-android
libvap_vk.so
com.kaus-io:vap-vk-android
vap-encode
com.kaus-io:vap-encode
vap-compose
VapAnimation
VapRequest
VapAnimationState
com.kaus-io:vap-compose
vap-coil
com.kaus-io:vap-coil
example-vap-shared
example-vap-android
example-vap-desktop
app-vap-tool
播放只依赖一个工件;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>")
三层:加载 → 驱动(解码会话) → 渲染。
isPlaying
VapAnimation(composition, request)
生命周期: VapRequest.presentFirstFrameEagerly 默认 true,composition 一就绪 就打开解码器,把打包第 0 帧画到同一个 Surface / Canvas。暂停期间会话保持热状态, 只在 composition 对象变更或 state 离开组合树时释放。切换 isPlaying 不会冷启动 MediaCodec / Vulkan。
VapRequest.presentFirstFrameEagerly
true
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。
rememberVapComposition(spec)
animateVapCompositionAsState(isPlaying, iterations, fps, …)
来源:
VapCompositionSpec.File("/abs/clip.mp4") VapCompositionSpec.Asset("demo_cat.mp4") // Android assets/,不拷磁盘 VapCompositionSpec.Bytes(bytes) // 会拷贝;已持有的缓冲用 Bytes.wrap
Android 上默认走 AndroidEmbeddedExternalSurface: MediaCodec → AHardwareBuffer → Vulkan → 窗口。高帧率或大 alpha 素材走这条。
AndroidEmbeddedExternalSurface
Desktop 没有 Surface 宿主,始终 Canvas / Bitmap。
contentScale 默认 ContentScale.Fit(letterbox)。Crop 为覆盖;其余拉伸填满。
contentScale
ContentScale.Fit
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 抵消掉。
VapAnimationState.status
Idle
Preparing
Ready
Failed
loadVapComposition
loadVapCompositionAsync
rememberVapComposition(spec, onError)
spec
null
VapRequest.Builder
Listener
animateVapCompositionAsState(composition, request)
VapAnimation(state)
VapPlayerStatus
VapConstants.IterateForever
iterations
Int.MAX_VALUE
VapRequest.Listener:onStart / onSuccess / onError / onCancel / onCompleted(全部迭代自然结束)/ onPresented(逐帧,须无分配)。 进程级默认可在 Application.onCreate 设一次 VapRequest.processDefaults。
VapRequest.Listener
onStart
onSuccess
onError
onCancel
onCompleted
onPresented
Application.onCreate
VapRequest.processDefaults
加载时预热打包第 0 帧静图,让 Surface / Coil 在首个 MediaCodec 帧之前有画面。
MediaMetadataRetriever
编码后再嵌入海报:
./gradlew :app-vap-tool:run --args="poster <clip.mp4> --in-place"
Compose Desktop。无参是 GUI,带参是 CLI。编码器与 vap-encode 同一份。 本机需要 ffmpeg(PATH、--ffmpeg 或表单)。
ffmpeg
PATH
--ffmpeg
目录里 PNG,文件名必须是从 0 起的连续十进制序号:
0
frames/ 0.png 1.png 299.png
遇到首个缺号即停。带非数字前缀(frame_001.png)会被忽略。打包后最长边超过 1504 时,部分 MediaCodec 设备可能绿屏。
frame_001.png
./gradlew :app-vap-tool:run
设置 fps、alpha 蒙版缩放、编码(H.264 / H.265)、质量(bitrate / vbr / crf)、 alpha(on / off / auto)、ffmpeg 路径。点 Create VAP 后弹出循环预览。
bitrate
vbr
crf
on
off
auto
./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:逐帧扫描,只有真有透明像素才写蒙版。
--alpha auto
<输出目录>/(默认 <输入目录>/output/):
<输出目录>/
<输入目录>/output/
video.mp4
vapc.json
编码成功后中间产物 <输出目录>/frames/ PNG 序列会被删除;用 --keep-frames 可保留以便检查。
<输出目录>/frames/
--keep-frames
vap-tool poster 再追加 vapp box,不改写 stco / co64。
vap-tool poster
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 来源:
example-vap-android/src/main/assets/
demo_cat.mp4
demo_home.mp4
demo_about.mp4
demo_idle.mp4
demo_think.mp4
demo_speech.mp4
demo_touch_wake.mp4
详见 example-vap-android/src/main/assets/README.md。 PAG 基准还需要同名 .pag(同样未内置)。
example-vap-android/src/main/assets/README.md
.pag
快照,不是产品承诺。约 2026-07,高通 Bengal(arm64)车机,libpag PAGView vs VAP Vulkan Surface(VapAnimation,内容 30fps / UI 60fps)。预热 1s,播放 10s, 只看最后 5s。CPU 为 Process.getElapsedCpuTime() / 墙钟(多核可超 100%)。 Δ 为 VAP − PAG。
PAGView
Process.getElapsedCpuTime()
此后合成器又改过(批次 present、去掉 AHB 导入缓存、opening / vapp 海报),数字只作方向参考。
稳态锁在约 30 / 60。真正拉开的是大画幅 CPU和略低的 PSS。小图标换引擎换不来 CPU。870 dp 上 UI 掉帧略多(相对 60Hz:1.3 vs 0.3),内容帧率相同。
MIT License。
版权所有:中国计算机学会技术支持:开源发展技术委员会 京ICP备13000930号-9 京公网安备 11010802047560号
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。
模块
vap-logcom.kaus-io:vap-logvap-coreVapSource/VapConfig/ MP4 box(vapc、vapp)com.kaus-io:vap-corevap-decode-apiVapcParser、VapFrameDecodercom.kaus-io:vap-decode-apivap-decode-androidVapSurfaceFrameDecodercom.kaus-io:vap-decode-androidvap-decode-jvmcom.kaus-io:vap-decode-jvmvap-vk-androidlibvap_vk.so,C++)com.kaus-io:vap-vk-androidvap-encodecom.kaus-io:vap-encodevap-composeVapAnimation/VapRequest/VapAnimationStatecom.kaus-io:vap-composevap-coilcom.kaus-io:vap-coilexample-vap-sharedexample-vap-androidexample-vap-desktopapp-vap-tool播放只依赖一个工件;
vap-compose会带上对应平台解码器:从 PNG 制作:
Android 上用 Coil 抽静图:
Compose 播放
三层:加载 → 驱动(解码会话) → 渲染。
VapAnimationState挂在不会随页面 dispose 的祖先上,只切isPlaying。VapAnimation(composition, request)。dispose 会拆掉热解码器。1. 推荐:提升 state
异步加载用
rememberVapComposition(spec)(IO 线程,可取消)。animateVapCompositionAsState(isPlaying, iterations, fps, …)的散落参数重载仍在, 新代码优先VapRequest。来源:
2. Surface 与 Canvas
Android 上默认走
AndroidEmbeddedExternalSurface: MediaCodec → AHardwareBuffer → Vulkan → 窗口。高帧率或大 alpha 素材走这条。Desktop 没有 Surface 宿主,始终 Canvas / Bitmap。
contentScale默认ContentScale.Fit(letterbox)。Crop为覆盖;其余拉伸填满。封装重载
state 绑在当前 Composable。不要用在需要切页保活解码器的场景。
叠加层读
VapAnimationState.status(Idle/Preparing/Ready/Failed)。 请始终挂着VapAnimation——等到Ready再换播放器会拆掉 Surface,把第 0 帧 eager present 抵消掉。API 速查
loadVapComposition/loadVapCompositionAsyncvapc、包装VapSource、预热打包第 0 帧海报(vapp或 retriever)rememberVapComposition(spec, onError)spec变化时后台重载,不闪nullVapRequest/VapRequest.BuilderListeneranimateVapCompositionAsState(composition, request)VapAnimation(state)/VapAnimation(composition, request)VapPlayerStatusIdle→Preparing→Ready/FailedVapConstants.IterateForeveriterations用的Int.MAX_VALUEVapRequest.Listener:onStart/onSuccess/onError/onCancel/onCompleted(全部迭代自然结束)/onPresented(逐帧,须无分配)。 进程级默认可在Application.onCreate设一次VapRequest.processDefaults。海报(
vapp)加载时预热打包第 0 帧静图,让 Surface / Coil 在首个 MediaCodec 帧之前有画面。
vappbox(打包第 0 帧 PNG)则直接用。MediaMetadataRetriever。编码后再嵌入海报:
制作工具(app-vap-tool)
Compose Desktop。无参是 GUI,带参是 CLI。编码器与
vap-encode同一份。 本机需要ffmpeg(PATH、--ffmpeg或表单)。输入
目录里 PNG,文件名必须是从
0起的连续十进制序号:遇到首个缺号即停。带非数字前缀(
frame_001.png)会被忽略。打包后最长边超过 1504 时,部分 MediaCodec 设备可能绿屏。GUI
设置 fps、alpha 蒙版缩放、编码(H.264 / H.265)、质量(
bitrate/vbr/crf)、 alpha(on/off/auto)、ffmpeg 路径。点 Create VAP 后弹出循环预览。CLI
拿不准用
--alpha auto:逐帧扫描,只有真有透明像素才写蒙版。输出
<输出目录>/(默认<输入目录>/output/):video.mp4vapc的 H.264 / H.265,这才是可播的 VAPvapc.json编码成功后中间产物
<输出目录>/frames/PNG 序列会被删除;用--keep-frames可保留以便检查。vap-tool poster再追加vappbox,不改写stco/co64。构建 / 运行
演示素材
Android / Desktop 演示会在
example-vap-android/src/main/assets/下找下列文件名。 二进制不在 git 里(体积大);请自行放入许可兼容的文件,或用app-vap-tool编码。 缺文件时选择器为空。拟定 Wikimedia 来源:demo_cat.mp4demo_home.mp4demo_about.mp4demo_idle.mp4demo_think.mp4demo_speech.mp4demo_touch_wake.mp4详见
example-vap-android/src/main/assets/README.md。 PAG 基准还需要同名.pag(同样未内置)。Android 性能对比(相对 libpag)
快照,不是产品承诺。约 2026-07,高通 Bengal(arm64)车机,libpag
PAGViewvs VAP Vulkan Surface(VapAnimation,内容 30fps / UI 60fps)。预热 1s,播放 10s, 只看最后 5s。CPU 为Process.getElapsedCpuTime()/ 墙钟(多核可超 100%)。 Δ 为 VAP − PAG。此后合成器又改过(批次 present、去掉 AHB 导入缓存、opening /
vapp海报),数字只作方向参考。稳态锁在约 30 / 60。真正拉开的是大画幅 CPU和略低的 PSS。小图标换引擎换不来 CPU。870 dp 上 UI 掉帧略多(相对 60Hz:1.3 vs 0.3),内容帧率相同。
开源协议
MIT License。