@moviie/player-expo
v0.29.0
Published
Moviie video player for Expo (expo-video, telemetry, native features).
Maintainers
Readme
@moviie/player-expo
React Native / Expo player for the Moviie platform. Wraps expo-video with the Moviie playback API, custom chrome, telemetry, PiP, background audio, and optional Chromecast.
Full documentation: docs.moviie.ai/player-expo
Platforms
One package, the same useMoviiePlayer + <MoviieVideo /> API on every Expo target:
| Platform | Rendition |
|----------|-----------|
| iOS / Android | expo-video player with the Moviie chrome (PiP, background audio, native fullscreen, telemetry) |
| Web (expo start --web / expo export --platform web) | The official Moviie watch embed in an iframe — full web player (HLS, AI menu, watermark), controlled via the Player JS API |
On web the iframe mounts immediately from embedId and authorizes itself, so the
publishable key is optional there (it still enables the playback metadata in
the hook result). Details and per-platform differences:
docs.moviie.ai/player-expo/expo-web.
Prerequisites
You need a Moviie account and a publishable API key (mvi_pub_*).
- Create an account at app.moviie.ai/signin.
- Go to Organization Settings → API Keys and create a Publishable key.
- Copy the key: it starts with
mvi_pub_.
Set it as an environment variable in your project:
# .env.local
EXPO_PUBLIC_MOVIIE_PUBLISHABLE_KEY=mvi_pub_your_key_hereNever use a secret key (
mvi_priv_*) in a client app. The playback API rejects it.
Install
pnpm add @moviie/player-expo \
expo expo-video expo-application \
react-native-svg react-native-reanimated react-native-gesture-handler \
react-native-safe-area-contextOptional extras:
| Package | Unlocks |
|---------|---------|
| expo-secure-store | Persistent viewer token + resume position across sessions |
| react-native-google-cast | Chromecast button: install only if you need it |
Setup
1. Add the config plugin to app.json:
{
"expo": {
"plugins": [
["@moviie/player-expo", { "backgroundPlayback": true, "pictureInPicture": true }],
["expo-video", { "supportsBackgroundPlayback": true, "supportsPictureInPicture": true }]
]
}
}2. Rebuild native projects after adding or changing plugins:
pnpm expo prebuild
pnpm expo run:ios # or run:android3. Wrap your app with MoviieProvider:
import { MoviieProvider, MoviieVideo, useMoviiePlayer } from "@moviie/player-expo"
function Player({ embedId }: { embedId: string }) {
const moviie = useMoviiePlayer({ embedId })
return <MoviieVideo {...moviie} aspectRatio={16 / 9} />
}
export default function App() {
return (
<MoviieProvider publishableKey={process.env.EXPO_PUBLIC_MOVIIE_PUBLISHABLE_KEY}>
<Player embedId="YOUR-EMBED-UUID" />
</MoviieProvider>
)
}Set EXPO_PUBLIC_MOVIIE_PUBLISHABLE_KEY to a mvi_pub_* key from your organization settings.
API reference
<MoviieProvider>
Provides the Moviie client to the component tree. Place it at the root of your app.
| Prop | Type | Required | Description |
|------|------|:--------:|-------------|
| publishableKey | string | ✓ | Publishable key (mvi_pub_*) from your Moviie organization settings. |
| children | ReactNode | ✓ | Your app tree. |
useMoviiePlayer(options)
Fetches playback metadata and wires native player state. Spread the return value onto <MoviieVideo>.
Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| embedId | string | — | Required. UUID of the embed from the Moviie dashboard. |
| autoplay | boolean | dashboard | Override the embed's autoplay setting. |
| pictureInPicture | boolean | dashboard | Override the embed's PiP setting. |
| pictureInPictureAutoStart | boolean | false | Automatically enter PiP when the app moves to the background. |
| backgroundPlayback | boolean | false | Keep audio playing when the app is backgrounded. |
| lockScreenNowPlaying | boolean | false | Show a Now Playing notification on the lock screen. |
| rememberPosition | boolean | dashboard | Override the embed's "remember position" setting. |
Returns:
| Field | Type | Description |
|-------|------|-------------|
| player | VideoPlayer \| null | expo-video player instance. null on web. |
| playback | MoviiePlaybackData \| null | Full playback metadata from the API. null while loading. |
| error | Error \| null | Typed error (see Error codes). null when healthy. |
| isLoading | boolean | true while the first playback fetch is in-flight. |
| videoPresentation | MoviieVideoPresentationProps | PiP and linear playback flags, pre-computed from playback + options. Spread onto <MoviieVideo>. |
| retry | () => void | Re-triggers the playback fetch. |
<MoviieVideo>
Main playback component. Accepts the full return value of useMoviiePlayer spread as props, plus presentation and behaviour overrides.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| player | VideoPlayer \| null | — | Player instance from useMoviiePlayer. |
| playback | MoviiePlaybackData \| null | — | Playback metadata from useMoviiePlayer. |
| aspectRatio | number | — | Width ÷ height ratio (e.g. 16 / 9). |
| native | boolean | false | Use the platform's native VideoView chrome instead of the Moviie custom skin. |
| nativeControls | boolean | auto | Show or hide native VideoView controls (only relevant when native={true}). |
| pictureInPicture | boolean | hook / dashboard | Override PiP availability for this component. |
| pictureInPictureAutoStart | boolean | false | Auto-enter PiP when the app moves to the background. |
| controlsAutoHideMs | number | 3000 | Milliseconds of inactivity before the Moviie chrome hides. |
| controlsVisibleByDefault | boolean | false | Show the chrome immediately on mount without a tap. |
| castAdapter | MoviieCastAdapter \| null | — | Chromecast adapter. Create with createGoogleCastAdapter() from @moviie/player-expo/cast. |
| cast | boolean | dashboard | Override the embed's Chromecast setting. false hides the button and silences the missing-adapter warning. |
| isLoading | boolean | — | From useMoviiePlayer. Shows a loading state in the chrome. |
| error | Error \| null | — | From useMoviiePlayer. Triggers the built-in error shell. |
| retry | () => void | — | From useMoviiePlayer. Called by the built-in retry button. |
| onRetry | () => void | — | Replace the default retry behaviour. You must call moviie.retry() yourself if needed. |
Also accepts all VideoViewProps from expo-video (except player, which is managed internally).
<MoviieErrorBoundary>
React error boundary that catches crashes inside <MoviieVideo> and prevents them from bubbling up to your app.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | — | Component tree to protect — wrap <MoviieVideo> here. |
| fallback | ({ error: Error, reset: () => void }) => ReactNode | built-in shell | Custom fallback UI rendered when a crash occurs. reset() unmounts and remounts the player. |
| onError | (error: Error, info: React.ErrorInfo) => void | — | Called on each caught error. Pass Sentry.captureException directly. |
import { MoviieErrorBoundary, MoviieVideo, useMoviiePlayer } from "@moviie/player-expo"
import * as Sentry from "@sentry/react-native"
const moviie = useMoviiePlayer({ embedId })
<MoviieErrorBoundary
onError={(err, info) => Sentry.captureException(err, { contexts: { react: info } })}
fallback={({ error, reset }) => <MyFallback message={error.message} onRetry={reset} />}
>
<MoviieVideo {...moviie} aspectRatio={16 / 9} />
</MoviieErrorBoundary>Error codes
Each error from useMoviiePlayer has a .code field and maps to a typed class from @moviie/player-sdk.
| Code | Class | Cause | Default CTA |
|------|-------|-------|-------------|
| auth | MoviieAuthError | Invalid or missing publishable key. | Open dashboard |
| not_found | MoviieNotFoundError | Embed ID does not exist or was deleted. | Open dashboard |
| bundle_blocked | MoviieBundleBlockedError | App bundle ID not in the embed's allowlist. Most common on mobile. | Open dashboard |
| referrer_blocked | MoviieReferrerBlockedError | Request origin present but not in the embed's referrer allowlist. | Open dashboard |
| direct_access_blocked | MoviieDirectAccessBlockedError | Video opened directly (no embedding origin) while direct-URL access is blocked. | Open dashboard |
| subscription_inactive | MoviieSubscriptionInactiveError | Organization subscription is paused or expired. | Open dashboard |
| network | MoviieNetworkError | Network request failed (timeout, unreachable). High frequency on mobile. | Retry |
| rate_limit | MoviieRateLimitError | Too many requests in a short period. | Retry |
| unknown | Error | Unrecognized error. | Retry |
Detect by instance or code string:
import { MoviieBundleBlockedError } from "@moviie/player-sdk"
if (error instanceof MoviieBundleBlockedError) { /* fix allowlist */ }
if (error?.code === "network") { /* retry */ }Chromecast (optional)
Cast support lives in the @moviie/player-expo/cast subpath. Install only if you need it:
pnpm add react-native-google-castAdd the plugin to app.json, then rebuild:
["react-native-google-cast", { "iosReceiverAppId": "CC1AD845", "androidReceiverAppId": "CC1AD845" }]Wire the adapter:
import { useMemo } from "react"
import { createGoogleCastAdapter } from "@moviie/player-expo/cast"
const castAdapter = useMemo(() => createGoogleCastAdapter(), [])
<MoviieVideo {...moviie} castAdapter={castAdapter} aspectRatio={16 / 9} />Without a castAdapter, the cast button is hidden and the player works normally.
End of playback
import { useMoviiePlaybackEnded } from "@moviie/player-expo"
useMoviiePlaybackEnded(player, () => {
// video reached the end
})Or use PLAYER_API_EVENTS.ENDED with useMoviieEvent for a generic handler.
Requirements
- Expo SDK 53+
expo-video3+- Dev client required:
expo-videodoes not work in Expo Go
