Native WebM broadcast player for React Native — VP9 video and Opus audio, with A/V sync owned by the platform.
Receive-only: one audio track, one video track, no capture and no encode. You push muxed WebM bytes in; native demuxes, decodes and presents them.
JavaScript forwards bytes and does nothing else — no buffering, no timing, no
demuxing. Everything below feedData is native, and A/V sync belongs to the
platform rather than to this library.
your transport (e.g. Hypercore) ──▶ JS ──feedData(ArrayBuffer)──▶ native
│
┌─────────────────────────────────────┴──────┐
iOS Android
libwebm + libopus + VideoToolbox ExoPlayer
│ │
AVSampleBufferRenderSynchronizer MediaCodec
The two platforms share a lock-free byte ring and the public API. iOS demuxes
with libwebm because AVSampleBufferAudioRenderer needs decoded packets;
Android hands the same muxed bytes to ExoPlayer, which demuxes internally. That
asymmetry is deliberate — leaning on each platform is the design.
- React Native 0.78+ (New Architecture)
- Node 18+
react-native-nitro-modules
npm install @heartit/webm-player react-native-nitro-modules
cd ios && pod installThe iOS pod builds an Opus XCFramework from source on first pod install.
import { createWebmPlayer, WebmPlaybackState } from '@heartit/webm-player'
const player = createWebmPlayer()
player.start()
// Each chunk is muxed WebM. Returns false when the ring is full, meaning the
// producer is outrunning the demuxer.
socket.on('data', (chunk: Uint8Array) => {
player.feedData(chunk.buffer)
})
player.gain = 1.0
player.muted = false
if (player.playbackState === WebmPlaybackState.Playing) {
const { videoWidth, videoHeight, audioUnderruns } = player.getMetrics()
}
player.setEndOfStream()
player.stop()createWebmPlayer() returns an independent instance with its own stream buffer;
call it once per stream.
| Member | Notes |
|---|---|
start() / stop() / pause() / resume() |
Returns boolean |
feedData(data: ArrayBuffer) |
Muxed WebM. false means the ring is full |
setEndOfStream() / resetStream() |
|
isRunning / isPaused / playbackState |
Read-only |
muted / gain (0–2) / playbackRate (0.5–2) |
Read-write |
getMetrics() |
Bytes fed, packets decoded, underruns, frame size, position |
npm install
npm run codegen # nitrogen: TypeScript spec -> native interfaces
npm run test:native # sanitizer suites (address / thread / undefined)
npm run test:fuzz # demuxer fuzzing
npm run lint:size # file size budgets
npm run lint:pack # published tarball must be self-containedThe TypeScript spec in src/specs/ is the parity contract: nitrogen fails the
build if either platform drops a member.
See docs/AUDIT.md for the production-readiness audit,
including what is verified and what still needs device validation.
Apache-2.0