react-native-transparent-video-player 0.3.2

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.
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import type { ImplProps } from './types';
3
+ /**
4
+ * Android implementation: native ExoPlayer + OpenGL (samplerExternalOES)
5
+ * view. Skia's useVideo is NOT used on Android — its HardwareBuffer importer
6
+ * cannot sample the YUV/vendor formats hardware decoders emit.
7
+ */
8
+ export declare function TransparentVideoView({ uri, width, height, loop, paused, style, onEnd, }: ImplProps): React.JSX.Element;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TransparentVideoView = TransparentVideoView;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const expo_modules_core_1 = require("expo-modules-core");
6
+ const react_1 = require("react");
7
+ const react_native_reanimated_1 = require("react-native-reanimated");
8
+ const NativeView = (0, expo_modules_core_1.requireNativeViewManager)('TransparentVideo');
9
+ // The native view takes a plain boolean; bridge a Reanimated SharedValue
10
+ // (supported for API parity with the iOS Skia path) into React state.
11
+ function useResolvedPaused(paused) {
12
+ const isShared = typeof paused === 'object' && paused !== null;
13
+ const [fromShared, setFromShared] = (0, react_1.useState)(isShared ? paused.value : false);
14
+ (0, react_native_reanimated_1.useAnimatedReaction)(() => (isShared ? paused.value : false), (value, previous) => {
15
+ if (isShared && value !== previous)
16
+ (0, react_native_reanimated_1.runOnJS)(setFromShared)(value);
17
+ }, [paused, isShared]);
18
+ return isShared ? fromShared : paused;
19
+ }
20
+ /**
21
+ * Android implementation: native ExoPlayer + OpenGL (samplerExternalOES)
22
+ * view. Skia's useVideo is NOT used on Android — its HardwareBuffer importer
23
+ * cannot sample the YUV/vendor formats hardware decoders emit.
24
+ */
25
+ function TransparentVideoView({ uri, width, height, loop, paused, style, onEnd, }) {
26
+ const pausedBool = useResolvedPaused(paused);
27
+ return ((0, jsx_runtime_1.jsx)(NativeView, { sourceUri: uri, loop: loop, paused: pausedBool, onVideoEnd: onEnd, style: [{ width, height }, style] }));
28
+ }
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import type { ImplProps } from './types';
3
+ /**
4
+ * iOS/web implementation: Skia canvas + RuntimeEffect unpack shader.
5
+ * (Android uses the native ExoPlayer + OpenGL view instead — see
6
+ * TransparentVideoView.android.tsx.)
7
+ */
8
+ export declare function TransparentVideoView({ uri, width, height, loop, paused, style, }: ImplProps): React.JSX.Element;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TransparentVideoView = TransparentVideoView;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_native_skia_1 = require("@shopify/react-native-skia");
6
+ const react_1 = require("react");
7
+ const react_native_reanimated_1 = require("react-native-reanimated");
8
+ // Recombines an alpha-packed frame: color sampled from the top half,
9
+ // alpha from the same x in the bottom half. The packed video is encoded
10
+ // with PREMULTIPLIED color (see the pack-alpha-video premultiply step), so
11
+ // the rgb passes through as-is — multiplying again here would darken edges.
12
+ const compiledEffect = react_native_skia_1.Skia.RuntimeEffect.Make(`
13
+ uniform shader video;
14
+ uniform float halfH;
15
+
16
+ half4 main(float2 xy) {
17
+ half3 rgb = video.eval(xy).rgb;
18
+ half a = video.eval(float2(xy.x, xy.y + halfH)).r;
19
+ return half4(rgb, a);
20
+ }
21
+ `);
22
+ if (!compiledEffect) {
23
+ throw new Error('TransparentVideo: failed to compile alpha-unpack shader');
24
+ }
25
+ const unpackAlphaEffect = compiledEffect;
26
+ /**
27
+ * iOS/web implementation: Skia canvas + RuntimeEffect unpack shader.
28
+ * (Android uses the native ExoPlayer + OpenGL view instead — see
29
+ * TransparentVideoView.android.tsx.)
30
+ */
31
+ function TransparentVideoView({ uri, width, height, loop, paused, style, }) {
32
+ const { currentFrame } = (0, react_native_skia_1.useVideo)(uri, { looping: loop, paused });
33
+ // Until the first frame arrives, the Fill would be painted with Skia's
34
+ // default paint — opaque black — flashing a black rectangle on mount.
35
+ // Gate it on first-frame readiness instead; an empty Canvas is transparent.
36
+ const [ready, setReady] = (0, react_1.useState)(false);
37
+ (0, react_1.useEffect)(() => setReady(false), [uri]);
38
+ (0, react_native_reanimated_1.useAnimatedReaction)(() => currentFrame.value !== null, (hasFrame, prev) => {
39
+ if (hasFrame && !prev)
40
+ (0, react_native_reanimated_1.runOnJS)(setReady)(true);
41
+ }, [currentFrame]);
42
+ return ((0, jsx_runtime_1.jsx)(react_native_skia_1.Canvas, { style: [{ width, height }, style], children: ready ? ((0, jsx_runtime_1.jsx)(react_native_skia_1.Fill, { children: (0, jsx_runtime_1.jsx)(react_native_skia_1.Shader, { source: unpackAlphaEffect, uniforms: { halfH: height }, children: (0, jsx_runtime_1.jsx)(react_native_skia_1.ImageShader, { image: currentFrame, fit: "fill", rect: { x: 0, y: 0, width, height: height * 2 } }) }) })) : null }));
43
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { TransparentVideo } from './TransparentVideo';
2
+ export type { TransparentVideoProps } from './TransparentVideo';
package/lib/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TransparentVideo = void 0;
4
+ var TransparentVideo_1 = require("./TransparentVideo");
5
+ Object.defineProperty(exports, "TransparentVideo", { enumerable: true, get: function () { return TransparentVideo_1.TransparentVideo; } });
package/lib/types.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+ /**
4
+ * Props of the internal per-platform view implementation. Both
5
+ * TransparentVideoView.tsx (iOS/web, Skia) and
6
+ * TransparentVideoView.android.tsx (native ExoPlayer + OpenGL) must satisfy
7
+ * this exact signature so TypeScript checks them interchangeably.
8
+ */
9
+ export interface ImplProps {
10
+ /** Already-resolved playable URI (file:// or http(s)), or null while resolving. */
11
+ uri: string | null;
12
+ width: number;
13
+ height: number;
14
+ loop: boolean;
15
+ paused: SharedValue<boolean> | boolean;
16
+ style?: StyleProp<ViewStyle>;
17
+ /** Fires when a non-looping video finishes. Currently Android-only. */
18
+ onEnd?: () => void;
19
+ }
package/lib/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Resolves an asset module to a playable URI. Prefers expo-asset when it is
3
+ * installed (downloads the asset to the local filesystem, which the video
4
+ * decoder needs in release builds); falls back to React Native's
5
+ * Image.resolveAssetSource for bare RN apps without expo-asset.
6
+ */
7
+ export declare function useResolvedUri(source: number | string): string | null;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useResolvedUri = useResolvedUri;
4
+ const react_1 = require("react");
5
+ const react_native_1 = require("react-native");
6
+ /**
7
+ * Resolves an asset module to a playable URI. Prefers expo-asset when it is
8
+ * installed (downloads the asset to the local filesystem, which the video
9
+ * decoder needs in release builds); falls back to React Native's
10
+ * Image.resolveAssetSource for bare RN apps without expo-asset.
11
+ */
12
+ function useResolvedUri(source) {
13
+ const [uri, setUri] = (0, react_1.useState)(typeof source === 'string' ? source : null);
14
+ (0, react_1.useEffect)(() => {
15
+ if (typeof source === 'string') {
16
+ setUri(source);
17
+ return;
18
+ }
19
+ let cancelled = false;
20
+ let expoAsset = null;
21
+ try {
22
+ expoAsset = require('expo-asset');
23
+ }
24
+ catch {
25
+ expoAsset = null;
26
+ }
27
+ if (expoAsset) {
28
+ expoAsset.Asset.fromModule(source)
29
+ .downloadAsync()
30
+ .then((asset) => {
31
+ if (!cancelled)
32
+ setUri(asset.localUri ?? asset.uri);
33
+ })
34
+ .catch((e) => {
35
+ console.error('TransparentVideo: failed to resolve asset', e);
36
+ });
37
+ }
38
+ else {
39
+ const resolved = react_native_1.Image.resolveAssetSource(source);
40
+ if (resolved?.uri)
41
+ setUri(resolved.uri);
42
+ else
43
+ console.error('TransparentVideo: could not resolve asset module');
44
+ }
45
+ return () => {
46
+ cancelled = true;
47
+ };
48
+ }, [source]);
49
+ return uri;
50
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "react-native-transparent-video-player",
3
+ "version": "0.3.2",
4
+ "description": "Transparent (alpha) video playback for React Native — plays alpha-packed H.264 MP4s on every iOS/Android hardware decoder. Skia-rendered on iOS, native ExoPlayer+OpenGL view on Android. Includes a CLI to pack ProRes 4444 exports.",
5
+ "license": "MIT",
6
+ "author": "LucaL1fe",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/LucaL1fe/react-native-transparent-video-player.git",
10
+ "directory": "packages/react-native-transparent-video-player"
11
+ },
12
+ "homepage": "https://github.com/LucaL1fe/react-native-transparent-video-player",
13
+ "keywords": [
14
+ "react-native",
15
+ "expo",
16
+ "skia",
17
+ "transparent-video",
18
+ "alpha",
19
+ "animation",
20
+ "mascot",
21
+ "webp-alternative",
22
+ "gif-alternative"
23
+ ],
24
+ "main": "lib/index.js",
25
+ "types": "lib/index.d.ts",
26
+ "files": [
27
+ "lib",
28
+ "cli",
29
+ "src",
30
+ "android/build.gradle",
31
+ "android/src",
32
+ "expo-module.config.json",
33
+ "THIRD-PARTY-NOTICES.md"
34
+ ],
35
+ "bin": {
36
+ "pack-transparent-video": "cli/pack-transparent-video.mjs"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.json",
40
+ "prepublishOnly": "npm run build"
41
+ },
42
+ "peerDependencies": {
43
+ "@shopify/react-native-skia": ">=1.5.0",
44
+ "expo-asset": "*",
45
+ "react": ">=18",
46
+ "react-native": ">=0.74",
47
+ "react-native-reanimated": ">=3.0.0",
48
+ "expo-modules-core": "*"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "expo-asset": {
52
+ "optional": true
53
+ },
54
+ "expo-modules-core": {
55
+ "optional": true
56
+ }
57
+ },
58
+ "devDependencies": {
59
+ "@shopify/react-native-skia": "^2.2.12",
60
+ "@types/react": "^19.0.0",
61
+ "react": "^19.0.0",
62
+ "react-native": "^0.86.0",
63
+ "react-native-reanimated": "^4.0.0",
64
+ "typescript": "^5.6.0",
65
+ "expo-modules-core": "~57.0.3"
66
+ }
67
+ }
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
3
+ import type { SharedValue } from 'react-native-reanimated';
4
+
5
+ import { TransparentVideoView } from './TransparentVideoView';
6
+ import { useResolvedUri } from './useResolvedUri';
7
+
8
+ export interface TransparentVideoProps {
9
+ /**
10
+ * An alpha-packed video (color on the top half, grayscale matte on the
11
+ * bottom half): either a bundled asset module (`require('./x-packed.mp4')`)
12
+ * or a URI string (file:// or https://).
13
+ */
14
+ source: number | string;
15
+ /** Display width. The packed video's own height is 2x its visible height. */
16
+ width: number;
17
+ /** Display height (half the packed video's pixel height). */
18
+ height: number;
19
+ /** Loop playback (default true). Set false for one-shot animations. */
20
+ loop?: boolean;
21
+ paused?: SharedValue<boolean> | boolean;
22
+ style?: StyleProp<ViewStyle>;
23
+ /**
24
+ * Fires when a non-looping video finishes playing.
25
+ * Currently Android-only (native player event); on iOS it never fires —
26
+ * time-based fallbacks remain the caller's responsibility there.
27
+ */
28
+ onEnd?: () => void;
29
+ }
30
+
31
+ /**
32
+ * Plays an alpha-packed MP4 with real transparency.
33
+ *
34
+ * Platform split: iOS/web render through Skia (RuntimeEffect unpack shader);
35
+ * Android uses a native ExoPlayer + OpenGL view (Metro resolves
36
+ * ./TransparentVideoView to the .android implementation there).
37
+ */
38
+ export function TransparentVideo({
39
+ source,
40
+ width,
41
+ height,
42
+ loop = true,
43
+ paused = false,
44
+ style,
45
+ onEnd,
46
+ }: TransparentVideoProps) {
47
+ const uri = useResolvedUri(source);
48
+
49
+ return (
50
+ <TransparentVideoView
51
+ uri={uri}
52
+ width={width}
53
+ height={height}
54
+ loop={loop}
55
+ paused={paused}
56
+ style={style}
57
+ onEnd={onEnd}
58
+ />
59
+ );
60
+ }
@@ -0,0 +1,61 @@
1
+ import { requireNativeViewManager } from 'expo-modules-core';
2
+ import React, { useState } from 'react';
3
+ import type { StyleProp, ViewStyle } from 'react-native';
4
+ import { runOnJS, useAnimatedReaction } from 'react-native-reanimated';
5
+ import type { SharedValue } from 'react-native-reanimated';
6
+
7
+ import type { ImplProps } from './types';
8
+
9
+ interface NativeProps {
10
+ sourceUri: string | null;
11
+ loop: boolean;
12
+ paused: boolean;
13
+ onVideoEnd?: () => void;
14
+ style?: StyleProp<ViewStyle>;
15
+ }
16
+
17
+ const NativeView = requireNativeViewManager<NativeProps>('TransparentVideo');
18
+
19
+ // The native view takes a plain boolean; bridge a Reanimated SharedValue
20
+ // (supported for API parity with the iOS Skia path) into React state.
21
+ function useResolvedPaused(paused: SharedValue<boolean> | boolean): boolean {
22
+ const isShared = typeof paused === 'object' && paused !== null;
23
+ const [fromShared, setFromShared] = useState(
24
+ isShared ? (paused as SharedValue<boolean>).value : false
25
+ );
26
+ useAnimatedReaction(
27
+ () => (isShared ? (paused as SharedValue<boolean>).value : false),
28
+ (value, previous) => {
29
+ if (isShared && value !== previous) runOnJS(setFromShared)(value);
30
+ },
31
+ [paused, isShared]
32
+ );
33
+ return isShared ? fromShared : (paused as boolean);
34
+ }
35
+
36
+ /**
37
+ * Android implementation: native ExoPlayer + OpenGL (samplerExternalOES)
38
+ * view. Skia's useVideo is NOT used on Android — its HardwareBuffer importer
39
+ * cannot sample the YUV/vendor formats hardware decoders emit.
40
+ */
41
+ export function TransparentVideoView({
42
+ uri,
43
+ width,
44
+ height,
45
+ loop,
46
+ paused,
47
+ style,
48
+ onEnd,
49
+ }: ImplProps) {
50
+ const pausedBool = useResolvedPaused(paused);
51
+
52
+ return (
53
+ <NativeView
54
+ sourceUri={uri}
55
+ loop={loop}
56
+ paused={pausedBool}
57
+ onVideoEnd={onEnd}
58
+ style={[{ width, height }, style]}
59
+ />
60
+ );
61
+ }
@@ -0,0 +1,77 @@
1
+ import {
2
+ Canvas,
3
+ Fill,
4
+ ImageShader,
5
+ Shader,
6
+ Skia,
7
+ useVideo,
8
+ } from '@shopify/react-native-skia';
9
+ import React, { useEffect, useState } from 'react';
10
+ import { runOnJS, useAnimatedReaction } from 'react-native-reanimated';
11
+
12
+ import type { ImplProps } from './types';
13
+
14
+ // Recombines an alpha-packed frame: color sampled from the top half,
15
+ // alpha from the same x in the bottom half. The packed video is encoded
16
+ // with PREMULTIPLIED color (see the pack-alpha-video premultiply step), so
17
+ // the rgb passes through as-is — multiplying again here would darken edges.
18
+ const compiledEffect = Skia.RuntimeEffect.Make(`
19
+ uniform shader video;
20
+ uniform float halfH;
21
+
22
+ half4 main(float2 xy) {
23
+ half3 rgb = video.eval(xy).rgb;
24
+ half a = video.eval(float2(xy.x, xy.y + halfH)).r;
25
+ return half4(rgb, a);
26
+ }
27
+ `);
28
+
29
+ if (!compiledEffect) {
30
+ throw new Error('TransparentVideo: failed to compile alpha-unpack shader');
31
+ }
32
+ const unpackAlphaEffect = compiledEffect;
33
+
34
+ /**
35
+ * iOS/web implementation: Skia canvas + RuntimeEffect unpack shader.
36
+ * (Android uses the native ExoPlayer + OpenGL view instead — see
37
+ * TransparentVideoView.android.tsx.)
38
+ */
39
+ export function TransparentVideoView({
40
+ uri,
41
+ width,
42
+ height,
43
+ loop,
44
+ paused,
45
+ style,
46
+ }: ImplProps) {
47
+ const { currentFrame } = useVideo(uri, { looping: loop, paused });
48
+
49
+ // Until the first frame arrives, the Fill would be painted with Skia's
50
+ // default paint — opaque black — flashing a black rectangle on mount.
51
+ // Gate it on first-frame readiness instead; an empty Canvas is transparent.
52
+ const [ready, setReady] = useState(false);
53
+ useEffect(() => setReady(false), [uri]);
54
+ useAnimatedReaction(
55
+ () => currentFrame.value !== null,
56
+ (hasFrame, prev) => {
57
+ if (hasFrame && !prev) runOnJS(setReady)(true);
58
+ },
59
+ [currentFrame]
60
+ );
61
+
62
+ return (
63
+ <Canvas style={[{ width, height }, style]}>
64
+ {ready ? (
65
+ <Fill>
66
+ <Shader source={unpackAlphaEffect} uniforms={{ halfH: height }}>
67
+ <ImageShader
68
+ image={currentFrame}
69
+ fit="fill"
70
+ rect={{ x: 0, y: 0, width, height: height * 2 }}
71
+ />
72
+ </Shader>
73
+ </Fill>
74
+ ) : null}
75
+ </Canvas>
76
+ );
77
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { TransparentVideo } from './TransparentVideo';
2
+ export type { TransparentVideoProps } from './TransparentVideo';
package/src/types.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+
4
+ /**
5
+ * Props of the internal per-platform view implementation. Both
6
+ * TransparentVideoView.tsx (iOS/web, Skia) and
7
+ * TransparentVideoView.android.tsx (native ExoPlayer + OpenGL) must satisfy
8
+ * this exact signature so TypeScript checks them interchangeably.
9
+ */
10
+ export interface ImplProps {
11
+ /** Already-resolved playable URI (file:// or http(s)), or null while resolving. */
12
+ uri: string | null;
13
+ width: number;
14
+ height: number;
15
+ loop: boolean;
16
+ paused: SharedValue<boolean> | boolean;
17
+ style?: StyleProp<ViewStyle>;
18
+ /** Fires when a non-looping video finishes. Currently Android-only. */
19
+ onEnd?: () => void;
20
+ }
@@ -0,0 +1,60 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Image } from 'react-native';
3
+
4
+ // Minimal surface of expo-asset used below — typed locally so the package
5
+ // compiles without expo-asset installed (it is an optional peer).
6
+ interface ExpoAssetModule {
7
+ Asset: {
8
+ fromModule(module: number): {
9
+ downloadAsync(): Promise<{ localUri: string | null; uri: string }>;
10
+ };
11
+ };
12
+ }
13
+
14
+ /**
15
+ * Resolves an asset module to a playable URI. Prefers expo-asset when it is
16
+ * installed (downloads the asset to the local filesystem, which the video
17
+ * decoder needs in release builds); falls back to React Native's
18
+ * Image.resolveAssetSource for bare RN apps without expo-asset.
19
+ */
20
+ export function useResolvedUri(source: number | string): string | null {
21
+ const [uri, setUri] = useState<string | null>(
22
+ typeof source === 'string' ? source : null
23
+ );
24
+
25
+ useEffect(() => {
26
+ if (typeof source === 'string') {
27
+ setUri(source);
28
+ return;
29
+ }
30
+ let cancelled = false;
31
+
32
+ let expoAsset: ExpoAssetModule | null = null;
33
+ try {
34
+ expoAsset = require('expo-asset') as ExpoAssetModule;
35
+ } catch {
36
+ expoAsset = null;
37
+ }
38
+
39
+ if (expoAsset) {
40
+ expoAsset.Asset.fromModule(source)
41
+ .downloadAsync()
42
+ .then((asset) => {
43
+ if (!cancelled) setUri(asset.localUri ?? asset.uri);
44
+ })
45
+ .catch((e: unknown) => {
46
+ console.error('TransparentVideo: failed to resolve asset', e);
47
+ });
48
+ } else {
49
+ const resolved = Image.resolveAssetSource(source);
50
+ if (resolved?.uri) setUri(resolved.uri);
51
+ else console.error('TransparentVideo: could not resolve asset module');
52
+ }
53
+
54
+ return () => {
55
+ cancelled = true;
56
+ };
57
+ }, [source]);
58
+
59
+ return uri;
60
+ }