react-native-alpha-video-gpu 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +949 -2
- package/THIRD_PARTY_NOTICES.md +14 -0
- package/android/src/main/AndroidManifest.xml +9 -0
- package/android/src/main/cpp/CMakeLists.txt +41 -0
- package/android/src/main/cpp/alphavideogpu_jni.cpp +1168 -0
- package/android/src/main/cpp/astc_decomp.cpp +1550 -0
- package/android/src/main/cpp/astc_decomp.h +41 -0
- package/android/src/main/cpp/source_jni.cpp +242 -0
- package/android/src/main/cpp/source_jni.h +13 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuNative.java +43 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuReader.java +209 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuSource.java +328 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuView.java +2126 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/Fetcher.java +37 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/HttpFetcher.java +156 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/Ktx2Metadata.java +78 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/RangeSourceLink.java +54 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/ZstdSeekTable.java +120 -0
- package/android/src/rangesource/java/dev/mediacache/alphavideo/gpu/AppDataSource.java +50 -0
- package/ios/Sources/AlphaVideoGPU/AlphaVideoGpuSource.h +63 -0
- package/ios/Sources/AlphaVideoGPU/AlphaVideoGpuSource.mm +522 -0
- package/ios/Sources/AlphaVideoGPU/AlphaVideoGpuView.mm +2722 -0
- package/ios/Sources/AlphaVideoGPU/astc_decomp.cpp +1550 -0
- package/ios/Sources/AlphaVideoGPU/astc_decomp.h +41 -0
- package/ios/Sources/AlphaVideoGPU/include/AlphaVideoGPU.h +110 -0
- package/ios/Sources/AlphaVideoGPU/zstd_seek.h +250 -0
- package/package.json +77 -4
- package/react-native/android/build.gradle +92 -0
- package/react-native/android/src/main/AndroidManifest.xml +9 -0
- package/react-native/android/src/main/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuPackage.java +26 -0
- package/react-native/android/src/main/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuViewManager.java +187 -0
- package/react-native/android/src/main/java/dev/mediacache/alphavideo/gpu/reactnative/PlayerEventForwarder.java +107 -0
- package/react-native/android/src/newarch/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuViewManagerSpec.java +23 -0
- package/react-native/android/src/norangesource/java/dev/mediacache/alphavideo/gpu/AppDataSource.java +15 -0
- package/react-native/android/src/oldarch/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuViewManagerSpec.java +56 -0
- package/react-native/ios/RNAlphaVideoGpuViewComponentView.mm +176 -0
- package/react-native/ios/RNAlphaVideoGpuViewManager.mm +175 -0
- package/react-native/src/AlphaVideoGpuView.js +2 -0
- package/react-native/src/AlphaVideoGpuView.web.tsx +264 -0
- package/react-native/src/AlphaVideoGpuViewNativeComponent.ts +128 -0
- package/react-native/src/AlphaVideoGpuViewNativeComponent.web.ts +2 -0
- package/react-native/src/index.d.ts +95 -0
- package/react-native/src/index.js +162 -0
- package/react-native-alpha-video-gpu.podspec +37 -0
- package/react-native.config.js +12 -0
- package/third_party/licenses/Apache-2.0.txt +208 -0
- package/third_party/range-source/LICENSE +21 -0
- package/third_party/range-source/README.md +10 -0
- package/third_party/range-source/include/range_source.h +395 -0
- package/third_party/range-source/include/range_source_download.h +309 -0
- package/third_party/zstd/LICENSE +30 -0
- package/third_party/zstd/zstd.c +38717 -0
- package/third_party/zstd/zstd.h +2450 -0
package/README.md
CHANGED
|
@@ -1,3 +1,950 @@
|
|
|
1
|
-
#
|
|
1
|
+
# alpha-video-gpu
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="docs/alpha-video-gpu.avif" alt="Transparent ASTC videos playing over a checkerboard in the demo app on Android" width="360">
|
|
5
|
+
<br>
|
|
6
|
+
<a href="https://github.com/MediaCacheDev/alpha-video-test">Compare transparent formats in the demo app</a>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
Plays transparent (alpha) videos stored as ASTC texture arrays in KTX2 files, drawn with Metal on iOS, OpenGL ES on Android and WebGL on the web. Frames go to the GPU without being decoded, so CPU and battery use stay low. It comes as a React Native component, as standalone Android and iOS libraries, and as a web package.
|
|
10
|
+
|
|
11
|
+
| Platform | Package | View |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| React Native (Android, iOS, web) | npm `react-native-alpha-video-gpu` | `AlphaVideoGpuView` |
|
|
14
|
+
| Android | Maven `dev.mediacache:alpha-video-gpu` | `dev.mediacache.alphavideo.gpu.AlphaVideoGpuView` |
|
|
15
|
+
| iOS | Swift Package Manager, product `AlphaVideoGPU`; CocoaPods `alpha-video-gpu` | `AlphaVideoGpuView` |
|
|
16
|
+
| Web (plain JS/TS, React) | npm `alpha-video-gpu-web` | `createAlphaVideoGpuPlayer`, `AlphaVideoGpuPlayer` |
|
|
17
|
+
|
|
18
|
+
See it next to the other formats in the [demo app](#demo-and-related-packages).
|
|
19
|
+
|
|
20
|
+
## alpha-video-gpu or alpha-video?
|
|
21
|
+
|
|
22
|
+
alpha-video-gpu doesn't depend on a video codec. The GPU samples the frames as they are stored, so playback takes little CPU, needs no hardware video decoder, and runs wherever the GPU supports ASTC textures, which covers more devices than VP9 or AV1 hardware decoding. [alpha-video](https://github.com/MediaCacheDev/alpha-video) plays WebM VP9 and AV1 instead, for when file size matters more.
|
|
23
|
+
|
|
24
|
+
| | [alpha-video](https://github.com/MediaCacheDev/alpha-video) | alpha-video-gpu (this package) |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| **Format** | WebM: VP9 with alpha, or dual-stream VP9/AV1 | KTX2: an ASTC texture array, one layer per frame |
|
|
27
|
+
| **Made with** | ffmpeg, or any tool that exports alpha WebM | [alpha-video-gpu-encoder](https://github.com/MediaCacheDev/alpha-video-gpu-encoder) (`texenc`) |
|
|
28
|
+
| **Decoding** | Every frame: libvpx or dav1d on the CPU, or MediaCodec on Android | None |
|
|
29
|
+
| **CPU and battery** | A decode per frame, or one pass up front with `mode="preload"` | Low: zstd at most, and the GPU does the rest |
|
|
30
|
+
| **What the device needs** | Nothing for software decoding. Hardware decoding needs a VP9 or AV1 decoder (Android only), and each player takes 2 of its few instances | ASTC texture support: every iPhone and iPad since the A8 (2014), nearly every Android GPU, Apple silicon Macs and mobile browsers |
|
|
31
|
+
| **Many players at once** | Limited by the CPU, or by the hardware decoder's instances: 20 small loops saturated a budget phone | Dozens: 50 small uncompressed loops used under half of the same phone's CPU |
|
|
32
|
+
| **File size** | Smallest | About 4 to 6 times larger at similar quality |
|
|
33
|
+
| **Seeking** | Decodes from the previous keyframe | Any frame directly |
|
|
34
|
+
| **Web** | `<video>` in Chrome and Firefox | WebGL where the browser has ASTC |
|
|
35
|
+
| **Best for** | Long or large clips, downloads where size matters, files from standard video tools | UI effects, stickers and glows, many simultaneous loops, short to medium videos |
|
|
36
|
+
|
|
37
|
+
[Video Codecs vs GPU Texture Streaming](#video-codecs-vs-gpu-texture-streaming) below goes through the trade-offs in detail. [alpha-video-ffmpegkit](https://github.com/MediaCacheDev/alpha-video-ffmpegkit) was the first attempt at transparent WebM, on FFmpegKit, and stays as a reference; use alpha-video for WebM.
|
|
38
|
+
|
|
39
|
+
## Overview
|
|
40
|
+
|
|
41
|
+
This library renders videos from KTX2 files that hold an ASTC texture array, one frame per layer. The ASTC blocks go to the GPU as they are, and the GPU decodes them while sampling.
|
|
42
|
+
|
|
43
|
+
**Key characteristics:**
|
|
44
|
+
- No per-frame decoding: frames are uploaded as compressed ASTC blocks
|
|
45
|
+
- Hardware texture compression on iOS, Android and the mobile web
|
|
46
|
+
- Software ASTC decoding on emulators and devices without ASTC
|
|
47
|
+
- Frame size, count and rate from standard KTX2 fields (see [Metadata](#metadata))
|
|
48
|
+
- **Streaming playback** for long videos with minimal memory footprint
|
|
49
|
+
- **Uncompressed ASTC streaming** for many concurrent players
|
|
50
|
+
|
|
51
|
+
### Drawing size
|
|
52
|
+
|
|
53
|
+
Each frame is stored at one size, without mipmaps (the smaller, pre-filtered copies a GPU uses when it shrinks a texture), and the player samples it with bilinear filtering. At 100%, one texel per pixel on screen, or enlarged, frames look as clean as the source. Shrunk to less than half their size, each pixel on screen comes from the four texels nearest it and the rest are skipped, so edges turn jagged and fine detail sparkles as the clip moves, although the frames themselves are clean. The same clip played as video and scaled down the same way can look smoother.
|
|
54
|
+
|
|
55
|
+
So draw ASTC at 100% where you can: encode each clip at the size it appears on screen, in physical pixels (a 150 pt view on a 3x screen is 450 px). Down to half size still looks fine.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Video Codecs vs GPU Texture Streaming
|
|
60
|
+
|
|
61
|
+
ASTC makes a different trade-off from video codecs. VP9 and AV1 spend CPU on every frame to make files as small as possible. ASTC is a format the GPU samples directly, so playback takes as little CPU and memory traffic as possible and leaves the CPU to the rest of the app, even with many animations on screen. The cost is file size.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
VP9 / AV1: file → decode on the CPU → YUV → convert to RGBA → upload → draw
|
|
65
|
+
ASTC: file → zstd (or nothing) → upload as is → draw (the GPU decodes while sampling)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The measurements below use two clips: a glow (400×400, 181 frames, 30 fps, mostly transparent) and a manta ray (1920×1080, 424 frames, 29.97 fps, mostly opaque footage). Each was encoded from the same frames into every format.
|
|
69
|
+
|
|
70
|
+
### File size
|
|
71
|
+
|
|
72
|
+
| Clip | VP9 (CRF 18) | AV1 (CRF 28) | ASTC 4x4 | ASTC 6x6 | ASTC 8x8 |
|
|
73
|
+
|------|-----:|-----:|-----:|-----:|-----:|
|
|
74
|
+
| Glow | 1.0 MB | 0.60 MB | 5.5 MB | 2.5 MB | 1.4 MB |
|
|
75
|
+
| Manta | 7.0 MB | 6.4 MB | 95 MB | 63 MB | 38 MB |
|
|
76
|
+
|
|
77
|
+
At similar quality (PSNR), ASTC came out about 4 times the size of VP9 for the glow and 5.5 times for the manta. The encoder's README has the full tables, with quality at each block size and video at two quality settings.
|
|
78
|
+
|
|
79
|
+
### On a budget Android phone
|
|
80
|
+
|
|
81
|
+
Measured on a Samsung Galaxy A52s (Snapdragon 778G, 6 GB, Android 14), an inexpensive phone from 2021, in Release builds of the [demo app](#demo-and-related-packages). CPU is the whole phone's, as cores kept busy, so it includes Android's media processes and compositing; the phone has 8 cores. The phone has a VP9 hardware decoder but no AV1 one.
|
|
82
|
+
|
|
83
|
+
**One player** (each clip looping on its own, CPU above an empty screen):
|
|
84
|
+
|
|
85
|
+
| Clip | ASTC 4x4 | ASTC 8x8 | VP9, libvpx | VP9, MediaCodec | AV1, dav1d |
|
|
86
|
+
|------|-----:|-----:|-----:|-----:|-----:|
|
|
87
|
+
| Glow | 0.7 cores | 0.5 cores | 0.7 cores | 1.3 cores | 0.7 cores |
|
|
88
|
+
| Manta | 0.8 cores | 0.7 cores | 3.7 cores | 3.9 cores | 3.1 cores |
|
|
89
|
+
|
|
90
|
+
One small clip costs about the same in any format: most of that is Android redrawing the view each frame. At 1080p, video kept 3 to 4 cores busy and ASTC under one. MediaCodec decodes VP9's colour in hardware, but the alpha plane is still decoded and combined on the CPU.
|
|
91
|
+
|
|
92
|
+
**Many players** (glow loops in the demo's main screen, each in its own view):
|
|
93
|
+
|
|
94
|
+
| Players | Format | CPU | App memory | Demo UI |
|
|
95
|
+
|---------|--------|----:|-----------:|--------:|
|
|
96
|
+
| none | | 0.8 cores | 136 MB | 113 fps |
|
|
97
|
+
| 8 | ASTC, zstd | 2.1 cores | 452 MB | 60 fps |
|
|
98
|
+
| 8 | VP9, libvpx | 3.4 cores | 298 MB | 54 fps |
|
|
99
|
+
| 20 | ASTC, uncompressed | 2.4 cores | 371 MB | 52 fps |
|
|
100
|
+
| 20 | ASTC, zstd | 3.1 cores | 908 MB | 52 fps |
|
|
101
|
+
| 20 | VP9, libvpx | 8 cores (all) | 462 MB | 15 fps |
|
|
102
|
+
| 20 | VP9, decoder `auto` | 8 cores (all) | 428 MB | 3 fps |
|
|
103
|
+
| 50 | ASTC, uncompressed | 3.7 cores | 699 MB | 41 fps |
|
|
104
|
+
| 50 | ASTC, zstd | 8 cores (all) | 2.1 GB | 3 fps |
|
|
105
|
+
|
|
106
|
+
"Demo UI" is the frame rate of the demo's own interface on the phone's 120 Hz screen, which shows how much the players leave for the rest of an app. The VP9 here is the demo's glow file (5.7 MB, high quality), so it takes more decoding than the CRF 18 file above. With 20 or more players, use uncompressed files (`--zstd 0`): each zstd player decompresses on its own, and took about 40 MB of memory here, against about 12 MB uncompressed.
|
|
107
|
+
|
|
108
|
+
### When to use each
|
|
109
|
+
|
|
110
|
+
**Use video codecs ([alpha-video](https://github.com/MediaCacheDev/alpha-video)) when:**
|
|
111
|
+
- Download or app size comes first
|
|
112
|
+
- The content is long or high-resolution footage, especially opaque
|
|
113
|
+
- Only one or two videos play at a time
|
|
114
|
+
|
|
115
|
+
**Use ASTC (alpha-video-gpu) when:**
|
|
116
|
+
- Many animations play at once (UI effects, stickers, glows)
|
|
117
|
+
- The CPU should stay free for the app, or battery use matters
|
|
118
|
+
- Seeking and scrubbing need to be frame-exact
|
|
119
|
+
- Behaviour should be the same on every platform
|
|
120
|
+
|
|
121
|
+
### Streaming Modes
|
|
122
|
+
|
|
123
|
+
Both approaches support streaming for long content:
|
|
124
|
+
|
|
125
|
+
| Mode | Video Codec | GPU Texture |
|
|
126
|
+
|------|-------------|-------------|
|
|
127
|
+
| **Buffering** | Decode ahead into frame buffer | Decompress ahead into ring buffer |
|
|
128
|
+
| **Memory** | Decoded RGBA frames in RAM | Compressed ASTC in ring buffer |
|
|
129
|
+
| **Seeking** | Keyframe-based | Frame-accurate (every frame is a keyframe) |
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Encoding
|
|
134
|
+
|
|
135
|
+
Files are made with the separate [`alpha-video-gpu-encoder`](https://github.com/MediaCacheDev/alpha-video-gpu-encoder) package (CLI `texenc`), which compresses frames to ASTC with `toktx` from KTX-Software (or `astcenc` for the stream encoder).
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm install -g alpha-video-gpu-encoder
|
|
139
|
+
|
|
140
|
+
# ASTC texture array, KTX2-internal zstd (the default; streams with `stream`)
|
|
141
|
+
texenc --input ./frames --output video.ktx2 --fps 30 --astc-quality exhaustive --astc-perceptual
|
|
142
|
+
|
|
143
|
+
# Uncompressed ASTC (largest file, no decompression; mmap streaming)
|
|
144
|
+
texenc --input ./frames --output video.ktx2 --fps 30 --zstd 0
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
See the encoder's README for every option, video input and the memory-efficient stream encoder.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Emulator / Fallback Behavior
|
|
152
|
+
|
|
153
|
+
On devices and browsers without hardware ASTC support (iOS Simulator, Android Emulator, browsers without `WEBGL_compressed_texture_astc`), the player automatically decodes ASTC textures to RGBA in software, with the same decoder everywhere (compiled to WebAssembly on the web; see [Web Texture Format Support](#web-texture-format-support)).
|
|
154
|
+
|
|
155
|
+
| Platform | Hardware Support | Fallback |
|
|
156
|
+
|----------|------------------|----------|
|
|
157
|
+
| iOS Device | ASTC (Metal) | — |
|
|
158
|
+
| iOS Simulator | None | Software ASTC→RGBA decode |
|
|
159
|
+
| Android Device | ASTC (nearly all) | Software ASTC→RGBA decode |
|
|
160
|
+
| Android Emulator | Usually none | Software ASTC→RGBA decode |
|
|
161
|
+
| Web (mobile) | ASTC | — |
|
|
162
|
+
| Web (desktop) | ASTC where the browser has `WEBGL_compressed_texture_astc` | Software ASTC→RGBA decode (WebAssembly) |
|
|
163
|
+
|
|
164
|
+
**Important:** On emulators frames are decoded to uncompressed RGBA. There is no GPU compression benefit on emulators—they exist purely for functional testing. Always test performance on real devices. The same goes for browsers without ASTC, where decoding costs CPU time on the main thread and textures take 4 bytes a pixel.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Installation
|
|
169
|
+
|
|
170
|
+
### React Native
|
|
171
|
+
|
|
172
|
+
**npm**, package `react-native-alpha-video-gpu`, with [range-source](#streaming-and-storage) for URLs:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npm install react-native-alpha-video-gpu react-native-range-source
|
|
176
|
+
cd ios && pod install
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`react-native-range-source` reads URLs by range, keeps them on disk or in memory, and reads [data sources](#data-sources). React Native only links an app's own dependencies, so it's in the install line rather than a dependency of the player. Without it, a URL is downloaded whole for each play ([Without range-source](#without-range-source)). One install serves `react-native-alpha-video` too.
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
import { AlphaVideoGpuView } from 'react-native-alpha-video-gpu';
|
|
183
|
+
|
|
184
|
+
<AlphaVideoGpuView
|
|
185
|
+
source={{ uri: 'https://example.com/video.ktx2', cacheKey: 'intro.ktx2' }} // cacheKey names the stored copy; default, the URL
|
|
186
|
+
storage="auto" // where the bytes are kept between plays; see Streaming and storage
|
|
187
|
+
maxStorageBytes={512 * 1024 * 1024} // the store's cap, over every clip in it
|
|
188
|
+
style={{ width: 300, height: 300 }}
|
|
189
|
+
autoPlay={true} // the default; false waits for play()
|
|
190
|
+
loop={true} // the default; false plays once, then calls onFinished
|
|
191
|
+
paused={false} // a hold, for example while the screen is hidden
|
|
192
|
+
playbackSpeed={1.0}
|
|
193
|
+
stream={true} // decodes a window of frames while playing (default); false loads every frame
|
|
194
|
+
memoryStrategy="vram" // frames on the GPU (default), or 'ram'; see Options
|
|
195
|
+
streamingPrefetchAhead={8} // stream: frames kept decoded ahead, 1 to 100; see Buffering
|
|
196
|
+
enableFrameEvents={false} // true calls onFrameUpdate for every frame
|
|
197
|
+
verbose={false} // true logs info traces; warnings and errors always log
|
|
198
|
+
onLoad={({ nativeEvent }) => console.log(nativeEvent.frameCount, nativeEvent.frameRate)}
|
|
199
|
+
onStateChange={({ nativeEvent }) => console.log(nativeEvent.state)}
|
|
200
|
+
onFrameUpdate={({ nativeEvent }) => console.log(nativeEvent.frame, nativeEvent.totalFrames)}
|
|
201
|
+
onFinished={() => console.log('played once')}
|
|
202
|
+
onError={({ nativeEvent }) => console.error(nativeEvent.error, nativeEvent.code)}
|
|
203
|
+
/>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The example sets every option; [Options](#options) says when each one takes effect. zstd is compiled in from `third_party/`; there are no other native dependencies. On the web the component uses [`alpha-video-gpu-web`](#web-support), which is installed with it.
|
|
207
|
+
|
|
208
|
+
The view works on both React Native architectures: the new one (Fabric, the default since 0.76) and the old one (`newArchEnabled=false`, up to 0.81). It is tested on 0.83, and on 0.81 with the new architecture turned off.
|
|
209
|
+
|
|
210
|
+
### Android
|
|
211
|
+
|
|
212
|
+
**Maven Central**, artifact `dev.mediacache:alpha-video-gpu`:
|
|
213
|
+
|
|
214
|
+
```gradle
|
|
215
|
+
dependencies {
|
|
216
|
+
implementation 'dev.mediacache:alpha-video-gpu:1.0.0'
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
It brings [range-source](https://github.com/MediaCacheDev/range-source) (`dev.mediacache:range-source`, about 320 KB per architecture), which reads URLs by range and keeps them between plays. To leave it out ([Without range-source](#without-range-source)):
|
|
221
|
+
|
|
222
|
+
```gradle
|
|
223
|
+
implementation('dev.mediacache:alpha-video-gpu:1.0.0') {
|
|
224
|
+
exclude group: 'dev.mediacache', module: 'range-source'
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
To read videos through a Media3 `DataSource.Factory`, add `dev.mediacache:range-source-media3` ([Data sources](#data-sources)).
|
|
229
|
+
|
|
230
|
+
`AlphaVideoGpuView` is a `TextureView` in `dev.mediacache.alphavideo.gpu`. It takes the same sources and options as the React Native component, through setters:
|
|
231
|
+
|
|
232
|
+
```kotlin
|
|
233
|
+
val view = AlphaVideoGpuView(context)
|
|
234
|
+
view.setCacheKey("intro.ktx2") // what the stored copy is named; default, the URL
|
|
235
|
+
view.setStorage("auto") // where the bytes are kept between plays; see Streaming and storage
|
|
236
|
+
view.setMaxStorageBytes(512L * 1024 * 1024) // the store's cap, over every clip in it
|
|
237
|
+
view.setAutoPlay(true) // the default; false waits for play()
|
|
238
|
+
view.setLoop(true) // the default; false plays once, then calls onFinished
|
|
239
|
+
view.setPaused(false) // a hold, for example while the screen is hidden
|
|
240
|
+
view.setPlaybackSpeed(1.0f)
|
|
241
|
+
view.setStream(true) // decodes a window of frames while playing (default); false loads every frame
|
|
242
|
+
view.setMemoryStrategy("vram") // frames on the GPU (default), or "ram"; see Options
|
|
243
|
+
view.setStreamingPrefetchAhead(8) // stream: frames kept decoded ahead, 1 to 100; see Buffering
|
|
244
|
+
view.setEnableFrameEvents(false) // true calls onFrameUpdate for every frame
|
|
245
|
+
view.setVerbose(false) // true logs info traces to Logcat
|
|
246
|
+
view.setPlayerCallback(object : AlphaVideoGpuView.PlayerCallback {
|
|
247
|
+
override fun onLoad(width: Int, height: Int, frameCount: Int, frameRate: Double) {}
|
|
248
|
+
override fun onStateChange(state: String) {}
|
|
249
|
+
override fun onFrameUpdate(frame: Int, totalFrames: Int) {}
|
|
250
|
+
override fun onFinished() {}
|
|
251
|
+
override fun onError(error: String, code: String?) {} // code: AlphaVideoGpuView.ERROR_NO_SOURCE_MODULE, or null
|
|
252
|
+
})
|
|
253
|
+
view.setSource("https://example.com/video.ktx2")
|
|
254
|
+
container.addView(view)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Every `PlayerCallback` method has an empty default; `onStateChange` and `onFrameUpdate` (with `setEnableFrameEvents(true)`) are the others. Callbacks arrive on the main thread. `play()`, `pause()`, `stop()`, `seekToFrame()` and `seekToTime()` control playback, and `setPaused()` holds it as described under [Usage](#usage); detaching the view, or `destroy()`, frees it. http(s) sources need the `INTERNET` permission in the app's manifest. Needs `minSdk` 24 and OpenGL ES 3.0.
|
|
258
|
+
|
|
259
|
+
### iOS
|
|
260
|
+
|
|
261
|
+
**CocoaPods**, pod `alpha-video-gpu`:
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
pod 'alpha-video-gpu', '1.0.0'
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Swift Package Manager**, package `alpha-video-gpu`, product `AlphaVideoGPU`:
|
|
268
|
+
|
|
269
|
+
```swift
|
|
270
|
+
.package(url: "https://github.com/MediaCacheDev/alpha-video-gpu", from: "1.0.0")
|
|
271
|
+
// in the target's dependencies:
|
|
272
|
+
.product(name: "AlphaVideoGPU", package: "alpha-video-gpu")
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The module is `AlphaVideoGPU` either way.
|
|
276
|
+
|
|
277
|
+
Both bring [range-source](https://github.com/MediaCacheDev/range-source), which reads URLs by range and keeps them between plays: the pod's `Source` subspec, and the package's default trait, `Source` (Swift 6.1, Xcode 16.3 or later). To leave it out ([Without range-source](#without-range-source)), take the pod's core alone, `pod 'alpha-video-gpu', '1.0.0', :subspecs => ['Core']`. From a package, depend on the player with `traits: []`. An Xcode app project can't choose a package's traits, so an app that adds the package in Xcode always has range-source.
|
|
278
|
+
|
|
279
|
+
`AlphaVideoGpuView` is an Objective-C `MTKView`, so it works from Swift and Objective-C:
|
|
280
|
+
|
|
281
|
+
```swift
|
|
282
|
+
import AlphaVideoGPU
|
|
283
|
+
|
|
284
|
+
let view = AlphaVideoGpuView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
|
|
285
|
+
view.cacheKey = "intro.ktx2" // what the stored copy is named; default, the URL
|
|
286
|
+
view.storage = "auto" // where the bytes are kept between plays; see Streaming and storage
|
|
287
|
+
view.maxStorageBytes = 512 * 1024 * 1024 // the store's cap, over every clip in it
|
|
288
|
+
view.autoPlay = true // the default; false waits for play
|
|
289
|
+
view.loop = true // the default; false plays once, then calls onFinished
|
|
290
|
+
view.playbackPaused = false // a hold; MTKView's own `paused` is used by the view
|
|
291
|
+
view.playbackSpeed = 1.0
|
|
292
|
+
view.stream = true // decodes a window of frames while playing (default); false loads every frame
|
|
293
|
+
view.memoryStrategy = "vram" // frames on the GPU (default), or "ram"; see Options
|
|
294
|
+
view.streamingPrefetchAhead = 8 // stream: frames kept decoded ahead, 1 to 100; see Buffering
|
|
295
|
+
view.enableFrameEvents = false // true calls onFrameUpdate for every frame
|
|
296
|
+
view.verbose = false // true logs info traces to the Xcode console
|
|
297
|
+
view.onLoad = { width, height, frameCount, frameRate in }
|
|
298
|
+
view.onStateChange = { state in }
|
|
299
|
+
view.onFrameUpdate = { frame, totalFrames in }
|
|
300
|
+
view.onFinished = { }
|
|
301
|
+
view.onError = { error, code in } // code: AlphaVideoGpuErrorNoSourceModule, or nil
|
|
302
|
+
view.source = "https://example.com/video.ktx2" // loads on the next main-queue turn
|
|
303
|
+
container.addSubview(view)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`MTKView` already has `paused` and `delegate`, which the view uses itself: pause playback with `playbackPaused`, control it with `play`, `pause`, `stop`, `seekToFrame:` and `seekToTime:`, and get events through the `onLoad`, `onFrameUpdate`, `onStateChange`, `onFinished` and `onError` blocks, which are called on the main queue. Setting `source` to `nil` stops playback and frees the textures. iOS 13 or later.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Usage
|
|
311
|
+
|
|
312
|
+
```jsx
|
|
313
|
+
import { useRef } from 'react';
|
|
314
|
+
import { AlphaVideoGpuView } from 'react-native-alpha-video-gpu';
|
|
315
|
+
|
|
316
|
+
function MyComponent() {
|
|
317
|
+
const player = useRef(null); // player.current?.seekToTime(1.5), play(), pause(), stop(), seekToFrame(n)
|
|
318
|
+
return (
|
|
319
|
+
<AlphaVideoGpuView
|
|
320
|
+
ref={player}
|
|
321
|
+
source={{ uri: 'https://example.com/video.ktx2', cacheKey: 'intro.ktx2' }} // cacheKey names the stored copy; default, the URL
|
|
322
|
+
storage="auto" // where the bytes are kept between plays; see Streaming and storage
|
|
323
|
+
maxStorageBytes={512 * 1024 * 1024} // the store's cap, over every clip in it
|
|
324
|
+
style={{ width: 300, height: 300 }}
|
|
325
|
+
autoPlay={true} // the default; false waits for play()
|
|
326
|
+
loop={true} // the default; false plays once, then calls onFinished
|
|
327
|
+
paused={false} // a hold, for example while the screen is hidden
|
|
328
|
+
playbackSpeed={1.0}
|
|
329
|
+
stream={true} // decodes a window of frames while playing (default); false loads every frame
|
|
330
|
+
memoryStrategy="vram" // frames on the GPU (default), or 'ram'; see Options
|
|
331
|
+
streamingPrefetchAhead={8} // stream: frames kept decoded ahead, 1 to 100; see Buffering
|
|
332
|
+
enableFrameEvents={false} // true calls onFrameUpdate for every frame
|
|
333
|
+
verbose={false} // true logs info traces; warnings and errors always log
|
|
334
|
+
onLoad={({ nativeEvent }) => console.log(nativeEvent.frameCount, nativeEvent.frameRate)}
|
|
335
|
+
onStateChange={({ nativeEvent }) => console.log(nativeEvent.state)}
|
|
336
|
+
onFrameUpdate={({ nativeEvent }) => console.log(nativeEvent.frame, nativeEvent.totalFrames)}
|
|
337
|
+
onFinished={() => console.log('played once')}
|
|
338
|
+
onError={({ nativeEvent }) => console.error(nativeEvent.error, nativeEvent.code)}
|
|
339
|
+
/>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
A ref controls playback: `play()`, `pause()`, `stop()` (pauses on the first frame), `seekToFrame(frame)` and `seekToTime(seconds)`. A seek shows the frame, clamped to the clip, and playback carries on from it or stays paused; seeks before `onLoad` are ignored. With `stream`, a seek outside the buffered frames decodes again, from the nearest seek point before it or onwards from the decoder, whichever is closer. Files from the encoder have a seek point every 8 MB of ASTC by default (its `--seek-interval`), so a seek decodes at most that much, a few milliseconds. A file written as one zstd stream (`--seek-interval 0`, or an older encoder) has only the start of the file. Seeking far into one of those decodes everything before the target, in slices that don't block the UI. `autoPlay` sets how a video starts: off, it waits for `play()`. `paused` is a hold to turn on and off, for example while the screen is hidden; unpausing plays again when `autoPlay` is on, and with `autoPlay` off it takes `play()`. A clip that doesn't loop stops on its last frame, and `play()` starts it over.
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
### Options
|
|
348
|
+
|
|
349
|
+
The examples under [Installation](#installation), [Usage](#usage) and [Web Support](#web-support) set every option, each to its default. The memory options (`stream`, `memoryStrategy` and `streamingPrefetchAhead`) and the storage options (`storage`, `maxStorageBytes`, `cacheKey`) are read when a source loads, so set them before the source; on the web, pass them when creating the player, and `cacheKey` with `setSource`. `autoPlay`, `loop`, `paused`, `playbackSpeed`, `enableFrameEvents` and `verbose` can change at any time.
|
|
350
|
+
|
|
351
|
+
Where the frames live:
|
|
352
|
+
|
|
353
|
+
| | `stream` off | `stream` on (the default) |
|
|
354
|
+
|---|---|---|
|
|
355
|
+
| `'vram'` (the default) | Every frame in a GPU texture array | `streamingPrefetchAhead` + 2 frames in a GPU texture array, decoded straight into it |
|
|
356
|
+
| `'ram'` | Every frame in RAM, uploaded to the GPU as it's shown. Android and web; iOS loads every frame into VRAM | `streamingPrefetchAhead` + 2 frames in RAM, uploaded into 4 GPU slots as they're shown |
|
|
357
|
+
|
|
358
|
+
`streamingPrefetchAhead` is the only size to set: how many frames are kept decoded after the shown one, 1 to 100 (values outside are clamped). See [Buffering](#buffering) for the trade-off and [Playback Modes](#playback-modes) for when to use each mode.
|
|
359
|
+
|
|
360
|
+
### Sources
|
|
361
|
+
|
|
362
|
+
`source.uri` takes one of these forms. Anything else is reported through `onError`.
|
|
363
|
+
|
|
364
|
+
| Form | iOS | Android | Web | Notes |
|
|
365
|
+
|------|-----|---------|-----|-------|
|
|
366
|
+
| `https://…`, `http://…` | ✓ | ✓ | ✓ | Read by range requests as it plays, and kept between plays; see [Streaming and storage](#streaming-and-storage). Without range-source on Android and iOS, downloaded whole. `source.headers` go with every request. |
|
|
367
|
+
| anything, with `source.dataSource` | ✓ | ✓ | — | Read by a data source the app registered under that name; see [Data sources](#data-sources). On the web, pass a byte source instead (below). |
|
|
368
|
+
| `file:///abs/path`, `/abs/path` | ✓ | ✓ | — | Read in place. |
|
|
369
|
+
| `asset://path/in/bundle` | ✓ | ✓ | ✓ | Android: APK `assets/`. iOS: the app bundle's resource folder. Web: the site root (`/path/in/bundle`). |
|
|
370
|
+
| `content://…` | — | ✓ | — | Read through the `ContentResolver`. |
|
|
371
|
+
| bare resource name | — | ✓ | — | A `res/raw` resource, which is what `require()` resolves to in an Android release build. |
|
|
372
|
+
|
|
373
|
+
Nothing is copied: bundled assets, raw resources and local files are read where they are.
|
|
374
|
+
|
|
375
|
+
On the web, `source` can also be a `Blob` or `File`, or a byte source, `{ size, read(offset, length, signal) }`, that reads the file from the app's own code (a fetcher, a cache, decryption). See [the web package's README](web/README.md#sources-and-storage).
|
|
376
|
+
|
|
377
|
+
#### Streaming and storage
|
|
378
|
+
|
|
379
|
+
An http(s) source is read by HTTP range requests as it plays, through the same reader on every platform ([range-source](https://github.com/MediaCacheDev/range-source)). It's included on Android and iOS, and in React Native it's `react-native-range-source` ([Installation](#installation)). On Android and iOS:
|
|
380
|
+
|
|
381
|
+
- With `stream` on and a zstd file (the encoder's default), playback starts after a few small reads: the header, and the seek table at the end of the level. The level is then read from its start, 256 KB at a time as the decoder needs it, and a seek reads from the seek point before its target. A file written as one zstd stream has no seek table, so a far seek reads on from where the decoder is.
|
|
382
|
+
- An uncompressed file, and any file with `stream` off, is read whole before it plays, in one request.
|
|
383
|
+
- A server without range support works too. The file is then read from the start, and a seek reads it again up to where it lands.
|
|
384
|
+
- A dropped connection is retried 3 times, carrying on from where it stopped. A read that still fails is reported through `onError` as `Reading <uri> failed: …`, and playback stops on the last frame shown. A source that can't be opened (a 401, say) reports `Loading <uri> failed: …`.
|
|
385
|
+
- If the network is slower than the clip, playback waits for the bytes and the UI stays responsive.
|
|
386
|
+
- iOS needs the file's size, which servers send as `Content-Length` or `Content-Range`.
|
|
387
|
+
|
|
388
|
+
`storage` sets where the bytes are kept between plays:
|
|
389
|
+
|
|
390
|
+
| Value | Kept |
|
|
391
|
+
|---|---|
|
|
392
|
+
| `'auto'` (default) | `'disk'` for a URL, `'none'` for a data source |
|
|
393
|
+
| `'disk'` | In the app's cache folder: `alpha-video-gpu` in `getCacheDir()` on Android, in `Caches` on iOS. The system may clear it when space runs low |
|
|
394
|
+
| `'memory'` | In memory, for the app's lifetime |
|
|
395
|
+
| `'none'` | Nowhere: each play fetches again |
|
|
396
|
+
|
|
397
|
+
Storage is partial: it keeps the ranges played so far, and a clip held whole plays again without the network, offline too. `maxStorageBytes` (default 512 MB) caps the store across every clip, evicting the least recently played. Every view shares one disk store and one memory store, so the cap set last applies to it. A copy is dropped when the server's `ETag`, `Last-Modified` or length no longer matches it. A clip held whole isn't checked again; give a changed file a new `cacheKey` or URL.
|
|
398
|
+
|
|
399
|
+
Local files, bundled files, `content://` URIs and `res/raw` resources are read in place and never stored. On react-native-web, `'disk'` is the browser's storage, as `'auto'` (OPFS, else IndexedDB); the web package also takes `'opfs'`, `'indexeddb'`, `'cache'` and stores of an app's own ([Web Support](#web-support)).
|
|
400
|
+
|
|
401
|
+
#### Without range-source
|
|
402
|
+
|
|
403
|
+
An app that leaves range-source out (to have no network or disk code of ours, or with its own caching) still plays local files, bundled files, `content://` URIs and `res/raw` resources as before. A URL is downloaded whole, with one request from the start, into a temporary file, and plays as it arrives:
|
|
404
|
+
- a seek past what has arrived waits for it;
|
|
405
|
+
- loops and seeks back read the file;
|
|
406
|
+
- nothing is kept between plays, so a replay downloads it again;
|
|
407
|
+
- `headers` are sent.
|
|
408
|
+
|
|
409
|
+
`storage`, `maxStorageBytes` and `cacheKey` have no effect, and the player logs once that range-source isn't installed. A data source reports `onError` with the code `NO_SOURCE_MODULE` (`AlphaVideoGpuView.ERROR_NO_SOURCE_MODULE` on Android, `AlphaVideoGpuErrorNoSourceModule` on iOS), with a message naming the add-on.
|
|
410
|
+
|
|
411
|
+
#### Cache keys
|
|
412
|
+
|
|
413
|
+
Stored clips are named by `cacheKey` if you give one, otherwise by the URL. Set `cacheKey` when the URL changes while the file doesn't (presigned URLs), so a new URL still finds the stored copy, and doesn't reload a view that is already playing it:
|
|
414
|
+
|
|
415
|
+
```jsx
|
|
416
|
+
<AlphaVideoGpuView source={{ uri: presignedUrl, cacheKey: 'videos/glow.ktx2' }} />
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
`cacheKey` can also be passed as a top-level prop; it wins over `source.cacheKey`.
|
|
420
|
+
|
|
421
|
+
#### Data sources
|
|
422
|
+
|
|
423
|
+
An app that fetches, caches or decrypts the bytes itself registers a data source under a name, and names it in `source`. The player asks it for byte ranges as it plays, and passes `uri` and `headers` through, so the URI can be anything the data source understands:
|
|
424
|
+
|
|
425
|
+
```tsx
|
|
426
|
+
<AlphaVideoGpuView source={{ uri: 'https://cdn.example.com/intro.ktx2', dataSource: 'media3' }} />
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
A data source's bytes aren't stored by default, since it usually caches them itself; `storage="disk"` or `"memory"` stores them too. On the native views, call `setDataSource("name")` (Android) or set `dataSource = "name"` (iOS) before the source, with `setHeaders` or `headers`.
|
|
430
|
+
|
|
431
|
+
Data sources are range-source's, so they need it ([Without range-source](#without-range-source)), and one registered once serves this player and the WebM player ([alpha-video](https://github.com/MediaCacheDev/alpha-video)):
|
|
432
|
+
|
|
433
|
+
**Android:** implement `dev.mediacache.rangesource.RangeDataSource`. `open(uri, position, headers)` starts a read at `position` and returns the file's size and version (`new Info(size, etag, lastModified)`). `read` and `close` follow, and `cancel` (optional) stops a read blocked on the network from another thread. Register a factory, which makes one per player:
|
|
434
|
+
|
|
435
|
+
```kotlin
|
|
436
|
+
RangeDataSources.register("mine") { MyDataSource() }
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
To read through a Media3 `DataSource.Factory` (OkHttp, Cronet, a `CacheDataSource`), add range-source's adapter, which uses the app's own `androidx.media3:media3-datasource` (compiled against 1.8.0):
|
|
440
|
+
|
|
441
|
+
```gradle
|
|
442
|
+
implementation 'dev.mediacache:range-source-media3:1.0.0'
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
```kotlin
|
|
446
|
+
RangeDataSources.register("media3", Media3RangeDataSource.factory(dataSourceFactory))
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
The built-in fetcher is public too, to register with other timeouts or a user agent: `RangeHttpDataSource.Factory().setConnectTimeoutMs(…).setReadTimeoutMs(…).setUserAgent(…)`.
|
|
450
|
+
|
|
451
|
+
**iOS:** conform to `RangeDataSource`. Each call reads a window of a few megabytes: call `response` once with the file's size and version, then `data` with the bytes in order, then `completion`. One instance serves every player, so a read mustn't depend on the one before:
|
|
452
|
+
|
|
453
|
+
```swift
|
|
454
|
+
import RangeSource
|
|
455
|
+
|
|
456
|
+
final class MyDataSource: NSObject, RangeDataSource {
|
|
457
|
+
func read(url: URL, offset: Int64, length: Int64, headers: [String: String],
|
|
458
|
+
response: @escaping (RangeDataInfo) -> Void,
|
|
459
|
+
data: @escaping (Data) -> Void,
|
|
460
|
+
completion: @escaping (Error?) -> Void) -> RangeCancellable {
|
|
461
|
+
// response(RangeDataInfo(size: total, etag: etag, lastModified: nil)), data(…), completion(nil)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
RangeDataSources.register(MyDataSource(), name: "mine")
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**React Native:** register in native code, as above, in `MainApplication` (Android) and `AppDelegate` (iOS, `import RangeSource`).
|
|
469
|
+
|
|
470
|
+
#### Bundled with `require()`
|
|
471
|
+
|
|
472
|
+
Register the extensions with Metro:
|
|
473
|
+
|
|
474
|
+
```js
|
|
475
|
+
// metro.config.js
|
|
476
|
+
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
|
|
477
|
+
|
|
478
|
+
const defaultConfig = getDefaultConfig(__dirname);
|
|
479
|
+
module.exports = mergeConfig(defaultConfig, {
|
|
480
|
+
resolver: {
|
|
481
|
+
assetExts: [...defaultConfig.resolver.assetExts, 'ktx2'],
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Then resolve the module to a URI:
|
|
487
|
+
|
|
488
|
+
```jsx
|
|
489
|
+
import { Image } from 'react-native';
|
|
490
|
+
|
|
491
|
+
const video = Image.resolveAssetSource(require('./assets/video.ktx2'));
|
|
492
|
+
|
|
493
|
+
<AlphaVideoGpuView source={{ uri: video.uri }} />
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
In debug builds that URI is the Metro dev server (`http://…`, read and stored like any URL). In release builds it's a `file://` path in the iOS bundle, or a `res/raw` name on Android. On web, `Image.resolveAssetSource` doesn't exist; use the module value directly when it's a string, or `expo-asset`'s `Asset.fromModule(module).uri`.
|
|
497
|
+
|
|
498
|
+
#### Streaming from Android assets and resources
|
|
499
|
+
|
|
500
|
+
Android compresses APK entries it doesn't recognise, and a compressed entry can't be memory-mapped. Non-streaming playback (`stream={false}`) reads it anyway, but streaming, the default, needs the file stored uncompressed, and reports `onError` otherwise:
|
|
501
|
+
|
|
502
|
+
```groovy
|
|
503
|
+
// android/app/build.gradle
|
|
504
|
+
android {
|
|
505
|
+
androidResources {
|
|
506
|
+
noCompress += ['ktx2']
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Without it, `stream` on an `asset://` or `res/raw` file fails with an error naming this setting.
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## Metadata
|
|
516
|
+
|
|
517
|
+
The player reads everything from standard KTX 2.0 fields, so it needs no custom metadata:
|
|
518
|
+
|
|
519
|
+
| What | Where in the file |
|
|
520
|
+
|------|-------------------|
|
|
521
|
+
| Frame width and height | Header: `pixelWidth`, `pixelHeight` |
|
|
522
|
+
| Frame count | Header: `layerCount` (one layer per frame) |
|
|
523
|
+
| ASTC block size | Header: `vkFormat` |
|
|
524
|
+
| Frame rate | Key/value data: [`KTXanimData`](https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html#_ktxanimdata), the time per frame as a fraction, so 29.97 fps (1001/30000 s) is exact. Without it, 30 fps |
|
|
525
|
+
|
|
526
|
+
`onLoad` reports `frameRate` as a decimal (29.97…), and `seekToTime` uses it.
|
|
527
|
+
|
|
528
|
+
Only ASTC texture arrays play. Files in other formats (UASTC and other Basis Universal files, atlas packing, GLB containers, multi-part files) are rejected with an error; encode them again with `texenc`. Files from earlier encoders, which also carried a JSON `BasisPlayerMetadata` entry, still play, at 30 fps.
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Web Support
|
|
534
|
+
|
|
535
|
+
On React Native Web, `AlphaVideoGpuView` draws with the WebGL player from [`alpha-video-gpu-web`](web). For other web apps, install that package on its own:
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
npm install alpha-video-gpu-web
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
import { createAlphaVideoGpuPlayer } from 'alpha-video-gpu-web';
|
|
543
|
+
|
|
544
|
+
const player = createAlphaVideoGpuPlayer(document.querySelector('canvas')!, {
|
|
545
|
+
autoPlay: true, // false waits for play()
|
|
546
|
+
loop: true, // false plays once, then calls onFinished
|
|
547
|
+
playbackSpeed: 1.0,
|
|
548
|
+
stream: true, // decodes a window of frames while playing (default); false loads every frame
|
|
549
|
+
memoryStrategy: 'vram', // frames on the GPU (default), or 'ram'; see Options
|
|
550
|
+
streamingPrefetchAhead: 8, // stream: frames kept decoded ahead, 1 to 100; see Buffering
|
|
551
|
+
frameEvents: true, // the web default; false calls onFrameUpdate only after seeks
|
|
552
|
+
verbose: false, // true logs info traces to the browser console
|
|
553
|
+
storage: 'auto', // where played bytes are kept: OPFS, else IndexedDB; or 'indexeddb', 'cache', 'memory', 'none'
|
|
554
|
+
maxStorageBytes: 512 * 1024 * 1024, // this player's cap across every clip, least recently played evicted first
|
|
555
|
+
onLoad: ({ width, height, frameCount, frameRate }) => {},
|
|
556
|
+
onStateChange: (state) => {},
|
|
557
|
+
onFrameUpdate: (frame, total) => {},
|
|
558
|
+
onFinished: () => {},
|
|
559
|
+
onError: (error) => console.error(error), // an AlphaVideoGpuError; see its `code`
|
|
560
|
+
});
|
|
561
|
+
player.setSource('https://example.com/video.ktx2', { cacheKey: 'intro.ktx2', headers: {} }); // also a Blob, bytes or a byte source
|
|
562
|
+
player.setPaused(false);
|
|
563
|
+
// player.play(), player.pause(), player.resume(), player.stop(), player.seekToFrame(n),
|
|
564
|
+
// player.seekToTime(seconds), player.destroy()
|
|
565
|
+
// Changeable later: player.autoPlay, .loop, .playbackSpeed, .frameEvents, .verbose
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
```tsx
|
|
569
|
+
import { AlphaVideoGpuPlayer } from 'alpha-video-gpu-web/react';
|
|
570
|
+
|
|
571
|
+
<AlphaVideoGpuPlayer
|
|
572
|
+
src="https://example.com/video.ktx2"
|
|
573
|
+
cacheKey="intro.ktx2" // names the stored copy; default, the URL
|
|
574
|
+
width={300}
|
|
575
|
+
height={300}
|
|
576
|
+
className="video"
|
|
577
|
+
style={{ display: 'block' }}
|
|
578
|
+
autoPlay={true}
|
|
579
|
+
loop={true}
|
|
580
|
+
paused={false}
|
|
581
|
+
playbackSpeed={1.0}
|
|
582
|
+
stream={true}
|
|
583
|
+
memoryStrategy="vram"
|
|
584
|
+
streamingPrefetchAhead={8}
|
|
585
|
+
verbose={false}
|
|
586
|
+
onLoad={({ frameCount }) => {}}
|
|
587
|
+
onStateChange={(state) => {}}
|
|
588
|
+
onFrameUpdate={(frame, total) => {}}
|
|
589
|
+
onFinished={() => {}}
|
|
590
|
+
onError={(error) => console.error(error)}
|
|
591
|
+
/>
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
The zstd decoder (JS and WebAssembly) is bundled inline, so no extra scripts or files need to be served. React is an optional peer dependency, needed only for `alpha-video-gpu-web/react`.
|
|
595
|
+
|
|
596
|
+
### Web Texture Format Support
|
|
597
|
+
|
|
598
|
+
| Format | Browser Support | Notes |
|
|
599
|
+
|--------|-----------------|-------|
|
|
600
|
+
| **ASTC** | All with WebGL 2 | Sampled by the GPU with `WEBGL_compressed_texture_astc`, else decoded to RGBA in WebAssembly |
|
|
601
|
+
| **Zstd compression** | All | Bundled WASM decoder |
|
|
602
|
+
|
|
603
|
+
Without `WEBGL_compressed_texture_astc`, frames are decoded to RGBA as they're uploaded, by a 42 KB WebAssembly decoder that only those browsers download. Preloaded playback then keeps the clip as ASTC and decodes the shown frame; [the web package's README](web/README.md#without-gpu-astc) has the costs. If WebAssembly is blocked too (a Content-Security-Policy without `'wasm-unsafe-eval'`), `onError` gets an `AlphaVideoGpuError` whose `code` is `'ASTC_UNSUPPORTED'` (`nativeEvent.code` in the React Native component); show something else in its place.
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
## Playback Modes
|
|
608
|
+
|
|
609
|
+
The player supports multiple playback strategies optimized for different use cases.
|
|
610
|
+
|
|
611
|
+
### Memory Strategy: VRAM vs RAM
|
|
612
|
+
|
|
613
|
+
The `memoryStrategy` prop controls where the main frame buffer is located. The goal is to **maximize** usage of one memory type while **minimizing** the other:
|
|
614
|
+
|
|
615
|
+
| Strategy | Main Buffer | Behavior | Performance |
|
|
616
|
+
|----------|-------------|----------|-------------|
|
|
617
|
+
| `vram` (default) | GPU VRAM | Maximizes VRAM, minimizes RAM | **Generally preferred** - frames are decoded straight into GPU textures, outside the app's heap |
|
|
618
|
+
| `ram` | CPU RAM | Maximizes RAM, minimizes VRAM | Use when VRAM is constrained - but note that high RAM usage (1GB+) can cause app slowdowns |
|
|
619
|
+
|
|
620
|
+
On iOS, playback without `stream` always keeps every frame in VRAM.
|
|
621
|
+
|
|
622
|
+
**Why `vram` is the default:** when streaming, it holds `streamingPrefetchAhead` + 2 frames on the GPU and almost nothing in RAM, where `ram` holds the same frames in RAM plus 4 GPU slots. So it uses less memory in total and copies each frame once, and on Android the upload runs asynchronously through a pixel buffer. Its frames stay out of the Java heap on Android (whose limit is often 256–512 MB) and the JavaScript heap on the web. Phones share physical memory between CPU and GPU, so on iOS the textures still count toward the app's memory limit, but `vram` still uses less of it. The ring is allocated as one texture array when the source loads; a clip too large for it reports `onError`.
|
|
623
|
+
|
|
624
|
+
### Non-Streaming
|
|
625
|
+
|
|
626
|
+
With `stream={false}`, all frames are loaded into memory before playback begins. Best for short-to-medium videos.
|
|
627
|
+
|
|
628
|
+
```jsx
|
|
629
|
+
<AlphaVideoGpuView
|
|
630
|
+
source={{ uri: 'https://example.com/video.ktx2' }}
|
|
631
|
+
stream={false}
|
|
632
|
+
autoPlay
|
|
633
|
+
loop
|
|
634
|
+
/>
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**Non-streaming memory behavior:**
|
|
638
|
+
|
|
639
|
+
| Strategy | Description |
|
|
640
|
+
|----------|-------------|
|
|
641
|
+
| `vram` | All frames loaded directly to GPU VRAM, skipping RAM entirely. Fastest playback, instant frame switching. |
|
|
642
|
+
| `ram` | All frames held in CPU RAM, streamed to a small VRAM buffer on demand. Minimizes VRAM while RAM holds all data. |
|
|
643
|
+
|
|
644
|
+
```jsx
|
|
645
|
+
<AlphaVideoGpuView
|
|
646
|
+
source={{ uri: 'https://example.com/video.ktx2' }}
|
|
647
|
+
stream={false}
|
|
648
|
+
memoryStrategy="ram"
|
|
649
|
+
autoPlay
|
|
650
|
+
loop
|
|
651
|
+
/>
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### Streaming Mode (Default)
|
|
655
|
+
|
|
656
|
+
For very long videos or memory-constrained scenarios, streaming mode decodes frames on-demand during playback using a ring buffer. Only a small window of frames is kept in memory at any time, providing **constant and predictable memory usage** regardless of video length.
|
|
657
|
+
|
|
658
|
+
Streaming is on unless `stream` is `false`, for files with KTX2-internal zstd (the encoder default) or no compression (`--zstd 0`).
|
|
659
|
+
|
|
660
|
+
The `streamMode` metadata key that older encoders wrote is ignored; only the `stream` prop decides.
|
|
661
|
+
|
|
662
|
+
```jsx
|
|
663
|
+
<AlphaVideoGpuView
|
|
664
|
+
source={{ uri: 'https://example.com/video.ktx2' }}
|
|
665
|
+
stream
|
|
666
|
+
memoryStrategy="vram"
|
|
667
|
+
streamingPrefetchAhead={8}
|
|
668
|
+
/>
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
#### Streaming Props
|
|
672
|
+
|
|
673
|
+
| Prop | Default | Description |
|
|
674
|
+
|------|---------|-------------|
|
|
675
|
+
| `stream` | `true` | Stream a texture array instead of loading every frame |
|
|
676
|
+
| `memoryStrategy` | `"vram"` | Where the decoded frames are kept: `"vram"` on the GPU (minimizes RAM), `"ram"` in RAM with 4 GPU slots (minimizes VRAM) |
|
|
677
|
+
| `streamingPrefetchAhead` | `8` | Frames kept decoded after the shown one (1–100); see [Buffering](#buffering) |
|
|
678
|
+
|
|
679
|
+
#### VRAM Streaming (Default)
|
|
680
|
+
|
|
681
|
+
Frames are decoded straight into a GPU texture array of `streamingPrefetchAhead` + 2 layers, used as a ring: frame n goes in layer n modulo the size. With `streamingPrefetchAhead={5}`, 7 layers:
|
|
682
|
+
|
|
683
|
+
```
|
|
684
|
+
[GPU texture array: 5 + 2 = 7 layers]
|
|
685
|
+
Frame 100 layer 2 previous frame, which the GPU may still be drawing
|
|
686
|
+
Frame 101 layer 3 ← shown
|
|
687
|
+
Frame 102 layer 4 ┐
|
|
688
|
+
Frame 103 layer 5 │
|
|
689
|
+
Frame 104 layer 6 │ decoded ahead (streamingPrefetchAhead = 5)
|
|
690
|
+
Frame 105 layer 0 │
|
|
691
|
+
Frame 106 layer 1 ┘
|
|
692
|
+
When frame 102 shows, frame 107 is decoded into layer 2, over frame 100
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
#### RAM Streaming
|
|
696
|
+
|
|
697
|
+
The same frames are decoded into RAM instead, and each is uploaded into one of 4 GPU slots as it's about to show. That keeps VRAM at 4 frames whatever `streamingPrefetchAhead` is, and costs an upload per frame shown.
|
|
698
|
+
|
|
699
|
+
```
|
|
700
|
+
[RAM: 5 + 2 = 7 frames] [GPU: 4 slots]
|
|
701
|
+
Frames 100–106 (101 shown, 102–106 ahead) → the shown frame and the next ones to show
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
#### Why Streaming?
|
|
705
|
+
|
|
706
|
+
Streaming enables playback of arbitrarily long videos with **fixed memory footprint**:
|
|
707
|
+
- A 10,000 frame video uses the same memory as a 100 frame video
|
|
708
|
+
- Memory is set by one prop, `streamingPrefetchAhead`
|
|
709
|
+
- Ideal for long-form content, digital signage, or memory-constrained devices
|
|
710
|
+
|
|
711
|
+
#### Buffering
|
|
712
|
+
|
|
713
|
+
`streamingPrefetchAhead` is the only buffer setting. The decoder runs ahead of the playhead until that many frames after the shown one are ready, and a frame's space is reused once it's behind the playhead, so the player holds `streamingPrefetchAhead` + 2 frames: the previous one, the shown one and the ones ahead. With `vram` they are the GPU ring itself, so its size follows from the same setting; with `ram` they are in RAM, and the GPU has 4 slots whatever the setting.
|
|
714
|
+
|
|
715
|
+
The frames ahead are a safety margin. Decompressing zstd usually keeps well ahead, but some stretches take longer than a frame's time: the restart at each loop, other players or the app busy on the CPU, or the bytes of a URL still arriving. While frames are left ahead, playback carries on through them; when none are left, the shown frame stays up until the next is ready. A larger margin rides out longer slow stretches and costs memory, 1 byte a pixel per frame in `astc4x4` (less for larger blocks, and 4 bytes on the web without GPU ASTC):
|
|
716
|
+
|
|
717
|
+
| Frame size | One frame (`astc4x4`) | At the default (10 frames) |
|
|
718
|
+
|---|---|---|
|
|
719
|
+
| 400×400 | 0.16 MB | 1.6 MB |
|
|
720
|
+
| 1080×1080 | 1.2 MB | 12 MB |
|
|
721
|
+
| 1920×1080 | 2.1 MB | 21 MB |
|
|
722
|
+
|
|
723
|
+
The default, 8, is about a quarter of a second at 30 fps, the same as alpha-video's. Decompressing zstd is lighter work than decoding video, so it is a generous margin; go higher for large frames on low-end phones, or on the web to keep short loops in the ring.
|
|
724
|
+
|
|
725
|
+
It doesn't change the decoding work, only how far ahead it's done, and it doesn't buffer bytes: with range-source, the reads ahead of the decoder are its own (see [Streaming and storage](#streaming-and-storage)). Uncompressed files decode nothing ahead and ignore it. On the web a clip whose frames all fit in the `vram` ring loops without decoding again, so a larger value can save the CPU work of short loops there.
|
|
726
|
+
|
|
727
|
+
To tune it on Android and iOS, turn on `verbose`. At the end of every pass through the clip the player logs a line like `Pass buffer: fewest frames ahead 6 of 8, stalls 0`: the fewest frames it had ready during the pass, and how many times it had none and held a frame. Watch a few loops on the slowest device you support, with as many players as the app shows at once. If the fewest stays well above 0 and there are no stalls, a smaller value saves memory; with stalls, a larger one helps if they come from short slow stretches, but if decoding can't keep up on average no margin will, and fewer players at once or an uncompressed file will.
|
|
728
|
+
|
|
729
|
+
### Encoding for Streaming
|
|
730
|
+
|
|
731
|
+
The encoder's defaults (ASTC texture array, KTX2-internal zstd) already produce a streamable file; play it with `stream`.
|
|
732
|
+
|
|
733
|
+
#### Seeking in zstd files
|
|
734
|
+
|
|
735
|
+
A zstd decoder can only start at the beginning of a zstd frame. By default the encoder starts a new frame every 8 MB of ASTC and ends the level with a seek table in zstd's [seekable format](https://github.com/facebook/zstd/tree/dev/contrib/seekable_format). A streaming seek then starts at the frame before its target, unless carrying on from the decoder is closer. On an M4 Pro the slowest seek took 6 ms, against 1.5 s (5 minutes of 360p) and 2.9 s (60 s of 1080p) for the same clips as one stream; the 8 MB seek points cost 0.8% and 1.6% in size.
|
|
736
|
+
|
|
737
|
+
`--seek-interval 0` writes one stream instead, the smallest file, for clips that only play and loop. Players that don't know the table, and every other zstd tool, read both layouts as ordinary zstd. This player reads one-stream files too, and seeks in them by decoding from the start.
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
## Uncompressed ASTC Streaming
|
|
742
|
+
|
|
743
|
+
For the most players at once and the least CPU, uncompressed files skip zstd entirely. The KTX2 file holds the ASTC blocks as they are, and the player uploads them straight from the memory-mapped file.
|
|
744
|
+
|
|
745
|
+
### When to Use
|
|
746
|
+
|
|
747
|
+
- **Dozens of simultaneous players** on one screen
|
|
748
|
+
- **Always-on displays** (kiosks, digital signage), where CPU and battery add up
|
|
749
|
+
- **Smooth playback while the CPU is busy** with the rest of the app
|
|
750
|
+
- **Storage is not a constraint** (local assets, fast network)
|
|
751
|
+
|
|
752
|
+
### Performance Characteristics
|
|
753
|
+
|
|
754
|
+
The glow clip (400×400, 181 frames) on the budget phone from [the measurements above](#on-a-budget-android-phone):
|
|
755
|
+
|
|
756
|
+
| Metric | Zstd compressed | Uncompressed (`--zstd 0`) |
|
|
757
|
+
|--------|-----------------|---------------|
|
|
758
|
+
| File size | 4.9 MB | 29 MB (6x larger) |
|
|
759
|
+
| CPU during playback | Zstd decode + upload | Upload only |
|
|
760
|
+
| 20 players | 3.1 cores, 908 MB, demo UI at 52 fps | 2.4 cores, 371 MB, demo UI at 52 fps |
|
|
761
|
+
| 50 players | All 8 cores, 2.1 GB, demo UI at 3 fps | 3.7 cores, 699 MB, demo UI at 41 fps |
|
|
762
|
+
| Memory bandwidth | Lower | Higher |
|
|
763
|
+
|
|
764
|
+
### Encoding Uncompressed ASTC
|
|
765
|
+
|
|
766
|
+
```bash
|
|
767
|
+
texenc --input ./frames --output video_uncompressed.ktx2 --fps 30 --zstd 0
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
`--zstd 0` leaves the ASTC blocks uncompressed, so the player can memory-map the file and upload frames without decompressing them.
|
|
771
|
+
|
|
772
|
+
### Usage
|
|
773
|
+
|
|
774
|
+
```jsx
|
|
775
|
+
<AlphaVideoGpuView
|
|
776
|
+
source={{ uri: 'https://example.com/video_uncompressed.ktx2' }}
|
|
777
|
+
stream
|
|
778
|
+
/>
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### How It Works
|
|
782
|
+
|
|
783
|
+
1. **Memory-mapped file access** - The KTX2 file is mmap'd, allowing the OS to page frames in/out efficiently
|
|
784
|
+
2. **Direct GPU upload** - ASTC blocks are copied directly to GPU texture memory (no Zstd decompression needed)
|
|
785
|
+
3. **Ring buffer playback** - Each frame is copied into a ring of 4 GPU slots as it's shown, so only 4 frames are in GPU memory at once. Nothing is decoded ahead, so `streamingPrefetchAhead` and `memoryStrategy` don't apply
|
|
786
|
+
4. **No decoding on the CPU** - On devices with ASTC hardware, each frame is a copy from the mapped file to the GPU
|
|
787
|
+
|
|
788
|
+
### Trade-offs
|
|
789
|
+
|
|
790
|
+
| Pros | Cons |
|
|
791
|
+
|------|------|
|
|
792
|
+
| The lowest CPU use: no decompression | 6x larger file size than zstd |
|
|
793
|
+
| Scales to dozens of players (50 on a budget phone) | Higher storage/bandwidth requirements |
|
|
794
|
+
| Consistent frame timing | Not suitable for slow networks |
|
|
795
|
+
| Lower battery consumption | Larger app bundle if bundled |
|
|
796
|
+
| No decode latency on startup | — |
|
|
797
|
+
|
|
798
|
+
### Simulator Behavior
|
|
799
|
+
|
|
800
|
+
On iOS/Android simulators (no hardware ASTC), uncompressed streaming still requires software ASTC→RGBA decode. The player performs this decode on a background thread to keep the UI responsive, but performance will be significantly lower than on real devices.
|
|
801
|
+
|
|
802
|
+
---
|
|
803
|
+
|
|
804
|
+
## Performance Characteristics
|
|
805
|
+
|
|
806
|
+
- **CPU:** Low: zstd decompression for compressed files while streaming, frame uploads, and the platform redrawing the view for each new frame. On Android each player is a `TextureView`, and that redraw is most of what one small player costs. See [the measurements](#on-a-budget-android-phone).
|
|
807
|
+
- **Memory:** GPU texture memory = the ASTC size (ASTC decompresses on GPU)
|
|
808
|
+
- **Load time:** Direct upload of the ASTC blocks
|
|
809
|
+
|
|
810
|
+
### Playback Mode Comparison
|
|
811
|
+
|
|
812
|
+
| Mode | Load Time | Memory | CPU (Playback) | Best For |
|
|
813
|
+
|------|-----------|--------|----------------|----------|
|
|
814
|
+
| Non-streaming VRAM | Slow (all frames) | High VRAM, minimal RAM | Lowest | Short videos, best performance |
|
|
815
|
+
| Non-streaming RAM | Slow (all frames) | High RAM, minimal VRAM | Low (uploads) | VRAM-constrained devices |
|
|
816
|
+
| Streaming VRAM (Zstd) | Fast (prefetch) | Low VRAM ring buffer | Low (zstd) | Long videos, fixed memory |
|
|
817
|
+
| Streaming RAM (Zstd) | Fast (prefetch) | Medium RAM cache, minimal VRAM | Low (zstd) | Long videos, VRAM-constrained |
|
|
818
|
+
| Streaming Uncompressed | Fast (mmap) | Low VRAM ring buffer | Lowest | Many players at once, always-on displays |
|
|
819
|
+
|
|
820
|
+
Best suited for:
|
|
821
|
+
- Looping UI effects (glows, shimmers, highlights)
|
|
822
|
+
- Transparent overlays
|
|
823
|
+
- Kiosk/always-on displays
|
|
824
|
+
- Any video where consistent frame timing matters
|
|
825
|
+
- **Many simultaneous players** (dozens, with uncompressed files)
|
|
826
|
+
|
|
827
|
+
Not ideal for:
|
|
828
|
+
- Long-form video content
|
|
829
|
+
- Audio-synced playback
|
|
830
|
+
- Bandwidth-constrained streaming (use Zstd compression)
|
|
831
|
+
|
|
832
|
+
---
|
|
833
|
+
|
|
834
|
+
## Props Reference
|
|
835
|
+
|
|
836
|
+
| Prop | Type | Default | Description |
|
|
837
|
+
|------|------|---------|-------------|
|
|
838
|
+
| `source` | `{ uri: string, cacheKey?: string, headers?: Record<string, string>, dataSource?: string }` | — | What to play; see [Sources](#sources). On web also a `Blob` or a byte source, and `dataSource` has no effect |
|
|
839
|
+
| `style` | `ViewStyle` | — | Container style |
|
|
840
|
+
| `autoPlay` | `boolean` | `true` | Start playback once loaded |
|
|
841
|
+
| `loop` | `boolean` | `true` | Loop playback |
|
|
842
|
+
| `paused` | `boolean` | `false` | Pause playback (and streaming decompression) |
|
|
843
|
+
| `playbackSpeed` | `number` | `1.0` | Speed multiplier |
|
|
844
|
+
| `cacheKey` | `string` | — | Names the stored copy (default, the URL); overrides `source.cacheKey`. See [Cache keys](#cache-keys) |
|
|
845
|
+
| `storage` | `"auto"` \| `"disk"` \| `"memory"` \| `"none"` | `"auto"` | Where the bytes are kept between plays; see [Streaming and storage](#streaming-and-storage) |
|
|
846
|
+
| `maxStorageBytes` | `number` | 512 MB | The store's cap, over every clip in it |
|
|
847
|
+
| `memoryStrategy` | `"vram"` \| `"ram"` | `"vram"` | Where frames are kept; see [Options](#options) |
|
|
848
|
+
| `stream` | `boolean` | `true` | Stream a texture array with a ring buffer |
|
|
849
|
+
| `streamingPrefetchAhead` | `number` | `8` | Frames kept decoded after the shown one while streaming (1–100): a safety margin that costs memory. The player keeps this many plus 2, on the GPU with `vram` and in RAM with `ram` (see [Buffering](#buffering)) |
|
|
850
|
+
| `enableFrameEvents` | `boolean` | `false` | Send `onFrameUpdate` for every frame played, not only after seeks |
|
|
851
|
+
| `verbose` | `boolean` | `false` | Log info traces (Logcat, Xcode console, browser console); warnings and errors always log |
|
|
852
|
+
| `onLoad` | `({ nativeEvent: { width, height, frameCount, frameRate, duration } }) => void` | — | Metadata is known |
|
|
853
|
+
| `onStateChange` | `({ nativeEvent: { state } }) => void` | — | `state` is `"loading"`, `"ready"`, `"playing"`, `"paused"` or `"stopped"` |
|
|
854
|
+
| `onFrameUpdate` | `({ nativeEvent: { frame, totalFrames } }) => void` | — | After a seek, and for every frame with `enableFrameEvents` |
|
|
855
|
+
| `onFinished` | `() => void` | — | A non-looping video reached its last frame |
|
|
856
|
+
| `onError` | `({ nativeEvent: { error, code? } }) => void` | — | Loading or playback failed. `code` is set for errors an app may handle: `'NO_SOURCE_MODULE'` on Android and iOS ([Without range-source](#without-range-source)), `'ASTC_UNSUPPORTED'` or `'WEBGL_UNSUPPORTED'` on the web |
|
|
857
|
+
|
|
858
|
+
The events have the same shape on Android, iOS and web. Playback is controlled with props (`paused`, `autoPlay`, a new `source`) and with the ref (see [Usage](#usage)).
|
|
859
|
+
|
|
860
|
+
---
|
|
861
|
+
|
|
862
|
+
## Repository layout
|
|
863
|
+
|
|
864
|
+
| Path | What | Package |
|
|
865
|
+
|---|---|---|
|
|
866
|
+
| `android/` | The Android library: `AlphaVideoGpuView`, source loading, and the JNI code for zstd, reading through range-source or the basic download, the software ASTC decoder and texture uploads (`src/main/cpp`). Builds on its own (`./gradlew` in that folder) | `dev.mediacache:alpha-video-gpu` |
|
|
867
|
+
| `ios/Sources/AlphaVideoGPU/` | The iOS library: `AlphaVideoGpuView` (Objective-C++) and source loading, with its public header in `include/`. `ios/Tests` has its tests with and without range-source | SwiftPM `AlphaVideoGPU` (`Package.swift`), CocoaPods `alpha-video-gpu` (`alpha-video-gpu.podspec`) |
|
|
868
|
+
| `third_party/zstd/` | zstd 1.4.9 (single-file library), compiled into both native libraries | |
|
|
869
|
+
| `third_party/range-source/` | The headers of [range-source](https://github.com/MediaCacheDev/range-source): its C interface, which the players find the add-on through at runtime, and the basic input. `scripts/vendor_range_source.sh` updates it | |
|
|
870
|
+
| `react-native/` | `src/` (the component), `android/` (the view manager; its Gradle module compiles `android/` too), `ios/` (a subclass of the iOS view with React props) | npm `react-native-alpha-video-gpu` (`package.json`, `react-native-alpha-video-gpu.podspec`) |
|
|
871
|
+
| `web/` | The WebGL player for browsers, with its React wrapper. `npm run build` there builds `dist/` | npm `alpha-video-gpu-web` |
|
|
872
|
+
| `scripts/publish-android.sh` | `local` publishes the Android library to `~/.m2`; `central` signs it and uploads it to the Maven Central Portal (see the script for the variables) | |
|
|
873
|
+
|
|
874
|
+
A clone builds with no setup step, since the decoders are in the repo (`third_party/` for the native libraries, `web/src/` for the web package):
|
|
875
|
+
|
|
876
|
+
```bash
|
|
877
|
+
cd web && npm ci && npm run build # web: dist/
|
|
878
|
+
cd android && ./gradlew assembleRelease # Android: the AAR (needs the Android SDK and NDK)
|
|
879
|
+
xcodebuild -scheme AlphaVideoGPU -destination 'generic/platform=iOS Simulator' build # iOS, from the repo root
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
A release is the tag `v<version>`: Swift Package Manager resolves it, and `alpha-video-gpu.podspec` fetches its sources from it. After pushing the tag, `pod trunk push alpha-video-gpu.podspec` (only while trunk accepts new versions). [range-source](https://github.com/MediaCacheDev/range-source) is released first on every channel, since the players depend on it. Publish `alpha-video-gpu-web` to npm before `react-native-alpha-video-gpu`, which depends on it.
|
|
883
|
+
|
|
884
|
+
## Tests
|
|
885
|
+
|
|
886
|
+
The fixtures in `tests/fixtures/` are test cards from the tester app, encoded to KTX2 with 4×4, 6×6 and 8×8 ASTC blocks, as one zstd stream and with a seek table, and described in `expected.json`. Every frame carries its index as a row of cells in the colour and in the alpha, beside colour swatches and an alpha ramp, so a test can tell which frame it got and how far off its colours are. They're copies of a few of the tester app's cards (`npm run testcards` in [alpha-video-test](https://github.com/MediaCacheDev/alpha-video-test)); regenerate them there.
|
|
887
|
+
|
|
888
|
+
| Where | Command | What it checks |
|
|
889
|
+
|---|---|---|
|
|
890
|
+
| Web, under Node | `cd web && npm test` | The KTX2 parser, the seek table, streaming reads and seeks in zstd and uncompressed files, and every frame of the fixtures through the software ASTC decoder, read back by its frame code |
|
|
891
|
+
| Android, on the host | `cd android && ./gradlew testReleaseUnitTest` | The seek table parser (JUnit) |
|
|
892
|
+
| iOS, on the host | `clang -O1 -Ithird_party -Iios/Sources/AlphaVideoGPU ios/Tests/zstd_seek_test.c third_party/zstd/zstd.c -o /tmp/zstd_seek_test && /tmp/zstd_seek_test` | The seek table, reads and seeks (`zstd_seek.h`), from the repository root |
|
|
893
|
+
| iOS simulator | `xcodebuild test -scheme AlphaVideoGPU -destination 'platform=iOS Simulator,name=iPhone 16 Pro'`, and the same with `-scheme WithoutRangeSource-Package` in `ios/Tests/WithoutRangeSource` | With range-source (`SourceTests`), a data source registered with it. Without it (a package depending on this one with `traits: []`), a file plays and a data source reports `NO_SOURCE_MODULE`. `RANGE_SOURCE_PATH` points them at a range-source checkout instead of its release |
|
|
894
|
+
| Web, in a browser | `web/test/browser/index.html`, see below | Every streaming and memory mode on every fixture in a real browser: frame codes after seeks and in playback, from the canvas |
|
|
895
|
+
| Android and iOS devices | the tester app's `test:pixels` and `test:streaming` | Every mode and memory strategy on every card, pixel by pixel, and streaming; see below |
|
|
896
|
+
|
|
897
|
+
The browser page isn't automated. Build `web/dist/` (`cd web && npm run build`), serve this repository over HTTP, and open `web/test/browser/index.html`; results show on the page and in `window.results`. range-source's test server supports range requests, which the streaming modes use:
|
|
898
|
+
|
|
899
|
+
```bash
|
|
900
|
+
cd ../range-source/web && npm run serve -- <path to this repo> --port 8766
|
|
901
|
+
# then open http://localhost:8766/web/test/browser/index.html
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
`?astc=off` hides `WEBGL_compressed_texture_astc` to test the software decoder, `?only=<regex>` picks cases, `?cards=<path>` plays other cards (the tester app's include more sizes and layouts), and `?storage=`, `?replay` and `?input=` exercise storage and sources (see the [web README](web/README.md#testing)). Add `--no-range` to the server for one without range support.
|
|
905
|
+
|
|
906
|
+
On Android and iOS the tester app plays the cards through the React Native component. In [alpha-video-test](https://github.com/MediaCacheDev/alpha-video-test#tests), with this repo's tarball installed and a Release build on an emulator, phone or simulator:
|
|
907
|
+
|
|
908
|
+
```bash
|
|
909
|
+
npm run test:pixels -- --platform android --player gpu # default (VRAM streaming), stream ram and preload, on every card
|
|
910
|
+
npm run test:pixels -- --platform ios --player gpu
|
|
911
|
+
npm run test:pixels -- --platform ios --player gpu --only 'astc4x4_seek' --frames 0,37 # one fixture, two frames
|
|
912
|
+
npm run test:pixels -- --platform android --player gpu --playback # playback and looping
|
|
913
|
+
npm run test:streaming -- --platform android --player gpu # range requests, seek table, storage, headers, Media3 data source
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
The tester app installs `react-native-range-source`. Without it (`npm uninstall react-native-range-source`, then rebuild), `test:streaming` checks the basic input instead: one download per play, and `NO_SOURCE_MODULE` for a data source.
|
|
917
|
+
|
|
918
|
+
`npm pack` here (and in `web/`) makes the tarballs the tester app installs; rebuild its app after installing them. Everything, in order:
|
|
919
|
+
|
|
920
|
+
```bash
|
|
921
|
+
(cd web && npm test)
|
|
922
|
+
(cd android && ./gradlew testReleaseUnitTest)
|
|
923
|
+
clang -O1 -Ithird_party -Iios/Sources/AlphaVideoGPU ios/Tests/zstd_seek_test.c third_party/zstd/zstd.c -o /tmp/zstd_seek_test && /tmp/zstd_seek_test
|
|
924
|
+
xcodebuild test -scheme AlphaVideoGPU -destination 'platform=iOS Simulator,name=iPhone 16 Pro'
|
|
925
|
+
(cd ios/Tests/WithoutRangeSource && xcodebuild test -scheme WithoutRangeSource-Package -destination 'platform=iOS Simulator,name=iPhone 16 Pro')
|
|
926
|
+
# the browser page, with ?astc=off too; then, in alpha-video-test: test:pixels and test:streaming with --player gpu on each platform
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
## Requirements
|
|
930
|
+
|
|
931
|
+
- React Native 0.71+
|
|
932
|
+
- Android: minSdkVersion 24, OpenGL ES 3.0; compileSdkVersion comes from the app (default 36)
|
|
933
|
+
- iOS: iOS 13.0+, Metal
|
|
934
|
+
- Web: WebGL 2; ASTC on the GPU needs `WEBGL_compressed_texture_astc`, and without it WebAssembly
|
|
935
|
+
|
|
936
|
+
## Demo and related packages
|
|
937
|
+
|
|
938
|
+
[alpha-video-test](https://github.com/MediaCacheDev/alpha-video-test) is a React Native app (Android, iOS and web) that plays the same video in each of these formats side by side, with FPS, CPU and memory readouts, so you can compare them on your own device.
|
|
939
|
+
|
|
940
|
+
- [alpha-video-gpu-encoder](https://github.com/MediaCacheDev/alpha-video-gpu-encoder): makes the KTX2 files for this player (`texenc`).
|
|
941
|
+
- [alpha-video](https://github.com/MediaCacheDev/alpha-video): WebM VP9 and AV1 with alpha, decoded in software by a shared C++ core or in hardware by MediaCodec on Android. The smallest files.
|
|
942
|
+
- [alpha-video-ffmpegkit](https://github.com/MediaCacheDev/alpha-video-ffmpegkit): the first attempt at WebM alpha, on FFmpegKit, kept as a reference.
|
|
943
|
+
- [react-native-apng-avif](https://github.com/MediaCacheDev/react-native-apng-avif): animated APNG and AVIF with alpha.
|
|
944
|
+
- [alpha-glow-button](https://github.com/MediaCacheDev/alpha-glow-button): buttons whose glow is a transparent video.
|
|
945
|
+
|
|
946
|
+
---
|
|
947
|
+
|
|
948
|
+
## License
|
|
949
|
+
|
|
950
|
+
The code in this repository is MIT. The ASTC decoder is Apache-2.0, and zstd is BSD-3-Clause; see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|