react-native-video-provider 0.2.3 → 0.2.5
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.
- package/AuVideo.podspec +1 -1
- package/README.md +72 -3
- package/android/build.gradle +25 -0
- package/lib/module/components/VideoFeed.js +170 -0
- package/lib/module/components/VideoFeed.js.map +1 -0
- package/lib/module/components/VideoPlayer.js +84 -6
- package/lib/module/components/VideoPlayer.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/VideoFeed.d.ts +56 -0
- package/lib/typescript/src/components/VideoFeed.d.ts.map +1 -0
- package/lib/typescript/src/components/VideoPlayer.d.ts +38 -2
- package/lib/typescript/src/components/VideoPlayer.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +6 -4
- package/scripts/postinstall.js +58 -0
- package/src/components/VideoFeed.tsx +231 -0
- package/src/components/VideoPlayer.tsx +186 -53
- package/src/index.ts +6 -0
package/AuVideo.podspec
CHANGED
|
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
|
|
|
11
11
|
s.authors = package["author"]
|
|
12
12
|
|
|
13
13
|
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
-
s.source = { :git => "https://github.com/
|
|
14
|
+
s.source = { :git => "https://github.com/gurdeep99/react-native-au-video.git", :tag => "#{s.version}" }
|
|
15
15
|
|
|
16
16
|
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
17
17
|
s.private_header_files = "ios/**/*.h"
|
package/README.md
CHANGED
|
@@ -18,8 +18,14 @@ Feed ──▶ Detail ──▶ Fullscreen (rotation unlocked) ──▶ Floatin
|
|
|
18
18
|
navigation, tab switches, unmounts, Fast Refresh
|
|
19
19
|
- 🔁 **Same-video handoff** — `setSource` with the same `id` is a no-op for
|
|
20
20
|
the engine; the player just re-parents to the new surface
|
|
21
|
+
- 📱 **Single-engine feed** — `VideoFeed`, a TikTok/Reels-style list where
|
|
22
|
+
only the scrolled-into-focus video plays, on one player, flat memory
|
|
21
23
|
- 📺 **Fullscreen** — built-in host that unlocks rotation while visible and
|
|
22
24
|
restores the previous orientation lock on exit
|
|
25
|
+
- 🧭 **Orientation control** — force portrait/landscape (+ inverted) per
|
|
26
|
+
player, scoped to fullscreen, or as a standing lock
|
|
27
|
+
- ⏸️ **Focus-aware** — auto-pause on app background and on screen navigation
|
|
28
|
+
(React Navigation's `useIsFocused()`)
|
|
23
29
|
- 🎈 **Floating player** — built-in draggable in-app window
|
|
24
30
|
- 🖼 **Picture in Picture** — Android + iOS
|
|
25
31
|
- ⚡ **New Architecture native** — TurboModule + Fabric, typed end to end
|
|
@@ -31,11 +37,15 @@ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full design and
|
|
|
31
37
|
## Installation
|
|
32
38
|
|
|
33
39
|
```sh
|
|
34
|
-
npm install react-native-video-provider
|
|
40
|
+
npm install react-native-video-provider react-native-svg
|
|
35
41
|
cd ios && pod install
|
|
36
42
|
```
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
`react-native-svg` is a peer dependency (used by the built-in control icons).
|
|
45
|
+
|
|
46
|
+
Requires the New Architecture. Works on React Native **0.79+** — the
|
|
47
|
+
TurboModule spec uses direct codegen-type imports so it parses on 0.79's
|
|
48
|
+
codegen as well as 0.80+.
|
|
39
49
|
|
|
40
50
|
## Quick start
|
|
41
51
|
|
|
@@ -131,7 +141,66 @@ player.attach('feed'); // render here
|
|
|
131
141
|
```
|
|
132
142
|
|
|
133
143
|
`<VideoPlayer>` is the convenience wrapper that does `setSource` + `attach` +
|
|
134
|
-
optional controls in one component.
|
|
144
|
+
optional controls in one component. Handy props:
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
<VideoPlayer
|
|
148
|
+
source={video}
|
|
149
|
+
autoplay muted repeat // playback flags
|
|
150
|
+
resizeMode="contain" // contain | cover | stretch
|
|
151
|
+
controls // built-in chrome (SVG icons)
|
|
152
|
+
onLoadComplete={(m) => {}} // duration/dimensions ready
|
|
153
|
+
onBuffering={(b) => {}}
|
|
154
|
+
onError={(e) => {}}
|
|
155
|
+
ref={playerRef} // → the VideoManager (playerRef.current.seek(…))
|
|
156
|
+
/>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
For a live stream (no known duration), the built-in controls automatically
|
|
160
|
+
hide the seek bar and show just mute + fullscreen.
|
|
161
|
+
|
|
162
|
+
## Video feed (single engine, only the focused one plays)
|
|
163
|
+
|
|
164
|
+
`<VideoFeed>` is a TikTok/Reels-style vertical feed. It renders many videos in
|
|
165
|
+
a FlatList but plays **only the one scrolled into focus** — on the same single
|
|
166
|
+
engine, so memory and CPU stay flat no matter how long the feed. Each item is
|
|
167
|
+
just a surface; scrolling hands the one player off to the focused item.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
import { VideoFeed } from 'react-native-video-provider';
|
|
171
|
+
|
|
172
|
+
<VideoFeed
|
|
173
|
+
data={videos} // [{ id, uri, title? }, …] — each needs a stable id
|
|
174
|
+
renderOverlay={({ item, focused }) => (
|
|
175
|
+
<Caption title={item.title} paused={!focused} />
|
|
176
|
+
)}
|
|
177
|
+
/>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Any extra `FlatList` prop passes through (`onEndReached` for infinite scroll,
|
|
181
|
+
`ListHeaderComponent`, …).
|
|
182
|
+
|
|
183
|
+
## Pausing on focus loss
|
|
184
|
+
|
|
185
|
+
Both `<VideoPlayer>` and `<VideoFeed>` pause automatically when the **app is
|
|
186
|
+
backgrounded** (opt out with `pauseOnFocusLost={false}` for background audio).
|
|
187
|
+
|
|
188
|
+
For **screen navigation** (navigating to another screen while the app stays
|
|
189
|
+
foregrounded), pass your navigation library's focus flag — React Navigation
|
|
190
|
+
keeps screens mounted, so there is no other reliable signal:
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
import { useIsFocused } from '@react-navigation/native';
|
|
194
|
+
|
|
195
|
+
function Screen() {
|
|
196
|
+
const isFocused = useIsFocused();
|
|
197
|
+
return <VideoPlayer source={video} isFocused={isFocused} />;
|
|
198
|
+
// VideoFeed takes the same prop.
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`false` pauses; returning to `true` resumes (reclaiming the engine if another
|
|
203
|
+
video took it while you were away).
|
|
135
204
|
|
|
136
205
|
## State & events
|
|
137
206
|
|
package/android/build.gradle
CHANGED
|
@@ -57,3 +57,28 @@ dependencies {
|
|
|
57
57
|
implementation "androidx.media3:media3-ui:${media3Version}"
|
|
58
58
|
implementation "androidx.core:core-ktx:1.13.1"
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
// Print the package banner while the app compiles. Best-effort and fully
|
|
62
|
+
// guarded — a missing node binary or any failure never breaks the build.
|
|
63
|
+
tasks.register("auVideoBanner") {
|
|
64
|
+
doLast {
|
|
65
|
+
try {
|
|
66
|
+
def script = new File(projectDir, "../scripts/postinstall.js")
|
|
67
|
+
if (script.exists()) {
|
|
68
|
+
def banner = providers.exec {
|
|
69
|
+
commandLine "node", script.absolutePath, "--force"
|
|
70
|
+
}.standardOutput.asText.get()
|
|
71
|
+
logger.lifecycle(banner.trim())
|
|
72
|
+
}
|
|
73
|
+
} catch (Exception e) {
|
|
74
|
+
// Never fail the build over a banner (e.g. node not on PATH).
|
|
75
|
+
logger.info("auVideoBanner skipped: ${e.message}")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
afterEvaluate {
|
|
81
|
+
tasks.matching { it.name == "preBuild" }.configureEach {
|
|
82
|
+
dependsOn("auVideoBanner")
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { FlatList, StyleSheet, useWindowDimensions, View } from 'react-native';
|
|
5
|
+
import { useVideoManager } from "../provider/VideoContext.js";
|
|
6
|
+
import { VideoSurface } from "./VideoSurface.js";
|
|
7
|
+
|
|
8
|
+
/** Surface id a feed item registers under. */
|
|
9
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
|
+
export const feedSurfaceId = id => `feed:${id}`;
|
|
11
|
+
|
|
12
|
+
/** Minimal shape of a FlatList viewability token (avoids RN type-name drift). */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A single-engine video feed: render many videos in a scrollable list, but
|
|
16
|
+
* only the one scrolled into focus plays — the rest are stopped. There is
|
|
17
|
+
* still exactly ONE native player; scrolling hands it off to the focused
|
|
18
|
+
* item's surface, so memory and CPU stay flat no matter how long the feed.
|
|
19
|
+
*
|
|
20
|
+
* Each item is a plain `VideoSurface` (never a `VideoPlayer`, which would
|
|
21
|
+
* fight for the engine on mount). Focus is driven centrally from FlatList
|
|
22
|
+
* viewability.
|
|
23
|
+
*
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <VideoFeed
|
|
26
|
+
* data={videos} // [{ id, uri, title? }, …]
|
|
27
|
+
* renderOverlay={({ item, focused }) => <Caption title={item.title} />}
|
|
28
|
+
* />
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function VideoFeed({
|
|
32
|
+
data,
|
|
33
|
+
itemHeight,
|
|
34
|
+
resizeMode,
|
|
35
|
+
muted = true,
|
|
36
|
+
repeat = true,
|
|
37
|
+
visibilityThreshold = 80,
|
|
38
|
+
renderOverlay,
|
|
39
|
+
onFocusChange,
|
|
40
|
+
pauseOnUnmount = true,
|
|
41
|
+
isFocused,
|
|
42
|
+
...rest
|
|
43
|
+
}) {
|
|
44
|
+
const manager = useVideoManager();
|
|
45
|
+
const {
|
|
46
|
+
height: windowHeight
|
|
47
|
+
} = useWindowDimensions();
|
|
48
|
+
const height = itemHeight ?? windowHeight;
|
|
49
|
+
const [focusedId, setFocusedId] = useState(null);
|
|
50
|
+
|
|
51
|
+
// Global engine settings — apply to whatever is currently playing.
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (muted) {
|
|
54
|
+
manager.mute();
|
|
55
|
+
} else {
|
|
56
|
+
manager.unmute();
|
|
57
|
+
}
|
|
58
|
+
}, [manager, muted]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
manager.setRepeat(repeat);
|
|
61
|
+
}, [manager, repeat]);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (resizeMode) {
|
|
64
|
+
manager.setResizeMode(resizeMode);
|
|
65
|
+
}
|
|
66
|
+
}, [manager, resizeMode]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
return () => {
|
|
69
|
+
if (pauseOnUnmount) {
|
|
70
|
+
manager.pause();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}, [manager, pauseOnUnmount]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (isFocused === undefined) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!isFocused) {
|
|
79
|
+
if (manager.store.getState().surfaceId === feedSurfaceId(focusedId ?? '')) {
|
|
80
|
+
manager.pause();
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Screen regained focus — resume the item that was in view.
|
|
85
|
+
const item = data.find(d => d.id === focusedId);
|
|
86
|
+
if (item) {
|
|
87
|
+
manager.setSource(item, {
|
|
88
|
+
autoplay: true,
|
|
89
|
+
surfaceId: feedSurfaceId(item.id)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}, [manager, isFocused, focusedId, data]);
|
|
93
|
+
const focus = useCallback((item, index) => {
|
|
94
|
+
setFocusedId(item?.id ?? null);
|
|
95
|
+
if (item) {
|
|
96
|
+
// Same-id handoff aware: re-focusing the same video never reloads.
|
|
97
|
+
// A different id replaces the source, which stops the previous one —
|
|
98
|
+
// so only the focused video ever plays.
|
|
99
|
+
manager.setSource(item, {
|
|
100
|
+
autoplay: true,
|
|
101
|
+
surfaceId: feedSurfaceId(item.id)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
onFocusChange?.(item, index);
|
|
105
|
+
}, [manager, onFocusChange]);
|
|
106
|
+
|
|
107
|
+
// FlatList forbids changing these on the fly, so keep them stable and read
|
|
108
|
+
// the latest `focus`/`data` through a ref.
|
|
109
|
+
const latest = useRef({
|
|
110
|
+
focus,
|
|
111
|
+
data
|
|
112
|
+
});
|
|
113
|
+
latest.current = {
|
|
114
|
+
focus,
|
|
115
|
+
data
|
|
116
|
+
};
|
|
117
|
+
const viewabilityConfig = useRef({
|
|
118
|
+
itemVisiblePercentThreshold: visibilityThreshold,
|
|
119
|
+
minimumViewTime: 100
|
|
120
|
+
}).current;
|
|
121
|
+
const onViewableItemsChanged = useRef(({
|
|
122
|
+
viewableItems
|
|
123
|
+
}) => {
|
|
124
|
+
const focusedToken = viewableItems.find(v => v.isViewable);
|
|
125
|
+
if (focusedToken) {
|
|
126
|
+
latest.current.focus(focusedToken.item, focusedToken.index ?? 0);
|
|
127
|
+
}
|
|
128
|
+
}).current;
|
|
129
|
+
const renderItem = useCallback(({
|
|
130
|
+
item,
|
|
131
|
+
index
|
|
132
|
+
}) => /*#__PURE__*/_jsxs(View, {
|
|
133
|
+
style: [styles.item, {
|
|
134
|
+
height
|
|
135
|
+
}],
|
|
136
|
+
children: [/*#__PURE__*/_jsx(VideoSurface, {
|
|
137
|
+
surfaceId: feedSurfaceId(item.id),
|
|
138
|
+
style: StyleSheet.absoluteFill
|
|
139
|
+
}), renderOverlay?.({
|
|
140
|
+
item,
|
|
141
|
+
index,
|
|
142
|
+
focused: item.id === focusedId
|
|
143
|
+
})]
|
|
144
|
+
}), [height, renderOverlay, focusedId]);
|
|
145
|
+
return /*#__PURE__*/_jsx(FlatList, {
|
|
146
|
+
data: data,
|
|
147
|
+
keyExtractor: item => item.id,
|
|
148
|
+
renderItem: renderItem,
|
|
149
|
+
onViewableItemsChanged: onViewableItemsChanged,
|
|
150
|
+
viewabilityConfig: viewabilityConfig,
|
|
151
|
+
getItemLayout: (_, index) => ({
|
|
152
|
+
length: height,
|
|
153
|
+
offset: height * index,
|
|
154
|
+
index
|
|
155
|
+
}),
|
|
156
|
+
showsVerticalScrollIndicator: false,
|
|
157
|
+
snapToInterval: height,
|
|
158
|
+
snapToAlignment: "start",
|
|
159
|
+
decelerationRate: "fast",
|
|
160
|
+
...rest
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const styles = StyleSheet.create({
|
|
164
|
+
item: {
|
|
165
|
+
width: '100%',
|
|
166
|
+
backgroundColor: '#000',
|
|
167
|
+
overflow: 'hidden'
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
//# sourceMappingURL=VideoFeed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["useCallback","useEffect","useRef","useState","FlatList","StyleSheet","useWindowDimensions","View","useVideoManager","VideoSurface","jsx","_jsx","jsxs","_jsxs","feedSurfaceId","id","VideoFeed","data","itemHeight","resizeMode","muted","repeat","visibilityThreshold","renderOverlay","onFocusChange","pauseOnUnmount","isFocused","rest","manager","height","windowHeight","focusedId","setFocusedId","mute","unmute","setRepeat","setResizeMode","pause","undefined","store","getState","surfaceId","item","find","d","setSource","autoplay","focus","index","latest","current","viewabilityConfig","itemVisiblePercentThreshold","minimumViewTime","onViewableItemsChanged","viewableItems","focusedToken","v","isViewable","renderItem","style","styles","children","absoluteFill","focused","keyExtractor","getItemLayout","_","length","offset","showsVerticalScrollIndicator","snapToInterval","snapToAlignment","decelerationRate","create","width","backgroundColor","overflow"],"sourceRoot":"../../../src","sources":["components/VideoFeed.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { useEffect } from 'react';
|
|
4
|
-
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
|
4
|
+
import { AppState, StyleSheet, View } from 'react-native';
|
|
5
5
|
import { useVideoManager } from "../provider/VideoContext.js";
|
|
6
|
+
import { useVideoEvents } from "../hooks/useVideoEvents.js";
|
|
6
7
|
import { VideoControls } from "./VideoControls.js";
|
|
7
8
|
import { VideoSurface } from "./VideoSurface.js";
|
|
8
9
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -14,23 +15,42 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
15
|
* ```tsx
|
|
15
16
|
* <VideoPlayer source={{ id: '123', uri }} style={{ aspectRatio: 16 / 9 }} />
|
|
16
17
|
* ```
|
|
18
|
+
*
|
|
19
|
+
* `ref` exposes the underlying `VideoManager` for imperative control
|
|
20
|
+
* (`ref.current.play()`, `.seek()`, …) — the same instance `useVideo()`
|
|
21
|
+
* returns elsewhere in the app.
|
|
17
22
|
*/
|
|
18
|
-
export
|
|
23
|
+
export const VideoPlayer = /*#__PURE__*/forwardRef(({
|
|
19
24
|
source,
|
|
20
25
|
autoplay = true,
|
|
21
26
|
surfaceId,
|
|
22
27
|
controls = true,
|
|
23
28
|
resizeMode,
|
|
29
|
+
repeat,
|
|
30
|
+
muted,
|
|
24
31
|
orientation,
|
|
25
32
|
fullscreenOrientation,
|
|
33
|
+
pauseOnFocusLost = true,
|
|
34
|
+
isFocused,
|
|
35
|
+
onLoadComplete,
|
|
36
|
+
onBuffering,
|
|
37
|
+
onError,
|
|
26
38
|
style,
|
|
27
39
|
...rest
|
|
28
|
-
}) {
|
|
40
|
+
}, ref) => {
|
|
29
41
|
const manager = useVideoManager();
|
|
30
42
|
const id = surfaceId ?? `player:${source.id}`;
|
|
43
|
+
|
|
44
|
+
// Read the latest source without retriggering effects on every render
|
|
45
|
+
// (source is usually a fresh object literal each render).
|
|
46
|
+
const sourceRef = useRef(source);
|
|
47
|
+
sourceRef.current = source;
|
|
48
|
+
useImperativeHandle(ref, () => manager, [manager]);
|
|
31
49
|
useEffect(() => {
|
|
50
|
+
// Don't autoplay a player that mounts already unfocused (isFocused
|
|
51
|
+
// false), else it'd flash play → pause on the next screen.
|
|
32
52
|
manager.setSource(source, {
|
|
33
|
-
autoplay,
|
|
53
|
+
autoplay: autoplay && isFocused !== false,
|
|
34
54
|
surfaceId: id
|
|
35
55
|
});
|
|
36
56
|
// Attach on mount / when the video identity changes. Other source
|
|
@@ -42,6 +62,22 @@ export function VideoPlayer({
|
|
|
42
62
|
manager.setResizeMode(resizeMode);
|
|
43
63
|
}
|
|
44
64
|
}, [manager, resizeMode]);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (repeat === undefined) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
manager.setRepeat(repeat);
|
|
70
|
+
}, [manager, repeat]);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (muted === undefined) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (muted) {
|
|
76
|
+
manager.mute();
|
|
77
|
+
} else {
|
|
78
|
+
manager.unmute();
|
|
79
|
+
}
|
|
80
|
+
}, [manager, muted]);
|
|
45
81
|
useEffect(() => {
|
|
46
82
|
if (!orientation || orientation === 'auto') {
|
|
47
83
|
return;
|
|
@@ -56,6 +92,47 @@ export function VideoPlayer({
|
|
|
56
92
|
manager.setFullscreenOrientation(fullscreenOrientation);
|
|
57
93
|
return () => manager.setFullscreenOrientation(null);
|
|
58
94
|
}, [manager, fullscreenOrientation]);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!pauseOnFocusLost) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const sub = AppState.addEventListener('change', next => {
|
|
100
|
+
// Only the player currently owning the engine reacts, so a video
|
|
101
|
+
// attached to another surface keeps playing untouched.
|
|
102
|
+
if (next !== 'active' && manager.store.getState().surfaceId === id) {
|
|
103
|
+
manager.pause();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return () => sub.remove();
|
|
107
|
+
}, [manager, id, pauseOnFocusLost]);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (isFocused === undefined) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const state = manager.store.getState();
|
|
113
|
+
if (isFocused) {
|
|
114
|
+
// Regained screen focus. If the engine is still ours, just resume;
|
|
115
|
+
// otherwise it moved to another video while we were blurred — reclaim
|
|
116
|
+
// it (same-id handoff means no reload if it never actually left).
|
|
117
|
+
if (state.surfaceId === id && state.currentVideo?.id === sourceRef.current.id) {
|
|
118
|
+
manager.play();
|
|
119
|
+
} else {
|
|
120
|
+
manager.setSource(sourceRef.current, {
|
|
121
|
+
autoplay: true,
|
|
122
|
+
surfaceId: id
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
} else if (state.surfaceId === id) {
|
|
126
|
+
// Lost screen focus while playing our video — pause. Guarded so we
|
|
127
|
+
// never pause a video that has already handed off elsewhere.
|
|
128
|
+
manager.pause();
|
|
129
|
+
}
|
|
130
|
+
}, [manager, id, isFocused]);
|
|
131
|
+
useVideoEvents({
|
|
132
|
+
onLoad: onLoadComplete,
|
|
133
|
+
onBuffer: e => onBuffering?.(e.buffering),
|
|
134
|
+
onError
|
|
135
|
+
});
|
|
59
136
|
return /*#__PURE__*/_jsxs(View, {
|
|
60
137
|
style: [styles.container, style],
|
|
61
138
|
...rest,
|
|
@@ -64,7 +141,8 @@ export function VideoPlayer({
|
|
|
64
141
|
style: styles.surface
|
|
65
142
|
}), controls ? /*#__PURE__*/_jsx(VideoControls, {}) : null]
|
|
66
143
|
});
|
|
67
|
-
}
|
|
144
|
+
});
|
|
145
|
+
VideoPlayer.displayName = 'VideoPlayer';
|
|
68
146
|
const styles = StyleSheet.create({
|
|
69
147
|
container: {
|
|
70
148
|
backgroundColor: '#000',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["useEffect","StyleSheet","View","useVideoManager","VideoControls","VideoSurface","jsx","_jsx","jsxs","_jsxs","VideoPlayer","source","autoplay","surfaceId","controls","resizeMode","orientation","fullscreenOrientation","style","rest","manager","id","setSource","setResizeMode","setOrientation","setFullscreenOrientation","styles","container","children","surface","create","backgroundColor","overflow","position","top","left","right","bottom"],"sourceRoot":"../../../src","sources":["components/VideoPlayer.tsx"],"mappings":";;AAAA,SAASA,SAAS,QAAQ,OAAO;
|
|
1
|
+
{"version":3,"names":["forwardRef","useEffect","useImperativeHandle","useRef","AppState","StyleSheet","View","useVideoManager","useVideoEvents","VideoControls","VideoSurface","jsx","_jsx","jsxs","_jsxs","VideoPlayer","source","autoplay","surfaceId","controls","resizeMode","repeat","muted","orientation","fullscreenOrientation","pauseOnFocusLost","isFocused","onLoadComplete","onBuffering","onError","style","rest","ref","manager","id","sourceRef","current","setSource","setResizeMode","undefined","setRepeat","mute","unmute","setOrientation","setFullscreenOrientation","sub","addEventListener","next","store","getState","pause","remove","state","currentVideo","play","onLoad","onBuffer","e","buffering","styles","container","children","surface","displayName","create","backgroundColor","overflow","position","top","left","right","bottom"],"sourceRoot":"../../../src","sources":["components/VideoPlayer.tsx"],"mappings":"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","ignoreList":[]}
|
package/lib/module/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { useVideoManager } from "./provider/VideoContext.js";
|
|
|
5
5
|
export { VideoManager, FULLSCREEN_SURFACE_ID, FLOATING_SURFACE_ID } from "./core/VideoManager.js";
|
|
6
6
|
export { VideoSurface } from "./components/VideoSurface.js";
|
|
7
7
|
export { VideoPlayer } from "./components/VideoPlayer.js";
|
|
8
|
+
export { VideoFeed, feedSurfaceId } from "./components/VideoFeed.js";
|
|
8
9
|
export { FullscreenPlayer } from "./components/FullscreenPlayer.js";
|
|
9
10
|
export { FloatingPlayer } from "./components/FloatingPlayer.js";
|
|
10
11
|
export { MiniPlayer } from "./components/MiniPlayer.js";
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["VideoProvider","useVideoManager","VideoManager","FULLSCREEN_SURFACE_ID","FLOATING_SURFACE_ID","VideoSurface","VideoPlayer","FullscreenPlayer","FloatingPlayer","MiniPlayer","VideoControls","GestureOverlay","useVideo","usePlayback","useFullscreen","usePiP","useVideoEvents","formatTime"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,SACEA,aAAa,QAER,6BAA0B;AACjC,SAASC,eAAe,QAAQ,4BAAyB;AACzD,SACEC,YAAY,EACZC,qBAAqB,EACrBC,mBAAmB,QACd,wBAAqB;AAE5B,SACEC,YAAY,QAEP,8BAA2B;AAClC,SAASC,WAAW,QAA+B,6BAA0B;AAC7E,SAASC,gBAAgB,QAAQ,kCAA+B;AAChE,SACEC,cAAc,QAET,gCAA6B;AACpC,SAASC,UAAU,QAA8B,4BAAyB;AAC1E,SACEC,aAAa,QAER,+BAA4B;AACnC,SACEC,cAAc,QAET,gCAA6B;AAEpC,SAASC,QAAQ,QAAQ,qBAAkB;AAC3C,SAASC,WAAW,QAAQ,wBAAqB;AACjD,SAASC,aAAa,QAAQ,0BAAuB;AACrD,SAASC,MAAM,QAAQ,mBAAgB;AACvC,SACEC,cAAc,QAET,2BAAwB;AAE/B,SAASC,UAAU,QAAQ,uBAAoB","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["VideoProvider","useVideoManager","VideoManager","FULLSCREEN_SURFACE_ID","FLOATING_SURFACE_ID","VideoSurface","VideoPlayer","VideoFeed","feedSurfaceId","FullscreenPlayer","FloatingPlayer","MiniPlayer","VideoControls","GestureOverlay","useVideo","usePlayback","useFullscreen","usePiP","useVideoEvents","formatTime"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,SACEA,aAAa,QAER,6BAA0B;AACjC,SAASC,eAAe,QAAQ,4BAAyB;AACzD,SACEC,YAAY,EACZC,qBAAqB,EACrBC,mBAAmB,QACd,wBAAqB;AAE5B,SACEC,YAAY,QAEP,8BAA2B;AAClC,SAASC,WAAW,QAA+B,6BAA0B;AAC7E,SACEC,SAAS,EACTC,aAAa,QAGR,2BAAwB;AAC/B,SAASC,gBAAgB,QAAQ,kCAA+B;AAChE,SACEC,cAAc,QAET,gCAA6B;AACpC,SAASC,UAAU,QAA8B,4BAAyB;AAC1E,SACEC,aAAa,QAER,+BAA4B;AACnC,SACEC,cAAc,QAET,gCAA6B;AAEpC,SAASC,QAAQ,QAAQ,qBAAkB;AAC3C,SAASC,WAAW,QAAQ,wBAAqB;AACjD,SAASC,aAAa,QAAQ,0BAAuB;AACrD,SAASC,MAAM,QAAQ,mBAAgB;AACvC,SACEC,cAAc,QAET,2BAAwB;AAE/B,SAASC,UAAU,QAAQ,uBAAoB","ignoreList":[]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type FlatListProps } from 'react-native';
|
|
3
|
+
import type { ResizeMode, VideoSource } from '../types/video.js';
|
|
4
|
+
/** Surface id a feed item registers under. */
|
|
5
|
+
export declare const feedSurfaceId: (id: string) => string;
|
|
6
|
+
export interface VideoFeedRenderInfo<T> {
|
|
7
|
+
item: T;
|
|
8
|
+
index: number;
|
|
9
|
+
/** True when this item is the one currently playing. */
|
|
10
|
+
focused: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface VideoFeedProps<T extends VideoSource> extends Omit<FlatListProps<T>, 'data' | 'renderItem' | 'keyExtractor' | 'onViewableItemsChanged' | 'viewabilityConfig' | 'getItemLayout'> {
|
|
13
|
+
/** The videos. Each needs a stable `id` (used for the surface + handoff). */
|
|
14
|
+
data: T[];
|
|
15
|
+
/** Height of each item. Default: the window height (a full-screen reels feed). */
|
|
16
|
+
itemHeight?: number;
|
|
17
|
+
resizeMode?: ResizeMode;
|
|
18
|
+
/** Start muted. Default `true` (autoplay policies + typical feed UX). */
|
|
19
|
+
muted?: boolean;
|
|
20
|
+
/** Loop the focused video. Default `true`. */
|
|
21
|
+
repeat?: boolean;
|
|
22
|
+
/** Percent of an item that must be visible to become focused. Default 80. */
|
|
23
|
+
visibilityThreshold?: number;
|
|
24
|
+
/** Overlay above each item — captions, like button, a tap-to-pause layer, … */
|
|
25
|
+
renderOverlay?: (info: VideoFeedRenderInfo<T>) => ReactNode;
|
|
26
|
+
/** Called when the focused item changes (null when none is visible). */
|
|
27
|
+
onFocusChange?: (item: T | null, index: number) => void;
|
|
28
|
+
/** Pause playback when the feed unmounts. Default `true`. */
|
|
29
|
+
pauseOnUnmount?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Screen-focus flag from your navigation library — e.g. React Navigation's
|
|
32
|
+
* `useIsFocused()`. `false` pauses the feed; returning to `true` resumes the
|
|
33
|
+
* item that was in view. Needed because navigating away keeps the feed
|
|
34
|
+
* mounted and the app foregrounded. Leave undefined if you don't navigate.
|
|
35
|
+
*/
|
|
36
|
+
isFocused?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A single-engine video feed: render many videos in a scrollable list, but
|
|
40
|
+
* only the one scrolled into focus plays — the rest are stopped. There is
|
|
41
|
+
* still exactly ONE native player; scrolling hands it off to the focused
|
|
42
|
+
* item's surface, so memory and CPU stay flat no matter how long the feed.
|
|
43
|
+
*
|
|
44
|
+
* Each item is a plain `VideoSurface` (never a `VideoPlayer`, which would
|
|
45
|
+
* fight for the engine on mount). Focus is driven centrally from FlatList
|
|
46
|
+
* viewability.
|
|
47
|
+
*
|
|
48
|
+
* ```tsx
|
|
49
|
+
* <VideoFeed
|
|
50
|
+
* data={videos} // [{ id, uri, title? }, …]
|
|
51
|
+
* renderOverlay={({ item, focused }) => <Caption title={item.title} />}
|
|
52
|
+
* />
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function VideoFeed<T extends VideoSource>({ data, itemHeight, resizeMode, muted, repeat, visibilityThreshold, renderOverlay, onFocusChange, pauseOnUnmount, isFocused, ...rest }: VideoFeedProps<T>): import("react").JSX.Element;
|
|
56
|
+
//# sourceMappingURL=VideoFeed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VideoFeed.d.ts","sourceRoot":"","sources":["../../../../src/components/VideoFeed.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAgB,CAAC;AAG9D,8CAA8C;AAC9C,eAAO,MAAM,aAAa,GAAI,IAAI,MAAM,WAAiB,CAAC;AAS1D,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC,IAAI,EAAE,CAAC,CAAC;IACR,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,WAAW,CAAE,SAAQ,IAAI,CACjE,aAAa,CAAC,CAAC,CAAC,EACd,MAAM,GACN,YAAY,GACZ,cAAc,GACd,wBAAwB,GACxB,mBAAmB,GACnB,eAAe,CAClB;IACC,6EAA6E;IAC7E,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,yEAAyE;IACzE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,8CAA8C;IAC9C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,+EAA+E;IAC/E,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC;IAC5D,wEAAwE;IACxE,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD,6DAA6D;IAC7D,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,WAAW,EAAE,EAC/C,IAAI,EACJ,UAAU,EACV,UAAU,EACV,KAAY,EACZ,MAAa,EACb,mBAAwB,EACxB,aAAa,EACb,aAAa,EACb,cAAqB,EACrB,SAAS,EACT,GAAG,IAAI,EACR,EAAE,cAAc,CAAC,CAAC,CAAC,+BA2HnB"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ViewProps } from 'react-native';
|
|
2
|
-
import
|
|
2
|
+
import { VideoManager } from '../core/VideoManager.js';
|
|
3
|
+
import type { VideoEventMap } from '../types/events.js';
|
|
4
|
+
import type { OrientationLock, ResizeMode, VideoError, VideoSource } from '../types/video.js';
|
|
3
5
|
export interface VideoPlayerProps extends ViewProps {
|
|
4
6
|
source: VideoSource;
|
|
5
7
|
/** Default true. */
|
|
@@ -13,6 +15,10 @@ export interface VideoPlayerProps extends ViewProps {
|
|
|
13
15
|
/** Show built-in controls. Default true. */
|
|
14
16
|
controls?: boolean;
|
|
15
17
|
resizeMode?: ResizeMode;
|
|
18
|
+
/** Loop playback when it ends. Default false. */
|
|
19
|
+
repeat?: boolean;
|
|
20
|
+
/** Default false. */
|
|
21
|
+
muted?: boolean;
|
|
16
22
|
/**
|
|
17
23
|
* Force the screen into this orientation while the player is mounted:
|
|
18
24
|
* `'landscape'`, `'inverted-landscape'`, `'portrait'` or
|
|
@@ -26,6 +32,32 @@ export interface VideoPlayerProps extends ViewProps {
|
|
|
26
32
|
* when it closes, so the rest of the app is unaffected.
|
|
27
33
|
*/
|
|
28
34
|
fullscreenOrientation?: OrientationLock;
|
|
35
|
+
/**
|
|
36
|
+
* Pause this player when it loses focus — i.e. the app goes to the
|
|
37
|
+
* background while this player's surface is the one currently playing.
|
|
38
|
+
* Default `true`. Set `false` to keep playing (e.g. background audio).
|
|
39
|
+
* Only the focused player acts, so a video attached elsewhere is untouched.
|
|
40
|
+
*/
|
|
41
|
+
pauseOnFocusLost?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Screen-focus flag from your navigation library — e.g. React Navigation's
|
|
44
|
+
* `useIsFocused()`. `false` pauses this player; returning to `true` reclaims
|
|
45
|
+
* the engine and resumes this video. This is the only reliable way to pause
|
|
46
|
+
* on screen navigation (React Navigation keeps screens mounted and the app
|
|
47
|
+
* stays foregrounded, so unmount/AppState never fire). Leave undefined if
|
|
48
|
+
* you don't use navigation.
|
|
49
|
+
*
|
|
50
|
+
* ```tsx
|
|
51
|
+
* const isFocused = useIsFocused(); // @react-navigation/native
|
|
52
|
+
* <VideoPlayer source={video} isFocused={isFocused} />
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
isFocused?: boolean;
|
|
56
|
+
/** Fires once metadata (duration, dimensions) is available. */
|
|
57
|
+
onLoadComplete?: (info: VideoEventMap['onLoad']) => void;
|
|
58
|
+
/** Fires whenever buffering starts or stops. */
|
|
59
|
+
onBuffering?: (buffering: boolean) => void;
|
|
60
|
+
onError?: (error: VideoError) => void;
|
|
29
61
|
}
|
|
30
62
|
/**
|
|
31
63
|
* Convenience all-in-one player: `setSource` (same-video handoff aware) +
|
|
@@ -35,6 +67,10 @@ export interface VideoPlayerProps extends ViewProps {
|
|
|
35
67
|
* ```tsx
|
|
36
68
|
* <VideoPlayer source={{ id: '123', uri }} style={{ aspectRatio: 16 / 9 }} />
|
|
37
69
|
* ```
|
|
70
|
+
*
|
|
71
|
+
* `ref` exposes the underlying `VideoManager` for imperative control
|
|
72
|
+
* (`ref.current.play()`, `.seek()`, …) — the same instance `useVideo()`
|
|
73
|
+
* returns elsewhere in the app.
|
|
38
74
|
*/
|
|
39
|
-
export declare
|
|
75
|
+
export declare const VideoPlayer: import("react").ForwardRefExoticComponent<VideoPlayerProps & import("react").RefAttributes<VideoManager>>;
|
|
40
76
|
//# sourceMappingURL=VideoPlayer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../../../../src/components/VideoPlayer.tsx"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../../../../src/components/VideoPlayer.tsx"],"names":[],"mappings":"AACA,OAAO,EAA8B,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAsB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAiB,CAAC;AACrD,OAAO,KAAK,EACV,eAAe,EACf,UAAU,EACV,UAAU,EACV,WAAW,EACZ,MAAM,mBAAgB,CAAC;AAIxB,MAAM,WAAW,gBAAiB,SAAQ,SAAS;IACjD,MAAM,EAAE,WAAW,CAAC;IACpB,oBAAoB;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,iDAAiD;IACjD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qBAAqB;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,eAAe,CAAC;IACxC;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;IACzD,gDAAgD;IAChD,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACvC;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,2GA0IvB,CAAC"}
|
|
@@ -3,6 +3,7 @@ export { useVideoManager } from './provider/VideoContext.js';
|
|
|
3
3
|
export { VideoManager, FULLSCREEN_SURFACE_ID, FLOATING_SURFACE_ID, } from './core/VideoManager.js';
|
|
4
4
|
export { VideoSurface, type VideoSurfaceProps, } from './components/VideoSurface.js';
|
|
5
5
|
export { VideoPlayer, type VideoPlayerProps } from './components/VideoPlayer.js';
|
|
6
|
+
export { VideoFeed, feedSurfaceId, type VideoFeedProps, type VideoFeedRenderInfo, } from './components/VideoFeed.js';
|
|
6
7
|
export { FullscreenPlayer } from './components/FullscreenPlayer.js';
|
|
7
8
|
export { FloatingPlayer, type FloatingPlayerProps, } from './components/FloatingPlayer.js';
|
|
8
9
|
export { MiniPlayer, type MiniPlayerProps } from './components/MiniPlayer.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,KAAK,kBAAkB,GACxB,MAAM,6BAA0B,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,MAAM,4BAAyB,CAAC;AAC1D,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,wBAAqB,CAAC;AAE7B,OAAO,EACL,YAAY,EACZ,KAAK,iBAAiB,GACvB,MAAM,8BAA2B,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,6BAA0B,CAAC;AAC9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAA+B,CAAC;AACjE,OAAO,EACL,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,gCAA6B,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,4BAAyB,CAAC;AAC3E,OAAO,EACL,aAAa,EACb,KAAK,kBAAkB,GACxB,MAAM,+BAA4B,CAAC;AACpC,OAAO,EACL,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,gCAA6B,CAAC;AAErC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAkB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAqB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAAuB,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAgB,CAAC;AACxC,OAAO,EACL,cAAc,EACd,KAAK,kBAAkB,GACxB,MAAM,2BAAwB,CAAC;AAEhC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAoB,CAAC;AAEhD,YAAY,EACV,WAAW,EACX,UAAU,EACV,eAAe,EACf,UAAU,EACV,cAAc,EACd,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,cAAc,GACf,MAAM,kBAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,KAAK,kBAAkB,GACxB,MAAM,6BAA0B,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,MAAM,4BAAyB,CAAC;AAC1D,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,wBAAqB,CAAC;AAE7B,OAAO,EACL,YAAY,EACZ,KAAK,iBAAiB,GACvB,MAAM,8BAA2B,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,6BAA0B,CAAC;AAC9E,OAAO,EACL,SAAS,EACT,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,2BAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAA+B,CAAC;AACjE,OAAO,EACL,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,gCAA6B,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,4BAAyB,CAAC;AAC3E,OAAO,EACL,aAAa,EACb,KAAK,kBAAkB,GACxB,MAAM,+BAA4B,CAAC;AACpC,OAAO,EACL,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,gCAA6B,CAAC;AAErC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAkB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAqB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAAuB,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAgB,CAAC;AACxC,OAAO,EACL,cAAc,EACd,KAAK,kBAAkB,GACxB,MAAM,2BAAwB,CAAC;AAEhC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAoB,CAAC;AAEhD,YAAY,EACV,WAAW,EACX,UAAU,EACV,eAAe,EACf,UAAU,EACV,cAAc,EACd,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,cAAc,GACf,MAAM,kBAAS,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-video-provider",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Singleton-engine video library for React Native (one native player, many surfaces)",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"android",
|
|
19
19
|
"ios",
|
|
20
20
|
"cpp",
|
|
21
|
+
"scripts/postinstall.js",
|
|
21
22
|
"*.podspec",
|
|
22
23
|
"react-native.config.js",
|
|
23
24
|
"!ios/build",
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
"example": "yarn workspace react-native-video-provider-example",
|
|
36
37
|
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
|
|
37
38
|
"prepare": "bob build",
|
|
39
|
+
"postinstall": "node scripts/postinstall.js",
|
|
38
40
|
"typecheck": "tsc",
|
|
39
41
|
"test": "jest",
|
|
40
42
|
"lint": "eslint \"**/*.{js,ts,tsx}\""
|
|
@@ -46,14 +48,14 @@
|
|
|
46
48
|
],
|
|
47
49
|
"repository": {
|
|
48
50
|
"type": "git",
|
|
49
|
-
"url": "git+https://github.com/
|
|
51
|
+
"url": "git+https://github.com/gurdeep99/react-native-au-video.git"
|
|
50
52
|
},
|
|
51
53
|
"author": "Gurdeep Singh <grdp99@gmail.com> (https://gurdeep.net)",
|
|
52
54
|
"license": "MIT",
|
|
53
55
|
"bugs": {
|
|
54
|
-
"url": "https://github.com/
|
|
56
|
+
"url": "https://github.com/gurdeep99/react-native-au-video/issues"
|
|
55
57
|
},
|
|
56
|
-
"homepage": "https://github.com/
|
|
58
|
+
"homepage": "https://github.com/gurdeep99/react-native-au-video#readme",
|
|
57
59
|
"publishConfig": {
|
|
58
60
|
"registry": "https://registry.npmjs.org/"
|
|
59
61
|
},
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prints a banner. Wrapped so it can NEVER fail a consumer's `npm install`
|
|
5
|
+
* or native build — any error is swallowed and the process always exits 0.
|
|
6
|
+
*
|
|
7
|
+
* Run modes:
|
|
8
|
+
* node scripts/postinstall.js → install banner (skipped in CI /
|
|
9
|
+
* non-interactive shells)
|
|
10
|
+
* node scripts/postinstall.js --force → always print (used by the
|
|
11
|
+
* Android build so it shows while
|
|
12
|
+
* compiling the app, where stdout
|
|
13
|
+
* is not a TTY)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const force =
|
|
17
|
+
process.argv.includes('--force') || process.env.AU_VIDEO_BANNER === '1';
|
|
18
|
+
|
|
19
|
+
const BANNER = `
|
|
20
|
+
++
|
|
21
|
+
+++ +++ +++++++++++ ++
|
|
22
|
+
++++++++++++++ ++++++++++ ++++++++++++++ +++ +++++++ ++ +++
|
|
23
|
+
++ +++ +++ ++++++++++++ +++ +++++ ++++ +++++++ ++++ +++++++ ++ +++
|
|
24
|
+
++ +++ +++ ++++ +++ +++++++++++++ ++++ +++++ ++++ +++++ +++++++ ++ +++++++++++
|
|
25
|
+
+++ +++ +++ ++++ ++++ +++ +++++++ +++++++ +++++++++ +++++++++ ++++ ++++
|
|
26
|
+
++++ +++ +++ ++++++++++ ++++++ +++ ++++++ +++++ +++ ++ +++ +++
|
|
27
|
+
+++ ++ +++ ++++ +++ +++ ++++++++ ++++ ++++ +++ ++++ +++ +++ +++
|
|
28
|
+
++++ ++++ ++++ +++ ++ +++++ ++ +++ ++++
|
|
29
|
+
+++++++ ++++ ++ ++++++ ++++++++++
|
|
30
|
+
++++ ++++ + +++ ++++ + +++++++++++
|
|
31
|
+
+++ +++ ++++ +++++ ++++ +++ +++ ++++ ++++ +++
|
|
32
|
+
++++++++ ++ ++ ++ +++ + ++
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const quiet = process.env.CI || process.env.npm_config_loglevel === 'silent';
|
|
37
|
+
const show = force || (!quiet && process.stdout && process.stdout.isTTY);
|
|
38
|
+
if (show) {
|
|
39
|
+
// Colorize only in a real terminal — under Gradle/CI (--force, not a TTY)
|
|
40
|
+
// stay plain so the log doesn't fill with raw escape codes.
|
|
41
|
+
const color = process.stdout && process.stdout.isTTY;
|
|
42
|
+
const cyan = color ? '\x1b[36m' : '';
|
|
43
|
+
const dim = color ? '\x1b[2m' : '';
|
|
44
|
+
const reset = color ? '\x1b[0m' : '';
|
|
45
|
+
process.stdout.write(cyan + BANNER + reset + '\n');
|
|
46
|
+
process.stdout.write(
|
|
47
|
+
dim +
|
|
48
|
+
' react-native-video-provider — one native player, many surfaces.\n' +
|
|
49
|
+
' Docs: https://github.com/gurdeep99/react-native-video-provider#readme\n' +
|
|
50
|
+
reset +
|
|
51
|
+
'\n'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Never break an install over a banner.
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
process.exit(0);
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import {
|
|
9
|
+
FlatList,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
useWindowDimensions,
|
|
12
|
+
View,
|
|
13
|
+
type FlatListProps,
|
|
14
|
+
} from 'react-native';
|
|
15
|
+
import { useVideoManager } from '../provider/VideoContext';
|
|
16
|
+
import type { ResizeMode, VideoSource } from '../types/video';
|
|
17
|
+
import { VideoSurface } from './VideoSurface';
|
|
18
|
+
|
|
19
|
+
/** Surface id a feed item registers under. */
|
|
20
|
+
export const feedSurfaceId = (id: string) => `feed:${id}`;
|
|
21
|
+
|
|
22
|
+
/** Minimal shape of a FlatList viewability token (avoids RN type-name drift). */
|
|
23
|
+
interface FeedViewToken {
|
|
24
|
+
item: unknown;
|
|
25
|
+
index: number | null | undefined;
|
|
26
|
+
isViewable: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface VideoFeedRenderInfo<T> {
|
|
30
|
+
item: T;
|
|
31
|
+
index: number;
|
|
32
|
+
/** True when this item is the one currently playing. */
|
|
33
|
+
focused: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface VideoFeedProps<T extends VideoSource> extends Omit<
|
|
37
|
+
FlatListProps<T>,
|
|
38
|
+
| 'data'
|
|
39
|
+
| 'renderItem'
|
|
40
|
+
| 'keyExtractor'
|
|
41
|
+
| 'onViewableItemsChanged'
|
|
42
|
+
| 'viewabilityConfig'
|
|
43
|
+
| 'getItemLayout'
|
|
44
|
+
> {
|
|
45
|
+
/** The videos. Each needs a stable `id` (used for the surface + handoff). */
|
|
46
|
+
data: T[];
|
|
47
|
+
/** Height of each item. Default: the window height (a full-screen reels feed). */
|
|
48
|
+
itemHeight?: number;
|
|
49
|
+
resizeMode?: ResizeMode;
|
|
50
|
+
/** Start muted. Default `true` (autoplay policies + typical feed UX). */
|
|
51
|
+
muted?: boolean;
|
|
52
|
+
/** Loop the focused video. Default `true`. */
|
|
53
|
+
repeat?: boolean;
|
|
54
|
+
/** Percent of an item that must be visible to become focused. Default 80. */
|
|
55
|
+
visibilityThreshold?: number;
|
|
56
|
+
/** Overlay above each item — captions, like button, a tap-to-pause layer, … */
|
|
57
|
+
renderOverlay?: (info: VideoFeedRenderInfo<T>) => ReactNode;
|
|
58
|
+
/** Called when the focused item changes (null when none is visible). */
|
|
59
|
+
onFocusChange?: (item: T | null, index: number) => void;
|
|
60
|
+
/** Pause playback when the feed unmounts. Default `true`. */
|
|
61
|
+
pauseOnUnmount?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Screen-focus flag from your navigation library — e.g. React Navigation's
|
|
64
|
+
* `useIsFocused()`. `false` pauses the feed; returning to `true` resumes the
|
|
65
|
+
* item that was in view. Needed because navigating away keeps the feed
|
|
66
|
+
* mounted and the app foregrounded. Leave undefined if you don't navigate.
|
|
67
|
+
*/
|
|
68
|
+
isFocused?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A single-engine video feed: render many videos in a scrollable list, but
|
|
73
|
+
* only the one scrolled into focus plays — the rest are stopped. There is
|
|
74
|
+
* still exactly ONE native player; scrolling hands it off to the focused
|
|
75
|
+
* item's surface, so memory and CPU stay flat no matter how long the feed.
|
|
76
|
+
*
|
|
77
|
+
* Each item is a plain `VideoSurface` (never a `VideoPlayer`, which would
|
|
78
|
+
* fight for the engine on mount). Focus is driven centrally from FlatList
|
|
79
|
+
* viewability.
|
|
80
|
+
*
|
|
81
|
+
* ```tsx
|
|
82
|
+
* <VideoFeed
|
|
83
|
+
* data={videos} // [{ id, uri, title? }, …]
|
|
84
|
+
* renderOverlay={({ item, focused }) => <Caption title={item.title} />}
|
|
85
|
+
* />
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function VideoFeed<T extends VideoSource>({
|
|
89
|
+
data,
|
|
90
|
+
itemHeight,
|
|
91
|
+
resizeMode,
|
|
92
|
+
muted = true,
|
|
93
|
+
repeat = true,
|
|
94
|
+
visibilityThreshold = 80,
|
|
95
|
+
renderOverlay,
|
|
96
|
+
onFocusChange,
|
|
97
|
+
pauseOnUnmount = true,
|
|
98
|
+
isFocused,
|
|
99
|
+
...rest
|
|
100
|
+
}: VideoFeedProps<T>) {
|
|
101
|
+
const manager = useVideoManager();
|
|
102
|
+
const { height: windowHeight } = useWindowDimensions();
|
|
103
|
+
const height = itemHeight ?? windowHeight;
|
|
104
|
+
const [focusedId, setFocusedId] = useState<string | null>(null);
|
|
105
|
+
|
|
106
|
+
// Global engine settings — apply to whatever is currently playing.
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (muted) {
|
|
109
|
+
manager.mute();
|
|
110
|
+
} else {
|
|
111
|
+
manager.unmute();
|
|
112
|
+
}
|
|
113
|
+
}, [manager, muted]);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
manager.setRepeat(repeat);
|
|
117
|
+
}, [manager, repeat]);
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (resizeMode) {
|
|
121
|
+
manager.setResizeMode(resizeMode);
|
|
122
|
+
}
|
|
123
|
+
}, [manager, resizeMode]);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
return () => {
|
|
127
|
+
if (pauseOnUnmount) {
|
|
128
|
+
manager.pause();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}, [manager, pauseOnUnmount]);
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (isFocused === undefined) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!isFocused) {
|
|
138
|
+
if (
|
|
139
|
+
manager.store.getState().surfaceId === feedSurfaceId(focusedId ?? '')
|
|
140
|
+
) {
|
|
141
|
+
manager.pause();
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Screen regained focus — resume the item that was in view.
|
|
146
|
+
const item = data.find((d) => d.id === focusedId);
|
|
147
|
+
if (item) {
|
|
148
|
+
manager.setSource(item, {
|
|
149
|
+
autoplay: true,
|
|
150
|
+
surfaceId: feedSurfaceId(item.id),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}, [manager, isFocused, focusedId, data]);
|
|
154
|
+
|
|
155
|
+
const focus = useCallback(
|
|
156
|
+
(item: T | null, index: number) => {
|
|
157
|
+
setFocusedId(item?.id ?? null);
|
|
158
|
+
if (item) {
|
|
159
|
+
// Same-id handoff aware: re-focusing the same video never reloads.
|
|
160
|
+
// A different id replaces the source, which stops the previous one —
|
|
161
|
+
// so only the focused video ever plays.
|
|
162
|
+
manager.setSource(item, {
|
|
163
|
+
autoplay: true,
|
|
164
|
+
surfaceId: feedSurfaceId(item.id),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
onFocusChange?.(item, index);
|
|
168
|
+
},
|
|
169
|
+
[manager, onFocusChange]
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// FlatList forbids changing these on the fly, so keep them stable and read
|
|
173
|
+
// the latest `focus`/`data` through a ref.
|
|
174
|
+
const latest = useRef({ focus, data });
|
|
175
|
+
latest.current = { focus, data };
|
|
176
|
+
|
|
177
|
+
const viewabilityConfig = useRef({
|
|
178
|
+
itemVisiblePercentThreshold: visibilityThreshold,
|
|
179
|
+
minimumViewTime: 100,
|
|
180
|
+
}).current;
|
|
181
|
+
|
|
182
|
+
const onViewableItemsChanged = useRef(
|
|
183
|
+
({ viewableItems }: { viewableItems: FeedViewToken[] }) => {
|
|
184
|
+
const focusedToken = viewableItems.find((v) => v.isViewable);
|
|
185
|
+
if (focusedToken) {
|
|
186
|
+
latest.current.focus(focusedToken.item as T, focusedToken.index ?? 0);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
).current;
|
|
190
|
+
|
|
191
|
+
const renderItem = useCallback(
|
|
192
|
+
({ item, index }: { item: T; index: number }) => (
|
|
193
|
+
<View style={[styles.item, { height }]}>
|
|
194
|
+
<VideoSurface
|
|
195
|
+
surfaceId={feedSurfaceId(item.id)}
|
|
196
|
+
style={StyleSheet.absoluteFill}
|
|
197
|
+
/>
|
|
198
|
+
{renderOverlay?.({ item, index, focused: item.id === focusedId })}
|
|
199
|
+
</View>
|
|
200
|
+
),
|
|
201
|
+
[height, renderOverlay, focusedId]
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<FlatList
|
|
206
|
+
data={data}
|
|
207
|
+
keyExtractor={(item) => item.id}
|
|
208
|
+
renderItem={renderItem}
|
|
209
|
+
onViewableItemsChanged={onViewableItemsChanged}
|
|
210
|
+
viewabilityConfig={viewabilityConfig}
|
|
211
|
+
getItemLayout={(_, index) => ({
|
|
212
|
+
length: height,
|
|
213
|
+
offset: height * index,
|
|
214
|
+
index,
|
|
215
|
+
})}
|
|
216
|
+
showsVerticalScrollIndicator={false}
|
|
217
|
+
snapToInterval={height}
|
|
218
|
+
snapToAlignment="start"
|
|
219
|
+
decelerationRate="fast"
|
|
220
|
+
{...rest}
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const styles = StyleSheet.create({
|
|
226
|
+
item: {
|
|
227
|
+
width: '100%',
|
|
228
|
+
backgroundColor: '#000',
|
|
229
|
+
overflow: 'hidden',
|
|
230
|
+
},
|
|
231
|
+
});
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
2
|
-
import { StyleSheet, View, type ViewProps } from 'react-native';
|
|
1
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
|
2
|
+
import { AppState, StyleSheet, View, type ViewProps } from 'react-native';
|
|
3
|
+
import { VideoManager } from '../core/VideoManager';
|
|
3
4
|
import { useVideoManager } from '../provider/VideoContext';
|
|
4
|
-
import
|
|
5
|
+
import { useVideoEvents } from '../hooks/useVideoEvents';
|
|
6
|
+
import type { VideoEventMap } from '../types/events';
|
|
7
|
+
import type {
|
|
8
|
+
OrientationLock,
|
|
9
|
+
ResizeMode,
|
|
10
|
+
VideoError,
|
|
11
|
+
VideoSource,
|
|
12
|
+
} from '../types/video';
|
|
5
13
|
import { VideoControls } from './VideoControls';
|
|
6
14
|
import { VideoSurface } from './VideoSurface';
|
|
7
15
|
|
|
@@ -18,6 +26,10 @@ export interface VideoPlayerProps extends ViewProps {
|
|
|
18
26
|
/** Show built-in controls. Default true. */
|
|
19
27
|
controls?: boolean;
|
|
20
28
|
resizeMode?: ResizeMode;
|
|
29
|
+
/** Loop playback when it ends. Default false. */
|
|
30
|
+
repeat?: boolean;
|
|
31
|
+
/** Default false. */
|
|
32
|
+
muted?: boolean;
|
|
21
33
|
/**
|
|
22
34
|
* Force the screen into this orientation while the player is mounted:
|
|
23
35
|
* `'landscape'`, `'inverted-landscape'`, `'portrait'` or
|
|
@@ -31,6 +43,32 @@ export interface VideoPlayerProps extends ViewProps {
|
|
|
31
43
|
* when it closes, so the rest of the app is unaffected.
|
|
32
44
|
*/
|
|
33
45
|
fullscreenOrientation?: OrientationLock;
|
|
46
|
+
/**
|
|
47
|
+
* Pause this player when it loses focus — i.e. the app goes to the
|
|
48
|
+
* background while this player's surface is the one currently playing.
|
|
49
|
+
* Default `true`. Set `false` to keep playing (e.g. background audio).
|
|
50
|
+
* Only the focused player acts, so a video attached elsewhere is untouched.
|
|
51
|
+
*/
|
|
52
|
+
pauseOnFocusLost?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Screen-focus flag from your navigation library — e.g. React Navigation's
|
|
55
|
+
* `useIsFocused()`. `false` pauses this player; returning to `true` reclaims
|
|
56
|
+
* the engine and resumes this video. This is the only reliable way to pause
|
|
57
|
+
* on screen navigation (React Navigation keeps screens mounted and the app
|
|
58
|
+
* stays foregrounded, so unmount/AppState never fire). Leave undefined if
|
|
59
|
+
* you don't use navigation.
|
|
60
|
+
*
|
|
61
|
+
* ```tsx
|
|
62
|
+
* const isFocused = useIsFocused(); // @react-navigation/native
|
|
63
|
+
* <VideoPlayer source={video} isFocused={isFocused} />
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
isFocused?: boolean;
|
|
67
|
+
/** Fires once metadata (duration, dimensions) is available. */
|
|
68
|
+
onLoadComplete?: (info: VideoEventMap['onLoad']) => void;
|
|
69
|
+
/** Fires whenever buffering starts or stops. */
|
|
70
|
+
onBuffering?: (buffering: boolean) => void;
|
|
71
|
+
onError?: (error: VideoError) => void;
|
|
34
72
|
}
|
|
35
73
|
|
|
36
74
|
/**
|
|
@@ -41,57 +79,152 @@ export interface VideoPlayerProps extends ViewProps {
|
|
|
41
79
|
* ```tsx
|
|
42
80
|
* <VideoPlayer source={{ id: '123', uri }} style={{ aspectRatio: 16 / 9 }} />
|
|
43
81
|
* ```
|
|
82
|
+
*
|
|
83
|
+
* `ref` exposes the underlying `VideoManager` for imperative control
|
|
84
|
+
* (`ref.current.play()`, `.seek()`, …) — the same instance `useVideo()`
|
|
85
|
+
* returns elsewhere in the app.
|
|
44
86
|
*/
|
|
45
|
-
export
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
export const VideoPlayer = forwardRef<VideoManager, VideoPlayerProps>(
|
|
88
|
+
(
|
|
89
|
+
{
|
|
90
|
+
source,
|
|
91
|
+
autoplay = true,
|
|
92
|
+
surfaceId,
|
|
93
|
+
controls = true,
|
|
94
|
+
resizeMode,
|
|
95
|
+
repeat,
|
|
96
|
+
muted,
|
|
97
|
+
orientation,
|
|
98
|
+
fullscreenOrientation,
|
|
99
|
+
pauseOnFocusLost = true,
|
|
100
|
+
isFocused,
|
|
101
|
+
onLoadComplete,
|
|
102
|
+
onBuffering,
|
|
103
|
+
onError,
|
|
104
|
+
style,
|
|
105
|
+
...rest
|
|
106
|
+
},
|
|
107
|
+
ref
|
|
108
|
+
) => {
|
|
109
|
+
const manager = useVideoManager();
|
|
110
|
+
const id = surfaceId ?? `player:${source.id}`;
|
|
111
|
+
|
|
112
|
+
// Read the latest source without retriggering effects on every render
|
|
113
|
+
// (source is usually a fresh object literal each render).
|
|
114
|
+
const sourceRef = useRef(source);
|
|
115
|
+
sourceRef.current = source;
|
|
116
|
+
|
|
117
|
+
useImperativeHandle(ref, () => manager, [manager]);
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
// Don't autoplay a player that mounts already unfocused (isFocused
|
|
121
|
+
// false), else it'd flash play → pause on the next screen.
|
|
122
|
+
manager.setSource(source, {
|
|
123
|
+
autoplay: autoplay && isFocused !== false,
|
|
124
|
+
surfaceId: id,
|
|
125
|
+
});
|
|
126
|
+
// Attach on mount / when the video identity changes. Other source
|
|
127
|
+
// fields (title, headers) don't retrigger: identity is source.id.
|
|
128
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
129
|
+
}, [manager, source.id, id]);
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (resizeMode) {
|
|
133
|
+
manager.setResizeMode(resizeMode);
|
|
134
|
+
}
|
|
135
|
+
}, [manager, resizeMode]);
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (repeat === undefined) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
manager.setRepeat(repeat);
|
|
142
|
+
}, [manager, repeat]);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (muted === undefined) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (muted) {
|
|
149
|
+
manager.mute();
|
|
150
|
+
} else {
|
|
151
|
+
manager.unmute();
|
|
152
|
+
}
|
|
153
|
+
}, [manager, muted]);
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (!orientation || orientation === 'auto') {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
manager.setOrientation(orientation);
|
|
160
|
+
return () => manager.setOrientation('auto');
|
|
161
|
+
}, [manager, orientation]);
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!fullscreenOrientation) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
manager.setFullscreenOrientation(fullscreenOrientation);
|
|
168
|
+
return () => manager.setFullscreenOrientation(null);
|
|
169
|
+
}, [manager, fullscreenOrientation]);
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!pauseOnFocusLost) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const sub = AppState.addEventListener('change', (next) => {
|
|
176
|
+
// Only the player currently owning the engine reacts, so a video
|
|
177
|
+
// attached to another surface keeps playing untouched.
|
|
178
|
+
if (next !== 'active' && manager.store.getState().surfaceId === id) {
|
|
179
|
+
manager.pause();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
return () => sub.remove();
|
|
183
|
+
}, [manager, id, pauseOnFocusLost]);
|
|
184
|
+
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (isFocused === undefined) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const state = manager.store.getState();
|
|
190
|
+
if (isFocused) {
|
|
191
|
+
// Regained screen focus. If the engine is still ours, just resume;
|
|
192
|
+
// otherwise it moved to another video while we were blurred — reclaim
|
|
193
|
+
// it (same-id handoff means no reload if it never actually left).
|
|
194
|
+
if (
|
|
195
|
+
state.surfaceId === id &&
|
|
196
|
+
state.currentVideo?.id === sourceRef.current.id
|
|
197
|
+
) {
|
|
198
|
+
manager.play();
|
|
199
|
+
} else {
|
|
200
|
+
manager.setSource(sourceRef.current, {
|
|
201
|
+
autoplay: true,
|
|
202
|
+
surfaceId: id,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
} else if (state.surfaceId === id) {
|
|
206
|
+
// Lost screen focus while playing our video — pause. Guarded so we
|
|
207
|
+
// never pause a video that has already handed off elsewhere.
|
|
208
|
+
manager.pause();
|
|
209
|
+
}
|
|
210
|
+
}, [manager, id, isFocused]);
|
|
211
|
+
|
|
212
|
+
useVideoEvents({
|
|
213
|
+
onLoad: onLoadComplete,
|
|
214
|
+
onBuffer: (e) => onBuffering?.(e.buffering),
|
|
215
|
+
onError,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<View style={[styles.container, style]} {...rest}>
|
|
220
|
+
<VideoSurface surfaceId={id} style={styles.surface} />
|
|
221
|
+
{controls ? <VideoControls /> : null}
|
|
222
|
+
</View>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
VideoPlayer.displayName = 'VideoPlayer';
|
|
95
228
|
|
|
96
229
|
const styles = StyleSheet.create({
|
|
97
230
|
container: {
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,12 @@ export {
|
|
|
14
14
|
type VideoSurfaceProps,
|
|
15
15
|
} from './components/VideoSurface';
|
|
16
16
|
export { VideoPlayer, type VideoPlayerProps } from './components/VideoPlayer';
|
|
17
|
+
export {
|
|
18
|
+
VideoFeed,
|
|
19
|
+
feedSurfaceId,
|
|
20
|
+
type VideoFeedProps,
|
|
21
|
+
type VideoFeedRenderInfo,
|
|
22
|
+
} from './components/VideoFeed';
|
|
17
23
|
export { FullscreenPlayer } from './components/FullscreenPlayer';
|
|
18
24
|
export {
|
|
19
25
|
FloatingPlayer,
|