Compare transparent formats in the demo app
Transparent video that plays everywhere: Android, iOS, React Native and every modern browser, including browsers that can't show transparent video themselves. Safari drops WebM's alpha channel, and no browser shows the alpha track of a dual-stream file. If you want transparent video and need to be confident it plays on every platform, this is the package to use.
It plays VP8 or VP9 WebM with alpha (standard YUVA WebM, what FFmpeg and libvpx write) and dual-stream VP8, VP9 or AV1 (colour and alpha in separate video tracks). Each platform tries the fastest way first and falls back until one works:
- Android: two MediaCodec decoders, one for colour and one for alpha, in hardware where the device has them. Then MediaCodec's software decoders, then the C++ core.
- iOS: the C++ core, which demuxes with libwebm, decodes with libvpx (VP8, VP9) and dav1d (AV1), and draws with Metal.
- Web: the browser's own
<video>where it keeps the alpha, then WebCodecs, then the same decoders compiled to WebAssembly (how it works).
One repository, four packages. The software decoders are codec modules, so an app ships only the ones it uses:
| Package | For | Install |
|---|---|---|
react-native-alpha-video (npm) |
React Native, and react-native-web | React Native |
dev.mediacache:alpha-video (Maven Central), with alpha-video-vpx and alpha-video-av1 |
Android | Android |
alpha-video (Swift Package Manager: products AlphaVideoVPX, AlphaVideoAV1; CocoaPods: alpha-video) |
iOS | iOS |
alpha-video-web (npm) |
Browsers: plain JS/TS, or React | Web, web/README.md |
See it next to the other formats in the demo app.
Both play transparent videos on Android, iOS and the web, and they suit different jobs. alpha-video plays video codecs. alpha-video-gpu doesn't decode anything: its frames are ASTC textures, which the GPU samples as they are.
| alpha-video (this package) | alpha-video-gpu | |
|---|---|---|
| Format | WebM: VP8 or VP9 with alpha, or dual-stream VP8, VP9 or AV1 | KTX2: an ASTC texture array, one layer per frame |
| Made with | ffmpeg, or any tool that exports alpha WebM | alpha-video-gpu-encoder (texenc) |
| Decoding | Every frame: libvpx or dav1d on the CPU, or MediaCodec on Android | None |
| CPU and battery | A decode per frame, or one pass up front with mode="preload" |
Near zero |
| What the device needs | Nothing for software decoding. Hardware decoding needs a VP8, 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. That covers more devices than VP9 or AV1 hardware decoding |
| Many players at once | Limited by the CPU, or by the hardware decoder's instances | Dozens: each one is just a texture |
| File size | Smallest | Several times larger |
| Seeking | Decodes from the previous keyframe | Any frame directly |
| Web | Every modern browser, Safari included: <video>, WebCodecs or WebAssembly decoding (alpha-video-web) |
WebGL where the browser has ASTC |
| 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 |
alpha-video-ffmpegkit was the first attempt at transparent WebM, built on FFmpegKit. It still works and stays as a reference implementation, but alpha-video does everything it does, and more.
- VP8 and VP9 Alpha Transparency - Full support for WebM videos with embedded YUVA alpha channel, VP8 or VP9 alike
- Hardware Decode on Android - Color and alpha decoded by two MediaCodec instances, from a standard alpha WebM (the alpha is a second VP8 or VP9 stream in each block's BlockAdditional) or from separate VP8, VP9 or AV1 tracks
- AV1 Support - Dual-stream AV1, 8- or 10-bit, on Android and iOS: MediaCodec on Android (in hardware where the device has an AV1 decoder), dav1d in the C++ core elsewhere
- High Performance - Native C++ core (libvpx, dav1d) with GPU-accelerated rendering
- iOS - Metal-based rendering with YUVA-to-RGBA GPU shader
- Android - TextureView with OpenGL ES 2.0, proper z-ordering and ScrollView support
- Web -
alpha-video-webshows the alpha in every modern browser:<video>where the browser keeps it, otherwise WebCodecs or WebAssembly decoding drawn in WebGL2 (see Web). On react-native-web,AlphaVideoViewuses it - Zero-Copy Frame Pipeline - Efficient memory usage with LRU frame caching (software path) and OES texture compositing (hardware path)
The player picks the decode path itself. On Android it uses MediaCodec when decoders that list the file's VP8, VP9 or AV1 profiles have 2 instances free, hardware first and then Google's software ones, and the C++ core otherwise (a Snapdragon 778G's VP9 decoder allows 3 instances, so one hardware player at a time). The decoder prop forces an engine. iOS always uses the C++ core.
| Software path (C++ core) | Hardware path (MediaCodec, Android) | |
|---|---|---|
| Files | Single-stream YUVA VP8 or VP9; dual-stream VP8, VP9 (8-bit) or AV1 (8- to 12-bit) | Single-stream YUVA VP8 or VP9; dual-stream VP8, VP9 or AV1 |
| Decoding | CPU (libvpx for VP8 and VP9, dav1d for AV1) | GPU (MediaCodec), or MediaCodec's software decoders |
| CPU usage during playback | Depends on mode: none once preload has decoded every frame (upload and render only); continuous decoding in stream; first pass only in streamAndPreload |
Near-zero (decode offloaded) |
| Looping | Seamless | Seamless |
| Max resolution | ~720p practical limit | 1080p+ (hardware limited) |
| Best for | Short loops, small videos, many simultaneous players | Long-duration, high-res, single-instance playback |
| iOS support | Yes (Metal) | No (Android only) |
| Codec | VP8, VP9 or AV1 | VP8, VP9 or AV1 |
| Instances | Unlimited (CPU bound) | 2 decoder instances each, of the decoder's limit (3 on a Snapdragon 778G, often more); further players use MediaCodec's software decoders, then the software path |
Rule of thumb: Standard YUVA WebM plays everywhere, including in Chrome and Firefox, and gets the hardware path on Android when it's free. Use dual-stream for AV1, or to encode alpha at its own quality; browsers show only its colour track.
One package per platform, all built from this repository.
The player is a core plus codec modules. The core demuxes, reads files by range, keeps the stored copy, runs the frame pipeline and draws; on Android it also drives MediaCodec. Each codec module adds one software decoder for the C++ core: vpx (libvpx, for VP8 and VP9) and av1 (dav1d).
| Core | vpx | av1 | |
|---|---|---|---|
| React Native | react-native-alpha-video |
both built in by default; codecs in the app's package.json picks |
|
| Android | dev.mediacache:alpha-video |
dev.mediacache:alpha-video-vpx |
dev.mediacache:alpha-video-av1 |
| iOS, Swift Package Manager | (no product of its own) | product AlphaVideoVPX |
product AlphaVideoAV1 |
| iOS, CocoaPods | alpha-video/Core |
alpha-video/VPX |
alpha-video/AV1 |
| Web | alpha-video-web |
loaded on its own, only for a VP8 or VP9 stream decoded in software | the same, for AV1 |
Which to take:
- iOS needs at least one. It has no platform decoder for these files, so the core alone plays nothing. Take vpx for VP8 and VP9 (standard alpha WebM is one of them) and av1 for AV1.
pod 'alpha-video'and React Native without a setting give both. - Android plays through MediaCodec without any: VP8 and VP9 on every device (Android's own software decoders), AV1 on Android 10 and later. A codec module is needed for the cases MediaCodec can't take: VP9 profile 1 (4:4:4, what a
gbrpalpha track encodes to), AV1 on Android 7 to 9, more players at once than MediaCodec has instances for, anddecoder="software".mode="preload"and"streamAndPreload"take effect on the C++ core only; MediaCodec streams. Apps with many players at once, or that rely on preload, should add vpx.
A file whose codec has no module reports onError with the code NO_DECODER, and a message naming the module, e.g. The colour track can't play: no AV1 decoder is installed (the av1 codec module). The modules register themselves: nothing to call. They're released with the core, at the same version; a module from another release is refused with a message. The registered decoders are listed by AlphaVideoCodecs.softwareDecoders() on Android (and getDecoderStatus()'s softwareDecoders in React Native) and AlphaVideoCodecs.softwareDecoders on iOS, e.g. libvpx v1.14.0. Another decoder can be added through the C interface in cpp/AlphaVideoDecoderPlugin.h and AlphaVideoCodecs.register(...) (Android) or registerPluginList:error: (iOS).
npm, package react-native-alpha-video, with range-source for URLs:
npm install react-native-alpha-video react-native-range-sourcereact-native-range-source reads URLs by range, keeps them on disk or in memory, and reads 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). One install serves react-native-alpha-video-gpu too.
iOS: install the pods. The view builds from the podspec inside the npm package.
cd ios && pod installAndroid: autolinking adds the library, so there's nothing else to do. Its native library is built for arm64-v8a, armeabi-v7a and x86_64. If your app also ships 32-bit x86, limit it to those three in android/app/build.gradle:
android {
defaultConfig {
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64'
}
}
}import { AlphaVideoView } from 'react-native-alpha-video';
<AlphaVideoView
source={{ uri: 'https://example.com/video.webm' }}
cacheKey="videos/intro.webm" // what the stored copy is named; default, the URL
storage="auto" // where the bytes are kept between plays; see Streaming and storage
maxStorageBytes={512 * 1024 * 1024} // the store's cap, over every clip in it
style={{ width: 300, height: 300 }}
autoPlay={true} // the default; false shows the first frame and waits for play()
loop={true} // the default; false plays once, then calls onEnd
paused={false} // a hold, for example while the screen is hidden
playbackSpeed={1.0} // changeable while playing; doesn't move the playhead
decoder="auto" // Android only; see Choosing the Decoder
mode="stream" // decodes a few frames ahead (default), or 'preload', 'streamAndPreload'
streamPrefetchAhead={8} // 'stream': frames kept decoded ahead; see Buffering
useDecodeQueue={true} // 'preload' and 'streamAndPreload': one clip decodes at a time, app-wide
verbose={false} // true logs lifecycle and per-frame events; errors always log
onLoad={({ totalFrames, width, height, decoder }) => console.log(totalFrames, decoder)}
onError={({ error }) => console.error(error)}
onEnd={() => console.log('played once')}
/>The example sets every option; Options says when each one takes effect.
The npm package carries the prebuilt libvpx and dav1d libraries, so nothing else is downloaded. Both codecs are built in unless the app's package.json lists the ones to keep:
{
"react-native-alpha-video": { "codecs": ["vpx"] }
}The Android build and pod install both read it (Expo prebuild too), and print the codecs they build in. Run pod install again after changing it. "codecs": [] builds in neither: Android then plays through MediaCodec alone, and on iOS pod install warns, as every video would report NO_DECODER. A name other than vpx or av1 stops the build. Only the codecs built in add to the app's size.
The package works on both React Native architectures: the new one (a Fabric view, and a TurboModule for the decoder status API; 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.
Expo:
- Expo Go: not supported, as the package has native code.
- A development build or EAS Build: supported. Run
npx expo prebuild(EAS Build does it for you), so the pods and Gradle dependencies are compiled in.
Maven Central, artifact dev.mediacache:alpha-video:
dependencies {
implementation 'dev.mediacache:alpha-video:1.0.0'
implementation 'dev.mediacache:alpha-video-vpx:1.0.0' // libvpx: VP8 and VP9 in software
implementation 'dev.mediacache:alpha-video-av1:1.0.0' // dav1d: AV1 in software
}The core alone plays through MediaCodec; each codec module adds a software decoder (Codecs says when it's needed). Each is its own AAR with its own native library, and registers itself when it's on the classpath, R8 keep rules included. Keep the three at one version. Native code is built for arm64-v8a, armeabi-v7a and x86_64. Android 7.0 (API 24) or later.
The core brings 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):
implementation('dev.mediacache:alpha-video:1.0.0') {
exclude group: 'dev.mediacache', module: 'range-source'
}To read videos through a Media3 DataSource.Factory, add dev.mediacache:range-source-media3 (Data sources).
dev.mediacache.alphavideo.AlphaVideoView is a TextureView. It takes the same sources and options as the React Native component, through setters:
val view = AlphaVideoView(context)
view.setCacheKey("videos/intro.webm") // what the stored copy is named; default, the URL
view.setStorage("auto") // where the bytes are kept between plays; see Streaming and storage
view.setMaxStorageBytes(512L * 1024 * 1024) // the store's cap, over every clip in it
view.setAutoPlay(true) // the default; false shows the first frame and waits for play()
view.setLoop(true) // the default; false plays once, then calls onEnd
view.setPaused(false) // a hold, for example while the screen is hidden
view.setPlaybackSpeed(1.0) // changeable while playing; doesn't move the playhead
view.setDecoder("auto") // see Choosing the Decoder
view.setMode("stream") // decodes a few frames ahead (default), or "preload", "streamAndPreload"
view.setStreamPrefetchAhead(8) // "stream": frames kept decoded ahead; see Buffering
view.setUseDecodeQueue(true) // "preload" and "streamAndPreload": one clip decodes at a time, app-wide
view.setVerbose(false) // true logs lifecycle and per-frame events to Logcat
view.setCallback(object : AlphaVideoView.PlayerCallback {
override fun onLoad(totalFrames: Int, width: Int, height: Int, decoder: String) {}
override fun onError(error: String, code: String?) {} // code: AlphaVideoView.ERROR_NO_DECODER, or null
override fun onEnd() {}
override fun onPreloadComplete(frames: Int, timeMs: Double) {} // every frame decoded
})
view.setSource("https://example.com/video.webm") // loads on the next main-thread turn
container.addView(view)
// When the view is gone for good. Detaching keeps the player so the view can be re-attached.
view.release()Control playback with play(), pause(), stop(), seekToFrame(frame) and seekToTime(seconds), and hold it with setPaused(paused), as described under Starting and pausing; seeks behave as described under Ref Methods. Callbacks arrive on the main thread. http(s) sources need the INTERNET permission in the app's manifest.
CocoaPods, pod alpha-video, with both codecs:
pod 'alpha-video', '1.0.0'
# or one codec, with range-source:
pod 'alpha-video', '1.0.0', :subspecs => ['VPX', 'Source']Swift Package Manager, package alpha-video, a product per codec, each with the core:
.package(url: "https://github.com/MediaCacheDev/alpha-video", from: "1.0.0")
// in the target's dependencies, one or both:
.product(name: "AlphaVideoVPX", package: "alpha-video"),
.product(name: "AlphaVideoAV1", package: "alpha-video"),The module is AlphaVideo either way. libvpx and dav1d come prebuilt, each with its codec.
Both bring 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), name the pod's subspecs without it, :subspecs => ['VPX', 'AV1']. 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.
AlphaVideoView is a UIView:
import AlphaVideo
let view = AlphaVideoView(frame: bounds)
view.delegate = self // see below
view.cacheKey = "videos/intro.webm" // what the stored copy is named; default, the URL
view.storage = "auto" // where the bytes are kept between plays; see Streaming and storage
view.maxStorageBytes = 512 * 1024 * 1024 // the store's cap, over every clip in it
view.autoPlay = true // the default; false shows the first frame and waits for play
view.loop = true // the default; false plays once, then calls didEnd
view.paused = false // a hold, for example while the screen is hidden
view.playbackSpeed = 1.0 // changeable while playing; doesn't move the playhead
view.mode = "stream" // decodes a few frames ahead (default), or "preload", "streamAndPreload"
view.streamPrefetchAhead = 8 // "stream": frames kept decoded ahead; see Buffering
view.useDecodeQueue = true // "preload" and "streamAndPreload": one clip decodes at a time, app-wide
view.verbose = false // true logs lifecycle and per-frame events to the Xcode console
// No decoder setting: iOS always decodes in software, with the C++ core
view.source = "https://example.com/video.webm" // loads on the next main-queue turn
addSubview(view)
extension MyViewController: AlphaVideoViewDelegate {
func alphaVideoView(_ view: AlphaVideoView, didLoadFrames totalFrames: Int32, width: Int32, height: Int32) {}
func alphaVideoView(_ view: AlphaVideoView, didFailWithError error: String, code: String?) {} // code: AlphaVideoErrorNoDecoder, or nil
func alphaVideoViewDidEnd(_ view: AlphaVideoView) {}
}Control playback with play, pause, stop, seekToFrame: and seekToTime:, and hold it with paused, as described under Starting and pausing; seeks behave as described under Ref Methods. Delegate calls arrive on the main queue. iOS 13 or later.
import { useRef } from 'react';
import { AlphaVideoView, AlphaVideoViewRef } from 'react-native-alpha-video';
function MyComponent() {
const player = useRef<AlphaVideoViewRef>(null); // play, pause, stop, seekToFrame, seekToTime
return (
<AlphaVideoView
ref={player}
source={{ uri: 'https://example.com/video.webm' }}
cacheKey="videos/intro.webm" // what the stored copy is named; default, the URL
storage="auto" // where the bytes are kept between plays; see Streaming and storage
maxStorageBytes={512 * 1024 * 1024} // the store's cap, over every clip in it
style={{ width: 300, height: 300 }}
autoPlay={true} // the default; false shows the first frame and waits for play()
loop={true} // the default; false plays once, then calls onEnd
paused={false} // a hold, for example while the screen is hidden
playbackSpeed={1.0} // changeable while playing; doesn't move the playhead
decoder="auto" // Android only; see Choosing the Decoder
mode="stream" // decodes a few frames ahead (default), or 'preload', 'streamAndPreload'
streamPrefetchAhead={8} // 'stream': frames kept decoded ahead; see Buffering
useDecodeQueue={true} // 'preload' and 'streamAndPreload': one clip decodes at a time, app-wide
verbose={false} // true logs lifecycle and per-frame events; errors always log
onLoad={({ totalFrames, width, height, decoder }) => console.log(totalFrames, decoder)}
onError={({ error }) => console.error(error)}
onEnd={() => console.log('played once')}
/>
);
}The player automatically detects whether a file contains a single YUVA stream or dual video streams, and on Android whether MediaCodec can decode it (see Decision Matrix). No prop changes are needed; decoder only overrides the choice on Android.
autoPlay sets how a video starts. Off, it loads, shows its first frame and waits for play(). paused is a hold to turn on and off, for example while the screen is hidden or the app is in the background. A video that starts paused also loads and shows its first frame, and unpausing plays it when autoPlay is on. With autoPlay off, unpausing leaves it where it is; call play().
The examples under Installation and Usage set every option, each to its default. paused, playbackSpeed and verbose change at once; the others are read when a source loads, so set them before the source.
iOS has no decoder: it always decodes in software. The prefetch and queue settings apply to the C++ core's software decoding; MediaCodec on Android streams with its own buffers and ignores them, and so does the web, where the browser plays the file. On the web mode, decoder, source.dataSource and the stream and queue props have no effect (see Web).
| Prop | Type | Default | Description |
|---|---|---|---|
source |
{ uri: string, headers?: Record<string, string>, dataSource?: string } |
required | Video source URL or local file path (see Supported Source Types). headers go with every request for it; dataSource names a registered data source to read it through. On the web also a Blob or a byte source (Web) |
cacheKey |
string |
the URL | What the store keeps the clip under, for URLs that change while the file doesn't (presigned URLs) |
storage |
'auto' | 'disk' | 'memory' | 'none' |
'auto' |
Where the bytes are kept between plays: 'auto' is 'disk' for URLs and 'none' for a data source (see Streaming and storage) |
maxStorageBytes |
number |
512 MB | The store's cap, over every clip in it, least recently played out first |
autoPlay |
boolean |
true |
Start playing once loaded. Off, the first frame shows and waits for play() |
loop |
boolean |
true |
Loop video playback |
paused |
boolean |
false |
Hold playback. Unpausing plays on when autoPlay is on |
playbackSpeed |
number |
1.0 |
Playback speed multiplier. Changeable while playing, and does not move the playhead — see below |
style |
ViewStyle |
- | Container style. On Android backgroundColor is ignored (the view is a TextureView); put a background on a parent view |
onLoad |
function |
- | Callback when video loads ({ totalFrames, width, height, decoder }); decoder names the engine and decoders, e.g. 'mediacodec-hw (c2.qti.vp9.decoder)', 'software (libvpx)', 'software (dav1d)'; on the web 'browser', 'webcodecs (vp09.00.41.08)' or 'software (vp9)' (vp8 for VP8) |
onError |
function |
- | Callback when error occurs ({ error, code }). code is 'NO_DECODER' when no codec decodes the file, on the web e.g. 'NO_ALPHA_PATH' or 'NEEDS_SERVICE_WORKER', else undefined |
onEnd |
function |
- | Callback when playback reaches the end with loop off |
mode |
'preload' | 'stream' | 'streamAndPreload' |
'stream' |
Playback mode (software path only, see below) |
decoder |
'auto' | 'mediacodec' | 'mediacodec-hw' | 'mediacodec-sw' | 'software' |
'auto' |
Android: the decoding engine (see Choosing the Decoder). iOS accepts it and uses the C++ core; the web has webDecoder |
webDecoder |
'auto' | 'browser' | 'webcodecs' | 'webcodecs-hw' | 'webcodecs-sw' | 'software' |
'auto' |
Web: the engine (see Web). 'browser' is a plain <video>; a forced engine reports onError rather than fall back |
streamPrefetchAhead |
number |
8 |
Frames kept decoded ahead of the shown one in stream mode, 1 or more: a safety margin that costs memory (see Buffering) |
useDecodeQueue |
boolean |
true |
In preload and streamAndPreload mode, decode one clip at a time across all players, so many at once don't saturate the CPU (see Global Decode Queue) |
verbose |
boolean |
false |
Log lifecycle and per-frame events (Logcat, the Xcode console); errors always log |
playbackSpeed can change at any time, including mid-playback, and only affects how long each
frame is shown. The playhead stays exactly where it was:
// Speeds the video up from wherever it currently is. No seek, no restart.
<AlphaVideoView source={source} playbackSpeed={hovered ? 2 : 1} />That distinction matters for anything driven by interaction. A rate change that also reset the position would read as a glitch rather than as an acceleration.
Values at or below zero are clamped to 0.01 rather than stopping playback — use paused to
stop.
A button that plays a multi-state glow sheet (default, hover and press stacked as rows in one
file) lives in its own package, alpha-glow-button.
Its alpha-glow-button/alpha-video entry point uses this player, and relies on the live
playbackSpeed above to change speed per state without restarting the loop.
import { AlphaVideoView, AlphaVideoViewRef } from 'react-native-alpha-video';
const playerRef = useRef<AlphaVideoViewRef>(null);
playerRef.current?.play();
playerRef.current?.pause();
playerRef.current?.stop();
playerRef.current?.seekToFrame(30); // frame number, from 0
playerRef.current?.seekToTime(1.5); // seconds; the frame showing at that timeSeeking works in every mode and on both decode paths. The target is clamped to the clip, and the
player keeps playing from there or stays paused showing that frame. In stream mode a seek far
from the playhead jumps the decoder to the nearest keyframe before it, so the wait depends on the
keyframe interval. A seek before onLoad is ignored.
decoder picks the engine on Android: MediaCodec (the platform's decoders, in hardware where the device has them) or the C++ core's own software decoders. iOS has no such setting and always uses the C++ core; the web uses the browser. decoder is read when a source loads, so set it before or together with source. Changing it on its own doesn't reload the video.
<AlphaVideoView source={source} decoder="auto" /> // the default
<AlphaVideoView source={source} decoder="mediacodec-hw" /> // hardware only, or onError| Value | Engine |
|---|---|
'auto' (default) |
Hardware MediaCodec for both tracks; else software MediaCodec for both; else the C++ core ('software') |
'mediacodec' |
MediaCodec, chosen per track: hardware if one is free, else software, so the two tracks can get one of each |
'mediacodec-hw' |
Hardware MediaCodec for both tracks |
'mediacodec-sw' |
Software MediaCodec for both tracks (Google's c2.android.* decoders) |
'software' |
The C++ core's software decoders, from the codec modules: libvpx for VP8 and 8-bit VP9, dav1d for 8- to 12-bit AV1 (drawn at 8 bits) |
'software' is the player's own decoding, the same as on iOS, so it doesn't depend on what the device has, only on the codec modules installed. Without the module for the file's codec, 'auto''s last step and 'software' report onError with NO_DECODER. 'mediacodec-sw' is Android's software decoders, run through MediaCodec.
At each load the player decides in this order:
'software'goes straight to the C++ core.- Can MediaCodec take the file? It needs colour and alpha as separate VP8, VP9 or AV1 bitstreams: standard YUVA WebM (the alpha is a second VP8 or VP9 stream inside each block) or a dual-stream file. If the file has neither (a VP9 track without alpha, say),
'auto'plays it on the C++ core and themediacodecvalues reportonError. - Reserve 2 decoders, one per track. A decoder qualifies if it decodes the track's codec, lists its profile (VP9 profiles 0 to 3, AV1 Main; every VP8 decoder takes all four VP8 versions) and has an instance free. Instances are counted across every player in the app, and decoders cap them: a Snapdragon 778G's VP9 hardware decoder allows 3. Hardware decoders are tried in the order Android lists them.
- Fall back, or not.
'auto'tries MediaCodec hardware for both tracks, then MediaCodec software for both, then the C++ core; it doesn't mix hardware and software. Every other value stops at its own engine and reportsonErrorwhen it can't get one, so a forced engine can be trusted for testing and benchmarks.
A player gives its decoders back when it unloads, or when its view loads a new source, before the new one reserves its own. With the Snapdragon 778G's 3 VP9 hardware instances:
| Players with VP9 files | 'auto' |
'mediacodec' |
|---|---|---|
| First | mediacodec-hw |
mediacodec-hw |
| Second (1 hardware instance left) | mediacodec-sw |
mediacodec: one hardware, one software |
| Third | mediacodec-sw |
mediacodec-sw |
| Once MediaCodec's software instances run out | software |
onError |
onLoad's decoder says what was used: 'mediacodec-hw (c2.qti.vp9.decoder)', 'mediacodec-sw (c2.android.vp9.decoder)', 'mediacodec (c2.qti.vp9.decoder, c2.android.vp9.decoder)' for one of each, 'software (libvpx)' or 'software (dav1d)'. 10-bit VP9 plays only on MediaCodec, since the C++ core decodes 8-bit VP9.
mode applies to the C++ core only; MediaCodec always streams. On a GPU without GL_EXT_YUV_target, frames from MediaCodec's software decoders can show BT.601 colours for BT.709 video, because the driver ignores their colour tag.
Query hardware decoder capabilities at runtime:
import { getDecoderStatus, getDecoderInfo } from 'react-native-alpha-video';
const status = await getDecoderStatus();
console.log(`VP9 HW: ${status.vp9HardwareAvailable}`);
console.log(`AV1 HW: ${status.av1HardwareAvailable}`);
console.log(`Max dual-stream players: ${status.maxDualStreamPlayers}`);
console.log(`Can create more: ${status.canCreateMore}`);
console.log(`Software decoders: ${status.softwareDecoders}`); // the codec modules, e.g. ['libvpx v1.14.0']
// Detailed string dump of all codec capabilities
const info = await getDecoderInfo();
console.log(info);- HTTP/HTTPS URLs:
{ uri: 'https://example.com/video.webm', headers: { Authorization: '…' } }. Read by range requests as it plays, by the platform's HTTP stack (HttpURLConnectionon Android,URLSessionon iOS), and kept on disk between plays. See Streaming and storage; without range-source, downloaded whole. - Anything a data source reads:
{ uri, dataSource: 'name' }, for an app that fetches, caches or decrypts the bytes itself. See Data sources. - Local files:
{ uri: 'file:///path/to/video.webm' }or an absolute path. - Bundled files:
{ uri: 'asset://videos/glow.webm' }. On Android this isandroid/app/src/main/assets/videos/glow.webm. On iOS it isvideos/glow.webminside the app bundle. On web it is/videos/glow.webmfrom the site root. - Android
content://URIs, for example from a document picker. require(): passImage.resolveAssetSource(require('./glow.webm')).uri. In debug builds this is a Metrohttp://localhost:8081URL. In release builds it is a file path on iOS and ares/rawresource name on Android.
Nothing is copied. Android reads asset:// files and res/raw resources directly from the APK, so they must be stored uncompressed. .webm and .mkv are uncompressed by default. For other extensions, add them to android.androidResources.noCompress in the app's build.gradle.
An http(s) source is read by HTTP range requests as it plays, through range-source, whose web package the web player uses too. It's included on Android and iOS, and in React Native it's react-native-range-source (Installation):
- In
streamandstreamAndPreloadmode (and on the Android hardware path in those modes) playback starts with the first frames. Playing from the start is one request, read on as the player needs it; a seek opens another where it lands, after one for the Cues (the seek index, which a WebM file keeps at the end).preloadfetches the whole file first, since it decodes every frame before showing one. - 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.
- A dropped connection is retried 3 times, carrying on from where it stopped. A read that still fails is reported through
onErrorasReading the video failed: …, and the player stops at the last frame it has. - If the network is slower than the clip's bitrate, playback waits for frames on the software path. On the Android hardware path its clock can run ahead during the stall and catch up quickly afterwards.
storage sets where the bytes are kept between plays:
| Value | Kept |
|---|---|
'auto' (default) |
'disk' for a URL, 'none' for a data source |
'disk' |
In the app's cache folder: alpha-video in getCacheDir() on Android, in Caches on iOS. The system may clear it when space runs low |
'memory' |
In memory, for the app's lifetime |
'none' |
Nowhere: each play fetches again |
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. Stored clips are named by cacheKey, else the URL, so a presigned URL that changes on each visit can keep the same copy. 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.
Local files, bundled files and content:// URIs are read in place and never stored.
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 and content:// URIs as before. A URL is downloaded whole, with one request from the start, into a temporary file, and plays as it arrives:
- a seek past what has arrived waits for it;
- loops and seeks back read the file;
- nothing is kept between plays, so a replay downloads it again;
headersare sent.
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 (AlphaVideoView.ERROR_NO_SOURCE_MODULE on Android, AlphaVideoErrorNoSourceModule on iOS), with a message naming the add-on.
An app that fetches, caches or decrypts video 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:
<AlphaVideoView source={{ uri: 'https://cdn.example.com/intro.webm', dataSource: 'media3' }} />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, set setDataSource("name") (Android) or dataSource = "name" (iOS) before the source, with setHeaders or headers.
Data sources are range-source's, so they need it (Without range-source), and one registered once serves this player and alpha-video-gpu:
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:
RangeDataSources.register("mine") { MyDataSource() }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):
implementation 'dev.mediacache:range-source-media3:1.0.0'RangeDataSources.register("media3", Media3RangeDataSource.factory(dataSourceFactory))The built-in fetcher is public too, to register with other timeouts or a user agent: RangeHttpDataSource.Factory().setConnectTimeoutMs(…).setReadTimeoutMs(…).setUserAgent(…).
iOS: conform to RangeDataSource, the role of AVAssetResourceLoaderDelegate without the AVFoundation types. 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:
import RangeSource
final class MyDataSource: NSObject, RangeDataSource {
func read(url: URL, offset: Int64, length: Int64, headers: [String: String],
response: @escaping (RangeDataInfo) -> Void,
data: @escaping (Data) -> Void,
completion: @escaping (Error?) -> Void) -> RangeCancellable {
// response(RangeDataInfo(size: total, etag: etag, lastModified: nil)), data(…), completion(nil)
}
}
RangeDataSources.register(MyDataSource(), name: "mine")React Native: register in native code, as above, in MainApplication (Android) and AppDelegate (iOS, import RangeSource).
alpha-video-web (in web/) plays transparent WebM in every modern browser, whether or not the browser can show transparent video itself. It makes every effort to decode the video: it starts with the fastest way and moves to the next only when one can't show the alpha.
- The browser's own
<video>. The fastest: no JavaScript runs per frame, and the browser streams and caches.canPlayTypecan't tell whether a browser keeps WebM's alpha; Safari answers"probably"and drops it. So the first time a page plays a file, a 599-byte clip is drawn into a canvas and two of its pixels are read.<video>is used when that probe shows alpha and the file is a single VP8 or VP9 track with alpha (Chrome, Edge and Firefox). The first 64 KB of the file tell which kind it is. With range-source's Service Worker registered,<video>reads by range through the same storage as the engines below, sendsheaders, and plays Blobs and byte sources (details). - WebCodecs. The C++ core's demuxer, compiled to WebAssembly, splits the file into its colour and alpha bitstreams, reading it by HTTP range requests as it plays: playback starts with the first frames, and a seek fetches only from where it lands. Between plays the bytes are kept in the browser's storage (OPFS, else IndexedDB), so a clip played through plays again offline. It feeds them to two
VideoDecoders, one per stream, which can use the device's hardware decoders. WebGL2 draws the pair with the same colour maths as the Android and iOS players. This is what plays dual-stream files in every browser, and every alpha WebM in Safari. - Software decoding in WebAssembly. The same demuxer feeds FFmpeg's VP8 and VP9 decoders or dav1d (AV1, through the same adapter as on Android and iOS), each stream in its own Web Worker. Each codec module is a module of its own (vpx for VP8 and VP9, av1), loaded only for streams it decodes. On cross-origin isolated pages each decoder also runs threads of its own. This covers what WebCodecs can't decode, such as VP9 profile 1 (RGB-coded) alpha in Safari and Firefox.
Only a file that no engine can decode ends in an error (NO_ALPHA_PATH). Each WebAssembly module is loaded only when its engine runs, and nothing needs hosting: the modules and Workers are inside the package. Every test card passes through every engine that can decode it, in Playwright's Chromium, Firefox and WebKit; real Safari and phones are still to be tested. The API, the React component and speed figures are in web/README.md.
On react-native-web, AlphaVideoView plays through alpha-video-web, which is installed with this package. iOS and Android bundles don't include it: the component imports it from a .web.tsx file. It honours source (with source.headers, sent with every request; on the web source can also be a Blob or File, or a byte source, { size, read(offset, length, signal) }, kept as the same object between renders), cacheKey (naming the stored copy), storage ('disk' is the browser's storage, as 'auto'), maxStorageBytes, autoPlay, loop, paused, playbackSpeed, webDecoder, onLoad, onError, onEnd and the ref methods. mode, decoder, source.dataSource and the stream and queue props have no effect. asset://path is served from the site root.
- Bundling: the WebAssembly engines are separate chunks, loaded by
import()only when a file needs them. With webpack, Vite or esbuild that needs nothing extra. - Storage for
<video>: copyrange-source-web/dist/range-source-sw.jsto the site's root and callregisterRangeSourceWorker('/range-source-sw.js')fromalpha-video-webbeforeAppRegistry.runApplication(details). - Content-Security-Policy: the software engine's Workers start from
blob:URLs, so a page with a CSP must allowworker-src blob:. - Threads: a cross-origin isolated page (COOP and COEP headers) lets the software decoders run threads (details).
- The old behaviour:
webDecoder="browser"gives a plain<video>. Safari then drops the alpha, and dual-stream files show only their colour track.
┌──────────────┐ ┌───────────────┐ ┌───────────────────────────────────┐
│ React Native│ │ Native Bridge │ │ Decode Backends │
│ TypeScript │◄──►│ iOS / Android │◄──►│ │
│ Component │ │ ViewManager │ │ ┌─────────────┐ ┌─────────────┐ │
└──────────────┘ └───────────────┘ │ │ Software │ │ Hardware │ │
│ │ (C++ core) │ │ (MediaCodec│ │
│ │ YUVA decode │ │ OES texture│ │
│ │ + GPU upload│ │ compositing│ │
│ └─────────────┘ └─────────────┘ │
└───────────────────────────────────┘
Software (C++ core) — Android and iOS:
- libwebm demuxes WebM, libvpx decodes VP8 or VP9 with embedded alpha, or the colour and alpha tracks of a dual-stream VP8 or VP9 file (dav1d those of AV1)
- YUVA420P frames cached in LRU
FrameCache - Uploaded to GPU as 4 textures (Y, U, V, A) per frame
- Fragment shader converts YUVA to RGBA on Metal (iOS) / OpenGL ES (Android)
Hardware (MediaCodec) — Android only:
- libwebm demuxes WebM into color packets (stream 0) and alpha packets (stream 1): the two tracks of a dual-stream file, or each block's main data and its BlockAdditional in a standard alpha WebM
- Two
MediaCodecinstances decode in parallel, outputting toSurfaceTexture(OES textures) - One-frame-per-cycle release gate ensures color and alpha stay in sync
- Fragment shader samples both OES textures:
gl_FragColor = vec4(color.rgb, alpha.r) - Zero CPU involvement in decoding — all work is on the hardware video decoder
C++ Core (WebMAlphaPlayer) — software decode path
- libwebm's parser for demuxing, with the core's own VP8, VP9 and AV1 header parsing
- Decoders from the codec modules, through a C plugin interface (
cpp/AlphaVideoDecoderPlugin.h): libvpx 1.14.0 for VP8 and VP9 (codecs/vpx), dav1d 1.5.1 for AV1 (codecs/av1) - Multithreaded: decode thread, playback thread, render thread
- LRU
FrameCacheprevents re-decoding during loops - Zero-copy pipeline from decoder to GPU
BufferedDualStreamPlayer — hardware decode path (Android)
- Manages demux loop, codec thread, playback loop, and GL thread
- PTS-gated frame release for temporal synchronization
- Handles loop transitions via
MediaCodec.flush()+ demuxer seek
SurfaceTextureVideoDecoder — codec-agnostic MediaCodec wrapper
- Supports VP8 (
video/x-vnd.on2.vp8), VP9 (video/x-vnd.on2.vp9) and AV1 (video/av01) - Opens the decoder
MediaCodecDecodersreserved for its track (see Choosing the Decoder) - Creates OES texture + SurfaceTexture per decoder instance
- One-frame-per-cycle release gate prevents output buffer flooding
AlphaVideoView — Android view
- TextureView-based (proper z-ordering, ScrollView support)
- Dedicated GL thread for EGL context and rendering
- Routes each file to MediaCodec or the C++ core (see Decision Matrix)
- Handles view lifecycle (attach/detach/z-index reorder) gracefully
iOS (Metal) — software decode only
AlphaVideoView: UIView that resolves the source, applies the mode and runs the global decode queueAlphaVideoMetalView: MTKView with YUVA-to-RGBA compute shader- Triple buffering prevents tearing
Web — WebPlayer.web.tsx renders alpha-video-web's React component: <video>, WebCodecs or WebAssembly decoding (see Web)
The mode prop controls the software decode path; the default is stream. The hardware path on Android always streams, whatever the mode.
<AlphaVideoView source={{ uri: 'long-video.webm' }} mode="stream" />
<AlphaVideoView source={{ uri: 'video.webm' }} mode="preload" />
<AlphaVideoView source={{ uri: 'video.webm' }} mode="streamAndPreload" />| Mode | Startup | Memory | Looping | Best For |
|---|---|---|---|---|
stream (default) |
Immediate | Low (cache only) | Re-decode | Long videos, memory-constrained |
preload |
Wait for full decode | High (all frames) | Seamless | Short loops, many simultaneous players |
streamAndPreload |
Immediate | High (after 1st pass) | Seamless | Fast start + smooth loops |
For http(s) sources, "Immediate" means as soon as the first frames have arrived, and preload also waits for the whole file (Streaming and storage).
Key insight: preload mode has zero CPU cost during playback since all frames are already decoded. This means 20+ preloaded videos can play simultaneously with minimal CPU impact (only GPU rendering).
In stream mode the decoder runs ahead of the playhead and pauses once streamPrefetchAhead frames after the shown one are ready. Each frame is dropped once it has been shown, so the player holds streamPrefetchAhead + 2 decoded frames: the shown one, the ones ahead of it, and the previous one until it's dropped. That is the only buffer setting; there is no separate cache size.
The frames ahead are a safety margin. Decoding usually keeps well ahead, but some stretches take longer than a frame's time: keyframes, the jump back to the start 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 decoded. A larger margin rides out longer slow stretches and costs memory, about 2.5 bytes a pixel per frame (the Y, U, V and alpha planes):
| Frame size | One frame | At the default (10 frames) |
|---|---|---|
| 400×400 | 0.4 MB | 4 MB |
| 1080×1080 | 2.9 MB | 29 MB |
| 1920×1080 | 5.2 MB | 52 MB |
The default, 8, is about a quarter of a second at 30 fps. On a Galaxy A52s (Snapdragon 778G) playing a 400×400 clip, the buffer stayed within 1 or 2 frames of full from the second pass on with as many as 16 players at once, from 3 frames up; at 1 and 2, 9 and 16 players ran out and held frames. So 3 to 5 is enough for small clips on a phone like that, and 8 leaves room for larger frames and slower phones. Go higher for large frames on low-end phones, or lower to save memory when many small clips play at once.
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). streamAndPreload and preload decode the whole clip whatever it's set to, and MediaCodec on Android and the web use their own buffers and ignore it.
To tune it, 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 preload or fewer players at once will.
When spawning many players simultaneously in preload or streamAndPreload mode, the library uses a global decode queue to serialize decoding operations. This prevents CPU saturation, UI freezes, and crashes.
// Default behavior - uses global queue (safe for many simultaneous players)
<AlphaVideoView source={{ uri: 'video.webm' }} />
// Opt out of queue (only if you know you have a single player)
<AlphaVideoView source={{ uri: 'video.webm' }} useDecodeQueue={false} />| Scenario | useDecodeQueue |
Why |
|---|---|---|
| Many players spawned at once | true (default) |
Prevents freezing and crashes |
| Single player, fastest load time | false |
No queue overhead |
mode="stream" |
N/A | Queue is automatically bypassed |
| Hardware path (Android) | N/A | Not applicable (MediaCodec handles scheduling) |
ffmpeg -i input.mov -c:v libvpx-vp9 -pix_fmt yuva420p -crf 18 -b:v 0 \
-speed 0 -auto-alt-ref 0 output.webm| Parameter | Description |
|---|---|
-c:v libvpx-vp9 |
VP9 codec with alpha support |
-pix_fmt yuva420p |
YUVA pixel format (includes alpha channel) |
-crf 18 |
Quality (0-63, lower = better; 18-24 is a good range) |
-auto-alt-ref 0 |
Required for alpha |
VP8 works the same way, with -c:v libvpx (FFmpeg's name for libvpx's VP8 encoder). Use it for tools or devices that only take VP8; VP9 compresses better.
ffmpeg -i input.mov -c:v libvpx -pix_fmt yuva420p -crf 10 -b:v 4M -auto-alt-ref 0 output_vp8.webmVP8 has a single colour matrix, BT.601, so the player converts VP8 with BT.601 at any size. Encode HD material for VP8 with -vf scale=out_color_matrix=bt601 or its colours shift slightly.
The dual-stream format stores color and alpha as two independent video streams inside a single WebM container. Stream 0 is the color video, stream 1 is a grayscale video whose red channel is sampled as alpha at render time.
From a single-stream YUVA source (extracts and re-encodes both channels):
# AV1 dual-stream from existing YUVA WebM
ffmpeg -c:v libvpx-vp9 -i input.webm \
-filter_complex "[0:v]format=yuv420p[rgb]; [0:v]alphaextract,format=yuv420p[alpha]" \
-map "[rgb]" -map "[alpha]" \
-c:v:0 libsvtav1 -crf 28 -preset 8 \
-c:v:1 libsvtav1 -crf 28 -preset 8 \
-metadata:s:v:1 title=Alpha \
output_dual_av1.webm
# VP9 dual-stream from existing YUVA WebM
ffmpeg -c:v libvpx-vp9 -i input.webm \
-filter_complex "[0:v]format=yuv420p[rgb]; [0:v]alphaextract,format=yuv420p[alpha]" \
-map "[rgb]" -map "[alpha]" \
-c:v:0 libvpx-vp9 -crf 20 -b:v 0 \
-c:v:1 libvpx-vp9 -crf 20 -b:v 0 \
-metadata:s:v:1 title=Alpha \
output_dual_vp9.webm
# VP8 dual-stream from existing YUVA WebM
ffmpeg -c:v libvpx -i input_vp8.webm \
-filter_complex "[0:v]format=yuv420p[rgb]; [0:v]alphaextract,format=yuv420p[alpha]" \
-map "[rgb]" -map "[alpha]" \
-c:v libvpx -crf 10 -b:v 4M -auto-alt-ref 0 \
-metadata:s:v:1 title=Alpha \
output_dual_vp8.webm| Parameter | Description |
|---|---|
-c:v libvpx-vp9 (input) |
Decode the YUVA source with libvpx so the alpha plane is available (libvpx for a VP8 source) |
format=yuv420p |
Strip alpha from the color stream |
alphaextract,format=yuv420p |
Extract the alpha channel as a grayscale video. yuv420p keeps VP9 at profile 0, which hardware decoders support; format=gray makes libvpx encode gbrp (profile 1), which many don't |
-map "[rgb]" -map "[alpha]" |
Place color as stream 0, alpha as stream 1 |
-c:v:0 / -c:v:1 |
Encode each stream independently (can use different CRF) |
-metadata:s:v:1 title=Alpha |
Optional: tag the alpha stream for identification |
From separate color + alpha files (e.g. from After Effects or Blender):
# AV1 dual-stream
ffmpeg -i color.mp4 -i alpha_grayscale.mp4 \
-map 0:v -map 1:v \
-c:v:0 libsvtav1 -crf 28 -preset 8 \
-c:v:1 libsvtav1 -crf 28 -preset 8 \
output_dual_av1.webm
# VP9 dual-stream
ffmpeg -i color.mp4 -i alpha_grayscale.mp4 \
-map 0:v -map 1:v \
-c:v libvpx-vp9 -crf 20 -b:v 0 \
output_dual_vp9.webmTips:
- AV1 (
libsvtav1) gives better compression than VP9 at the same visual quality, but requires an AV1 MediaCodec on the device. -preset 8is a good speed/quality tradeoff for AV1 encoding. Lower = slower but better compression.- Both streams must use the same codec (both VP8, both VP9 or both AV1). Mixing codecs is not supported.
# Check stream layout
ffprobe -v quiet -print_format json -show_streams output.webm
# Single-stream: look for codec_name=vp9 or vp8, and the tag ALPHA_MODE=1
# Dual-stream: look for 2 video streams (both vp8, both vp9 or both av1)The codec modules ship with prebuilt libvpx 1.14.0 (the VP8 and VP9 decoders) and dav1d 1.5.1 (the AV1 decoder, 8- to 12-bit) static libraries. libwebm's WebM parser is in third_party/libwebm and compiles with the C++ core. To rebuild the libraries:
npm run build:codecs:ios # iOS (device + simulator)
npm run build:codecs:android # Android (arm64-v8a, armeabi-v7a, x86_64)
npm run build:codecs:all # All platformsThe scripts download the sources themselves. dav1d builds with meson and ninja (brew install meson ninja), and the x86_64 builds (Android and the simulator) need nasm. The Android script uses the NDK in ANDROID_NDK_HOME (default $ANDROID_HOME/ndk/27.1.12297006).
| Platform | Output Directory | Architecture |
|---|---|---|
| iOS Device | prebuilt/ios/ |
arm64 |
| iOS Simulator | prebuilt/ios-simulator/ |
arm64 + x86_64 |
| Android | prebuilt/android/{abi}/ |
arm64-v8a, armeabi-v7a, x86_64 |
prebuilt/ is not committed. In a git clone, either rebuild it as above or download the release's copy:
scripts/fetch-prebuilt.sh # prebuilt/ from the GitHub Release, checksum-verifiedscripts/build_xcframework.sh # prebuilt/AlphaVideoLibvpx.xcframework and AlphaVideoDav1d.xcframework (.zip) for Package.swift
scripts/publish-android.sh local # dev.mediacache:alpha-video, -vpx and -av1 into ~/.m2
scripts/publish-android.sh central # signed, to the Maven Central Portal
scripts/package_release.sh # the GitHub Release assets, in dist/release-v<version>/Package.swift uses the xcframeworks in prebuilt/ when they exist, and the release zips otherwise. Their checksums must match the zips attached to the GitHub Release, so rebuilding an xcframework means updating its checksum. The same goes for the prebuilt archive and fetch-prebuilt.sh, although packaging the same libraries again gives the same archive. package_release.sh stops or warns when a checksum is out of date. publish-android.sh lists the environment variables it needs for Central.
A release carries the prebuilt archive, AlphaVideoLibvpx.xcframework.zip, AlphaVideoDav1d.xcframework.zip and the CocoaPods zip (alpha-video-<version>-ios.zip, the source of alpha-video.podspec, whose :sha256 must match it). pod trunk push alpha-video.podspec once the release is public. range-source is released first on every channel, since the player depends on it (the range-source pod before this one).
| Path | Contents |
|---|---|
cpp/ |
The C++ core (libwebm demux, the decoder plugin interface), shared by Android and iOS; the web runs its demuxer in WebAssembly (web/wasm) |
codecs/vpx, codecs/av1 |
The codec modules' adapters to libvpx and dav1d, shared by Android, iOS and (av1) the web |
android/ |
The Android library: AlphaVideoView, the MediaCodec dual-stream path and the JNI build; android/vpx and android/av1 are the codec modules |
ios/Sources/AlphaVideo/ |
The iOS library: AlphaVideoView and the Metal renderer; ios/Sources/AlphaVideoVPX and AlphaVideoAV1 register the codecs |
react-native/ |
The React Native component (src/) and its thin native view managers |
web/ |
alpha-video-web: the browser player, with the C++ demuxer and the decoders in WebAssembly |
Package.swift, react-native-alpha-video.podspec, package.json |
The SwiftPM, CocoaPods and npm packages |
The fixtures in tests/fixtures/ are test cards from the tester app: every frame carries its index as a row of cells in the colour and in the alpha planes, 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); regenerate them there.
| Where | Command | What it checks |
|---|---|---|
| Host (macOS) | npm run test:core |
core_tests: the C++ core on every fixture (demuxing, keyframes, seeks, colour from the bitstream and the container, every frame decoded through the codec modules). codec_registry_tests: the modules registered one at a time, as an app installs them, and NO_DECODER naming the missing module |
| Host, with FFmpeg | tests/core/build/demuxer_compare tests/fixtures/*/*.webm |
The core's libwebm demuxer against an FFmpeg one: the same info, packets and seeks. Built by test:core when FFmpeg 6.1+ is installed |
| iOS simulator | see below | The iOS library with each codec module linked alone: it finds the module by itself, plays its codecs, and reports NO_DECODER for the other's. With range-source, a data source registered with it; without it (tests/ios/WithoutRangeSource), a file plays and a data source reports NO_SOURCE_MODULE |
| Web, under Node | cd web && npm test |
The WebAssembly demuxer on every fixture, also fed one block per fetch (packets, seeks, interruption, a failing source); sniffing; the YUV-to-RGB matrices |
| Web, in browsers | cd web && npm run test:browsers |
Chromium, Firefox and WebKit through Playwright: every engine (<video>, WebCodecs, software) on every fixture, frame codes after seeks, alpha and colour, then playback through a loop. -- --isolated repeats it cross-origin isolated (threaded software decoders); -- chromium picks browsers |
| Web, streaming | cd web && npm run test:streaming |
Range requests and a seek near the end, a server without range support, replay from storage with no requests |
| Android and iOS devices | the tester app's test:pixels and test:streaming |
Every Android decoder (MediaCodec hardware and software, the C++ core) and the iOS core on every card, pixel by pixel, and streaming; see below |
The host tests need CMake, Ninja, pkg-config, libvpx and dav1d (brew install cmake ninja pkg-config libvpx dav1d; add ffmpeg for demuxer_compare), and a range-source checkout beside this repository (or -DRANGE_SOURCE_DIR=), whose reader they link as an app does. The web tests need npm install and npm run build in web/ first, and Playwright's browsers (npx playwright install chromium firefox webkit); test:streaming makes its clip with ffmpeg.
The iOS tests are Swift Package test targets: VPXOnlyTests and AV1OnlyTests each link one codec module alone, so they run one at a time, and SourceTests links range-source. tests/ios/WithoutRangeSource is a package that depends on this one with traits: [], as an app leaving range-source out does. They need the codec xcframeworks: with prebuilt/ in place (see Codec Build), scripts/build_xcframework.sh makes them there, where Package.swift looks first. RANGE_SOURCE_PATH points them at a range-source checkout instead of its release.
for t in VPXOnlyTests AV1OnlyTests SourceTests; do
xcodebuild test -scheme AlphaVideo-Package -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -only-testing:$t
done
(cd tests/ios/WithoutRangeSource && xcodebuild test -scheme WithoutRangeSource-Package -destination 'platform=iOS Simulator,name=iPhone 16 Pro')Android has no tests of its own here: its paths need a device, so the tester app covers them. In alpha-video-test, with this repo's tarball installed and a Release build on an emulator, phone or simulator:
npm run test:pixels -- --platform android --player cpp # every decoder on every card
npm run test:pixels -- --platform ios --player cpp
npm run test:pixels -- --platform android --player cpp --only mediacodec-hw # one decoder
npm run test:pixels -- --platform android --player cpp --playback # playback and looping
npm run test:streaming -- --platform android --player cpp # range requests, storage, headers, Media3 data sourceThe 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.
npm pack here (and in web/) makes the tarballs the tester app installs; rebuild its app after installing them. Everything, in order:
npm run test:core
(cd web && npm run build && npm test && npm run test:browsers && npm run test:browsers -- --isolated && npm run test:streaming)
for t in VPXOnlyTests AV1OnlyTests SourceTests; do xcodebuild test -scheme AlphaVideo-Package -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -only-testing:$t; done
(cd tests/ios/WithoutRangeSource && xcodebuild test -scheme WithoutRangeSource-Package -destination 'platform=iOS Simulator,name=iPhone 16 Pro')
# then, in alpha-video-test: test:pixels and test:streaming with --player cpp on each platformalpha-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.
- alpha-video-gpu: ASTC texture arrays in KTX2, drawn without decoding. The lowest CPU and battery use.
- alpha-video-gpu-encoder: makes the KTX2 files for alpha-video-gpu (
texenc). - alpha-video-ffmpegkit: the first attempt at WebM alpha, on FFmpegKit, kept as a reference.
- react-native-apng-avif: animated APNG and AVIF with alpha.
- alpha-glow-button: buttons whose glow is a transparent video, played by alpha-video, react-native-apng-avif or a
<video>.
Ideas, not plans:
- Drop FFmpeg on the web too. Android and iOS decode with libvpx and dav1d directly, and the web already demuxes with libwebm. In WebAssembly libvpx is 1.2-1.9x slower than FFmpeg's own VP9 decoder, even with row multithreading, so the web's software engine keeps FFmpeg's VP9 decoder for now.
- Hardware decoding of dual-stream files on iOS. Two VideoToolbox sessions, one per track, composited in the Metal shader, as Android does with MediaCodec, with the C++ core as the fallback. AV1 decodes in hardware on recent Apple chips (A17 Pro, M3 and later). VP9 likely can't, since VideoToolbox's VP9 decoder isn't available to iOS apps.
- Alpha for any codec. Dual-stream playback doesn't depend on the codec: colour and alpha are two ordinary video tracks, decoded separately and composited in the shader. The same handling would add transparency to codecs that have none, such as H.264, on Android, iOS and the web. H.264 decodes in hardware almost everywhere, so a dual-stream H.264 file would play transparent video in hardware on every platform. Each codec needs its container demuxed (MP4 as well as WebM) and a decoder on each platform: MediaCodec on Android, VideoToolbox on iOS, WebCodecs on the web.
- Audio. Decode a WebM's Opus or Vorbis track, keep video in sync with it, and drop frames when decoding falls behind.
- More codecs. AV2 once it's final, in software and through platform decoders where they exist.
- DRM. Unlikely: Widevine and FairPlay decode into protected surfaces, which the alpha shader can't read.
The code in this repository is MIT (see LICENSE).
On Android and iOS the core also contains libwebm's parser (BSD-3-Clause, with the WebM patent grant), the vpx module libvpx (BSD-3-Clause, with the WebM patent grant) and the av1 module dav1d (BSD-2-Clause). Apps built with it must include the notices of what they ship. The web player, alpha-video-web, contains libwebm's parser too, dav1d in its AV1 Workers, and FFmpeg's VP8 and VP9 decoders (LGPL-2.1-or-later) in its vpx Workers; its notices are in web/THIRD_PARTY_NOTICES.md. THIRD_PARTY_NOTICES.md has the versions, build options and source links; the licence texts are in licenses/.
Built with:
- libwebm - WebM demuxing
- libvpx - VP8 and VP9 codecs
- FFmpeg - VP8 and VP9 decoding on the web
- dav1d - AV1 decoder
- React Native - Cross-platform framework