Using Video
Drop a native video player into your layout tree with a single component. It draws decoded frames inside its own box, plays declaratively from props, and reports back through typed events.
@sigx/lynx-video exposes one component, VideoPlayer, a typed sigx-lynx wrapper over the native <video-player> element. On iOS it drives an AVPlayer inside an AVPlayerLayer; on Android it drives an androidx.media3 ExoPlayer inside a PlayerView. Prefer the component over the raw intrinsic — it maps friendly camelCase props onto the native attribute surface for you.
Run sigx prebuild once after adding the dependency. Prebuild auto-discovers the package, registers the <video-player> UI element on both platforms, and pulls in the media3 Gradle deps on Android. The element is unavailable until prebuild has run.
Basic usage
Give the player a src and a size, then let autoplay start it when the asset is ready. Because the component body returns a render function, signals and props read inside it stay reactive.
import { VideoPlayer } from '@sigx/lynx-video';
function ClipScreen() {
return () => (
<VideoPlayer
src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
autoplay
resizeMode="contain"
style={{ width: '100%', aspectRatio: 16 / 9 }}
/>
);
}
The player must be given a size — set width/height, or an aspectRatio, via style. It draws within its layout box like any other view. resizeMode controls how the frame fits: contain letterboxes, cover fills and crops, stretch distorts to fill.
Controlling play and pause declaratively
There is no imperative API in v1 — no seek or getStatus. You drive everything through props. The playing prop is a declarative play/pause toggle backed by a signal; when both playing and autoplay are set, playing wins.
import { component, signal } from '@sigx/lynx';
import { VideoPlayer } from '@sigx/lynx-video';
const Player = component(() => {
const playing = signal(false);
return () => (
<view style={{ width: '100%' }}>
<VideoPlayer
src="file:///clips/intro.mp4"
playing={playing()}
loop
muted
style={{ width: '100%', aspectRatio: 16 / 9 }}
/>
<text bindtap={() => playing.set(!playing())}>
{playing() ? 'Pause' : 'Play'}
</text>
</view>
);
});
loop restarts the clip natively at end-of-clip (iOS seeks to zero, Android uses REPEAT_MODE_ONE), so onEnd does not fire while looping. muted mutes audio independently of volume — it sets the effective volume to 0 without losing the stored value.
Reading duration and tracking progress
Listen for onLoad to learn an asset's duration and natural dimensions once metadata resolves. For live progress, onTimeUpdate broadcasts the current position roughly four times a second.
import { component, signal } from '@sigx/lynx';
import { VideoPlayer } from '@sigx/lynx-video';
const Progress = component(() => {
const durationMs = signal(0);
const positionMs = signal(0);
return () => (
<view>
<VideoPlayer
src="https://example.com/talk.mp4"
autoplay
onLoad={(e) => durationMs.set(e.detail.durationMs)}
onTimeUpdate={(e) => positionMs.set(e.detail.positionMs)}
style={{ width: '100%', aspectRatio: 16 / 9 }}
/>
<text>{Math.round((positionMs() / (durationMs() || 1)) * 100)}%</text>
</view>
);
});
onTimeUpdate fires every 250ms and crosses the native bridge on each call, so use it sparingly. For per-frame animation, read the duration once via onLoad and animate locally rather than reacting to every tick. On Android the 250ms timer only runs while playback is active. Note that durationMs is 0 for live or indeterminate-duration streams.
Handling errors and a poster
Show a poster before the first frame and surface failures through onError. An error fires on a non-recoverable load or playback failure — and immediately for an invalid src URL on iOS.
import { component, signal } from '@sigx/lynx';
import { VideoPlayer } from '@sigx/lynx-video';
const SafePlayer = component(() => {
const error = signal<string | null>(null);
return () => (
<view>
{error() && <text>{error()}</text>}
<VideoPlayer
src="https://example.com/maybe-broken.mp4"
poster="https://example.com/cover.jpg"
controls
onError={(e) => error.set(e.detail.message)}
style={{ width: '100%', aspectRatio: 16 / 9 }}
/>
</view>
);
});
Setting src reloads the player; clearing it (empty or undefined) stops playback and releases the asset, so a re-render that drops src will not keep playing in the background. src accepts https://, http://, file:// URIs, and absolute paths starting with / (treated as a local file).
Native setup notes
- iOS ATS — playing an
http://(non-HTTPS) URL requires anNSAppTransportSecurityexception in your app'sInfo.plist. The package does not relax ATS itself. - Controls —
controlsshows the platform default overlay. Android usesPlayerView's built-in transport controls. iOS v1 renders a minimal tap-to-play/pause overlay rather thanAVPlayerViewController; for full transport UI on iOS, build your own overlay driven by theplayingprop. - Poster on Android — the poster is a no-op stub in v1. Render an
<image>sibling and toggle it off ononLoadinstead. - media3 version — Android pins media3 1.4.1. If your app already depends on a different media3 version, align it via Gradle resolution to avoid duplicate-class errors.
- Audio session (iOS) — for audio-bearing video the component is intended to set
AVAudioSessionto.playback; apps that also use@sigx/lynx-audioshare a ref-counted session.
Notes
- Declarative only in v1: no
seek/getStatus. Drive playback through theplayingandsrcprops. playingoverridesautoplaywhen both are set.volumeis clamped to0..1;mutedis independent ofvolume.onEnddoes not fire whileloopistrue.onTimeUpdatefires ~4x/sec across the bridge — avoid using it to drive per-frame animation.
See also
- API reference — every export and its signature.
- Installation — project setup.
