react-native-video-provider 0.2.3 → 0.2.4
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/lib/module/components/VideoFeed.js +150 -0
- package/lib/module/components/VideoFeed.js.map +1 -0
- package/lib/module/components/VideoPlayer.js +52 -5
- 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 +49 -0
- package/lib/typescript/src/components/VideoFeed.d.ts.map +1 -0
- package/lib/typescript/src/components/VideoPlayer.d.ts +24 -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 +44 -0
- package/src/components/VideoFeed.tsx +201 -0
- package/src/components/VideoPlayer.tsx +129 -48
- 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"
|
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
...rest
|
|
42
|
+
}) {
|
|
43
|
+
const manager = useVideoManager();
|
|
44
|
+
const {
|
|
45
|
+
height: windowHeight
|
|
46
|
+
} = useWindowDimensions();
|
|
47
|
+
const height = itemHeight ?? windowHeight;
|
|
48
|
+
const [focusedId, setFocusedId] = useState(null);
|
|
49
|
+
|
|
50
|
+
// Global engine settings — apply to whatever is currently playing.
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (muted) {
|
|
53
|
+
manager.mute();
|
|
54
|
+
} else {
|
|
55
|
+
manager.unmute();
|
|
56
|
+
}
|
|
57
|
+
}, [manager, muted]);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
manager.setRepeat(repeat);
|
|
60
|
+
}, [manager, repeat]);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (resizeMode) {
|
|
63
|
+
manager.setResizeMode(resizeMode);
|
|
64
|
+
}
|
|
65
|
+
}, [manager, resizeMode]);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
return () => {
|
|
68
|
+
if (pauseOnUnmount) {
|
|
69
|
+
manager.pause();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}, [manager, pauseOnUnmount]);
|
|
73
|
+
const focus = useCallback((item, index) => {
|
|
74
|
+
setFocusedId(item?.id ?? null);
|
|
75
|
+
if (item) {
|
|
76
|
+
// Same-id handoff aware: re-focusing the same video never reloads.
|
|
77
|
+
// A different id replaces the source, which stops the previous one —
|
|
78
|
+
// so only the focused video ever plays.
|
|
79
|
+
manager.setSource(item, {
|
|
80
|
+
autoplay: true,
|
|
81
|
+
surfaceId: feedSurfaceId(item.id)
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
onFocusChange?.(item, index);
|
|
85
|
+
}, [manager, onFocusChange]);
|
|
86
|
+
|
|
87
|
+
// FlatList forbids changing these on the fly, so keep them stable and read
|
|
88
|
+
// the latest `focus`/`data` through a ref.
|
|
89
|
+
const latest = useRef({
|
|
90
|
+
focus,
|
|
91
|
+
data
|
|
92
|
+
});
|
|
93
|
+
latest.current = {
|
|
94
|
+
focus,
|
|
95
|
+
data
|
|
96
|
+
};
|
|
97
|
+
const viewabilityConfig = useRef({
|
|
98
|
+
itemVisiblePercentThreshold: visibilityThreshold,
|
|
99
|
+
minimumViewTime: 100
|
|
100
|
+
}).current;
|
|
101
|
+
const onViewableItemsChanged = useRef(({
|
|
102
|
+
viewableItems
|
|
103
|
+
}) => {
|
|
104
|
+
const focusedToken = viewableItems.find(v => v.isViewable);
|
|
105
|
+
if (focusedToken) {
|
|
106
|
+
latest.current.focus(focusedToken.item, focusedToken.index ?? 0);
|
|
107
|
+
}
|
|
108
|
+
}).current;
|
|
109
|
+
const renderItem = useCallback(({
|
|
110
|
+
item,
|
|
111
|
+
index
|
|
112
|
+
}) => /*#__PURE__*/_jsxs(View, {
|
|
113
|
+
style: [styles.item, {
|
|
114
|
+
height
|
|
115
|
+
}],
|
|
116
|
+
children: [/*#__PURE__*/_jsx(VideoSurface, {
|
|
117
|
+
surfaceId: feedSurfaceId(item.id),
|
|
118
|
+
style: StyleSheet.absoluteFill
|
|
119
|
+
}), renderOverlay?.({
|
|
120
|
+
item,
|
|
121
|
+
index,
|
|
122
|
+
focused: item.id === focusedId
|
|
123
|
+
})]
|
|
124
|
+
}), [height, renderOverlay, focusedId]);
|
|
125
|
+
return /*#__PURE__*/_jsx(FlatList, {
|
|
126
|
+
data: data,
|
|
127
|
+
keyExtractor: item => item.id,
|
|
128
|
+
renderItem: renderItem,
|
|
129
|
+
onViewableItemsChanged: onViewableItemsChanged,
|
|
130
|
+
viewabilityConfig: viewabilityConfig,
|
|
131
|
+
getItemLayout: (_, index) => ({
|
|
132
|
+
length: height,
|
|
133
|
+
offset: height * index,
|
|
134
|
+
index
|
|
135
|
+
}),
|
|
136
|
+
showsVerticalScrollIndicator: false,
|
|
137
|
+
snapToInterval: height,
|
|
138
|
+
snapToAlignment: "start",
|
|
139
|
+
decelerationRate: "fast",
|
|
140
|
+
...rest
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const styles = StyleSheet.create({
|
|
144
|
+
item: {
|
|
145
|
+
width: '100%',
|
|
146
|
+
backgroundColor: '#000',
|
|
147
|
+
overflow: 'hidden'
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
//# 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","rest","manager","height","windowHeight","focusedId","setFocusedId","mute","unmute","setRepeat","setResizeMode","pause","focus","item","index","setSource","autoplay","surfaceId","latest","current","viewabilityConfig","itemVisiblePercentThreshold","minimumViewTime","onViewableItemsChanged","viewableItems","focusedToken","find","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 } 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,20 +15,31 @@ 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
|
+
onLoadComplete,
|
|
35
|
+
onBuffering,
|
|
36
|
+
onError,
|
|
26
37
|
style,
|
|
27
38
|
...rest
|
|
28
|
-
}) {
|
|
39
|
+
}, ref) => {
|
|
29
40
|
const manager = useVideoManager();
|
|
30
41
|
const id = surfaceId ?? `player:${source.id}`;
|
|
42
|
+
useImperativeHandle(ref, () => manager, [manager]);
|
|
31
43
|
useEffect(() => {
|
|
32
44
|
manager.setSource(source, {
|
|
33
45
|
autoplay,
|
|
@@ -42,6 +54,22 @@ export function VideoPlayer({
|
|
|
42
54
|
manager.setResizeMode(resizeMode);
|
|
43
55
|
}
|
|
44
56
|
}, [manager, resizeMode]);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (repeat === undefined) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
manager.setRepeat(repeat);
|
|
62
|
+
}, [manager, repeat]);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (muted === undefined) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (muted) {
|
|
68
|
+
manager.mute();
|
|
69
|
+
} else {
|
|
70
|
+
manager.unmute();
|
|
71
|
+
}
|
|
72
|
+
}, [manager, muted]);
|
|
45
73
|
useEffect(() => {
|
|
46
74
|
if (!orientation || orientation === 'auto') {
|
|
47
75
|
return;
|
|
@@ -56,6 +84,24 @@ export function VideoPlayer({
|
|
|
56
84
|
manager.setFullscreenOrientation(fullscreenOrientation);
|
|
57
85
|
return () => manager.setFullscreenOrientation(null);
|
|
58
86
|
}, [manager, fullscreenOrientation]);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!pauseOnFocusLost) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const sub = AppState.addEventListener('change', next => {
|
|
92
|
+
// Only the player currently owning the engine reacts, so a video
|
|
93
|
+
// attached to another surface keeps playing untouched.
|
|
94
|
+
if (next !== 'active' && manager.store.getState().surfaceId === id) {
|
|
95
|
+
manager.pause();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return () => sub.remove();
|
|
99
|
+
}, [manager, id, pauseOnFocusLost]);
|
|
100
|
+
useVideoEvents({
|
|
101
|
+
onLoad: onLoadComplete,
|
|
102
|
+
onBuffer: e => onBuffering?.(e.buffering),
|
|
103
|
+
onError
|
|
104
|
+
});
|
|
59
105
|
return /*#__PURE__*/_jsxs(View, {
|
|
60
106
|
style: [styles.container, style],
|
|
61
107
|
...rest,
|
|
@@ -64,7 +110,8 @@ export function VideoPlayer({
|
|
|
64
110
|
style: styles.surface
|
|
65
111
|
}), controls ? /*#__PURE__*/_jsx(VideoControls, {}) : null]
|
|
66
112
|
});
|
|
67
|
-
}
|
|
113
|
+
});
|
|
114
|
+
VideoPlayer.displayName = 'VideoPlayer';
|
|
68
115
|
const styles = StyleSheet.create({
|
|
69
116
|
container: {
|
|
70
117
|
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","AppState","StyleSheet","View","useVideoManager","useVideoEvents","VideoControls","VideoSurface","jsx","_jsx","jsxs","_jsxs","VideoPlayer","source","autoplay","surfaceId","controls","resizeMode","repeat","muted","orientation","fullscreenOrientation","pauseOnFocusLost","onLoadComplete","onBuffering","onError","style","rest","ref","manager","id","setSource","setResizeMode","undefined","setRepeat","mute","unmute","setOrientation","setFullscreenOrientation","sub","addEventListener","next","store","getState","pause","remove","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,49 @@
|
|
|
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
|
+
/**
|
|
32
|
+
* A single-engine video feed: render many videos in a scrollable list, but
|
|
33
|
+
* only the one scrolled into focus plays — the rest are stopped. There is
|
|
34
|
+
* still exactly ONE native player; scrolling hands it off to the focused
|
|
35
|
+
* item's surface, so memory and CPU stay flat no matter how long the feed.
|
|
36
|
+
*
|
|
37
|
+
* Each item is a plain `VideoSurface` (never a `VideoPlayer`, which would
|
|
38
|
+
* fight for the engine on mount). Focus is driven centrally from FlatList
|
|
39
|
+
* viewability.
|
|
40
|
+
*
|
|
41
|
+
* ```tsx
|
|
42
|
+
* <VideoFeed
|
|
43
|
+
* data={videos} // [{ id, uri, title? }, …]
|
|
44
|
+
* renderOverlay={({ item, focused }) => <Caption title={item.title} />}
|
|
45
|
+
* />
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function VideoFeed<T extends VideoSource>({ data, itemHeight, resizeMode, muted, repeat, visibilityThreshold, renderOverlay, onFocusChange, pauseOnUnmount, ...rest }: VideoFeedProps<T>): import("react").JSX.Element;
|
|
49
|
+
//# 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;CAC1B;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,GAAG,IAAI,EACR,EAAE,cAAc,CAAC,CAAC,CAAC,+BAqGnB"}
|
|
@@ -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,18 @@ 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
|
+
/** Fires once metadata (duration, dimensions) is available. */
|
|
43
|
+
onLoadComplete?: (info: VideoEventMap['onLoad']) => void;
|
|
44
|
+
/** Fires whenever buffering starts or stops. */
|
|
45
|
+
onBuffering?: (buffering: boolean) => void;
|
|
46
|
+
onError?: (error: VideoError) => void;
|
|
29
47
|
}
|
|
30
48
|
/**
|
|
31
49
|
* Convenience all-in-one player: `setSource` (same-video handoff aware) +
|
|
@@ -35,6 +53,10 @@ export interface VideoPlayerProps extends ViewProps {
|
|
|
35
53
|
* ```tsx
|
|
36
54
|
* <VideoPlayer source={{ id: '123', uri }} style={{ aspectRatio: 16 / 9 }} />
|
|
37
55
|
* ```
|
|
56
|
+
*
|
|
57
|
+
* `ref` exposes the underlying `VideoManager` for imperative control
|
|
58
|
+
* (`ref.current.play()`, `.seek()`, …) — the same instance `useVideo()`
|
|
59
|
+
* returns elsewhere in the app.
|
|
38
60
|
*/
|
|
39
|
-
export declare
|
|
61
|
+
export declare const VideoPlayer: import("react").ForwardRefExoticComponent<VideoPlayerProps & import("react").RefAttributes<VideoManager>>;
|
|
40
62
|
//# 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,+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,2GAoGvB,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.4",
|
|
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,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prints a banner after install. Wrapped so it can NEVER fail a consumer's
|
|
5
|
+
* `npm install` — any error is swallowed and the process always exits 0.
|
|
6
|
+
* Skipped in CI and non-interactive shells to avoid polluting build logs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const BANNER = `
|
|
10
|
+
++
|
|
11
|
+
+++ +++ +++++++++++ ++
|
|
12
|
+
++++++++++++++ ++++++++++ ++++++++++++++ +++ +++++++ ++ +++
|
|
13
|
+
++ +++ +++ ++++++++++++ +++ +++++ ++++ +++++++ ++++ +++++++ ++ +++
|
|
14
|
+
++ +++ +++ ++++ +++ +++++++++++++ ++++ +++++ ++++ +++++ +++++++ ++ +++++++++++
|
|
15
|
+
+++ +++ +++ ++++ ++++ +++ +++++++ +++++++ +++++++++ +++++++++ ++++ ++++
|
|
16
|
+
++++ +++ +++ ++++++++++ ++++++ +++ ++++++ +++++ +++ ++ +++ +++
|
|
17
|
+
+++ ++ +++ ++++ +++ +++ ++++++++ ++++ ++++ +++ ++++ +++ +++ +++
|
|
18
|
+
++++ ++++ ++++ +++ ++ +++++ ++ +++ ++++
|
|
19
|
+
+++++++ ++++ ++ ++++++ ++++++++++
|
|
20
|
+
++++ ++++ + +++ ++++ + +++++++++++
|
|
21
|
+
+++ +++ ++++ +++++ ++++ +++ +++ ++++ ++++ +++
|
|
22
|
+
++++++++ ++ ++ ++ +++ + ++
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const quiet = process.env.CI || process.env.npm_config_loglevel === 'silent';
|
|
27
|
+
if (!quiet && process.stdout && process.stdout.isTTY) {
|
|
28
|
+
const cyan = '\x1b[36m';
|
|
29
|
+
const dim = '\x1b[2m';
|
|
30
|
+
const reset = '\x1b[0m';
|
|
31
|
+
process.stdout.write(cyan + BANNER + reset + '\n');
|
|
32
|
+
process.stdout.write(
|
|
33
|
+
dim +
|
|
34
|
+
' react-native-video-provider — one native player, many surfaces.\n' +
|
|
35
|
+
' Docs: https://github.com/gurdeep99/react-native-video-provider#readme\n' +
|
|
36
|
+
reset +
|
|
37
|
+
'\n'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// Never break an install over a banner.
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
process.exit(0);
|
|
@@ -0,0 +1,201 @@
|
|
|
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
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A single-engine video feed: render many videos in a scrollable list, but
|
|
66
|
+
* only the one scrolled into focus plays — the rest are stopped. There is
|
|
67
|
+
* still exactly ONE native player; scrolling hands it off to the focused
|
|
68
|
+
* item's surface, so memory and CPU stay flat no matter how long the feed.
|
|
69
|
+
*
|
|
70
|
+
* Each item is a plain `VideoSurface` (never a `VideoPlayer`, which would
|
|
71
|
+
* fight for the engine on mount). Focus is driven centrally from FlatList
|
|
72
|
+
* viewability.
|
|
73
|
+
*
|
|
74
|
+
* ```tsx
|
|
75
|
+
* <VideoFeed
|
|
76
|
+
* data={videos} // [{ id, uri, title? }, …]
|
|
77
|
+
* renderOverlay={({ item, focused }) => <Caption title={item.title} />}
|
|
78
|
+
* />
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function VideoFeed<T extends VideoSource>({
|
|
82
|
+
data,
|
|
83
|
+
itemHeight,
|
|
84
|
+
resizeMode,
|
|
85
|
+
muted = true,
|
|
86
|
+
repeat = true,
|
|
87
|
+
visibilityThreshold = 80,
|
|
88
|
+
renderOverlay,
|
|
89
|
+
onFocusChange,
|
|
90
|
+
pauseOnUnmount = true,
|
|
91
|
+
...rest
|
|
92
|
+
}: VideoFeedProps<T>) {
|
|
93
|
+
const manager = useVideoManager();
|
|
94
|
+
const { height: windowHeight } = useWindowDimensions();
|
|
95
|
+
const height = itemHeight ?? windowHeight;
|
|
96
|
+
const [focusedId, setFocusedId] = useState<string | null>(null);
|
|
97
|
+
|
|
98
|
+
// Global engine settings — apply to whatever is currently playing.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (muted) {
|
|
101
|
+
manager.mute();
|
|
102
|
+
} else {
|
|
103
|
+
manager.unmute();
|
|
104
|
+
}
|
|
105
|
+
}, [manager, muted]);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
manager.setRepeat(repeat);
|
|
109
|
+
}, [manager, repeat]);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (resizeMode) {
|
|
113
|
+
manager.setResizeMode(resizeMode);
|
|
114
|
+
}
|
|
115
|
+
}, [manager, resizeMode]);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
return () => {
|
|
119
|
+
if (pauseOnUnmount) {
|
|
120
|
+
manager.pause();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}, [manager, pauseOnUnmount]);
|
|
124
|
+
|
|
125
|
+
const focus = useCallback(
|
|
126
|
+
(item: T | null, index: number) => {
|
|
127
|
+
setFocusedId(item?.id ?? null);
|
|
128
|
+
if (item) {
|
|
129
|
+
// Same-id handoff aware: re-focusing the same video never reloads.
|
|
130
|
+
// A different id replaces the source, which stops the previous one —
|
|
131
|
+
// so only the focused video ever plays.
|
|
132
|
+
manager.setSource(item, {
|
|
133
|
+
autoplay: true,
|
|
134
|
+
surfaceId: feedSurfaceId(item.id),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
onFocusChange?.(item, index);
|
|
138
|
+
},
|
|
139
|
+
[manager, onFocusChange]
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// FlatList forbids changing these on the fly, so keep them stable and read
|
|
143
|
+
// the latest `focus`/`data` through a ref.
|
|
144
|
+
const latest = useRef({ focus, data });
|
|
145
|
+
latest.current = { focus, data };
|
|
146
|
+
|
|
147
|
+
const viewabilityConfig = useRef({
|
|
148
|
+
itemVisiblePercentThreshold: visibilityThreshold,
|
|
149
|
+
minimumViewTime: 100,
|
|
150
|
+
}).current;
|
|
151
|
+
|
|
152
|
+
const onViewableItemsChanged = useRef(
|
|
153
|
+
({ viewableItems }: { viewableItems: FeedViewToken[] }) => {
|
|
154
|
+
const focusedToken = viewableItems.find((v) => v.isViewable);
|
|
155
|
+
if (focusedToken) {
|
|
156
|
+
latest.current.focus(focusedToken.item as T, focusedToken.index ?? 0);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
).current;
|
|
160
|
+
|
|
161
|
+
const renderItem = useCallback(
|
|
162
|
+
({ item, index }: { item: T; index: number }) => (
|
|
163
|
+
<View style={[styles.item, { height }]}>
|
|
164
|
+
<VideoSurface
|
|
165
|
+
surfaceId={feedSurfaceId(item.id)}
|
|
166
|
+
style={StyleSheet.absoluteFill}
|
|
167
|
+
/>
|
|
168
|
+
{renderOverlay?.({ item, index, focused: item.id === focusedId })}
|
|
169
|
+
</View>
|
|
170
|
+
),
|
|
171
|
+
[height, renderOverlay, focusedId]
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<FlatList
|
|
176
|
+
data={data}
|
|
177
|
+
keyExtractor={(item) => item.id}
|
|
178
|
+
renderItem={renderItem}
|
|
179
|
+
onViewableItemsChanged={onViewableItemsChanged}
|
|
180
|
+
viewabilityConfig={viewabilityConfig}
|
|
181
|
+
getItemLayout={(_, index) => ({
|
|
182
|
+
length: height,
|
|
183
|
+
offset: height * index,
|
|
184
|
+
index,
|
|
185
|
+
})}
|
|
186
|
+
showsVerticalScrollIndicator={false}
|
|
187
|
+
snapToInterval={height}
|
|
188
|
+
snapToAlignment="start"
|
|
189
|
+
decelerationRate="fast"
|
|
190
|
+
{...rest}
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const styles = StyleSheet.create({
|
|
196
|
+
item: {
|
|
197
|
+
width: '100%',
|
|
198
|
+
backgroundColor: '#000',
|
|
199
|
+
overflow: 'hidden',
|
|
200
|
+
},
|
|
201
|
+
});
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
2
|
-
import { StyleSheet, View, type ViewProps } from 'react-native';
|
|
1
|
+
import { forwardRef, useEffect, useImperativeHandle } 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,18 @@ 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
|
+
/** Fires once metadata (duration, dimensions) is available. */
|
|
54
|
+
onLoadComplete?: (info: VideoEventMap['onLoad']) => void;
|
|
55
|
+
/** Fires whenever buffering starts or stops. */
|
|
56
|
+
onBuffering?: (buffering: boolean) => void;
|
|
57
|
+
onError?: (error: VideoError) => void;
|
|
34
58
|
}
|
|
35
59
|
|
|
36
60
|
/**
|
|
@@ -41,57 +65,114 @@ export interface VideoPlayerProps extends ViewProps {
|
|
|
41
65
|
* ```tsx
|
|
42
66
|
* <VideoPlayer source={{ id: '123', uri }} style={{ aspectRatio: 16 / 9 }} />
|
|
43
67
|
* ```
|
|
68
|
+
*
|
|
69
|
+
* `ref` exposes the underlying `VideoManager` for imperative control
|
|
70
|
+
* (`ref.current.play()`, `.seek()`, …) — the same instance `useVideo()`
|
|
71
|
+
* returns elsewhere in the app.
|
|
44
72
|
*/
|
|
45
|
-
export
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
73
|
+
export const VideoPlayer = forwardRef<VideoManager, VideoPlayerProps>(
|
|
74
|
+
(
|
|
75
|
+
{
|
|
76
|
+
source,
|
|
77
|
+
autoplay = true,
|
|
78
|
+
surfaceId,
|
|
79
|
+
controls = true,
|
|
80
|
+
resizeMode,
|
|
81
|
+
repeat,
|
|
82
|
+
muted,
|
|
83
|
+
orientation,
|
|
84
|
+
fullscreenOrientation,
|
|
85
|
+
pauseOnFocusLost = true,
|
|
86
|
+
onLoadComplete,
|
|
87
|
+
onBuffering,
|
|
88
|
+
onError,
|
|
89
|
+
style,
|
|
90
|
+
...rest
|
|
91
|
+
},
|
|
92
|
+
ref
|
|
93
|
+
) => {
|
|
94
|
+
const manager = useVideoManager();
|
|
95
|
+
const id = surfaceId ?? `player:${source.id}`;
|
|
58
96
|
|
|
59
|
-
|
|
60
|
-
manager.setSource(source, { autoplay, surfaceId: id });
|
|
61
|
-
// Attach on mount / when the video identity changes. Other source
|
|
62
|
-
// fields (title, headers) don't retrigger: identity is source.id.
|
|
63
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
64
|
-
}, [manager, source.id, id]);
|
|
97
|
+
useImperativeHandle(ref, () => manager, [manager]);
|
|
65
98
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
manager.setSource(source, { autoplay, surfaceId: id });
|
|
101
|
+
// Attach on mount / when the video identity changes. Other source
|
|
102
|
+
// fields (title, headers) don't retrigger: identity is source.id.
|
|
103
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
104
|
+
}, [manager, source.id, id]);
|
|
71
105
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
manager
|
|
77
|
-
return () => manager.setOrientation('auto');
|
|
78
|
-
}, [manager, orientation]);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (resizeMode) {
|
|
108
|
+
manager.setResizeMode(resizeMode);
|
|
109
|
+
}
|
|
110
|
+
}, [manager, resizeMode]);
|
|
79
111
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}, [manager, fullscreenOrientation]);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (repeat === undefined) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
manager.setRepeat(repeat);
|
|
117
|
+
}, [manager, repeat]);
|
|
87
118
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (muted === undefined) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (muted) {
|
|
124
|
+
manager.mute();
|
|
125
|
+
} else {
|
|
126
|
+
manager.unmute();
|
|
127
|
+
}
|
|
128
|
+
}, [manager, muted]);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!orientation || orientation === 'auto') {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
manager.setOrientation(orientation);
|
|
135
|
+
return () => manager.setOrientation('auto');
|
|
136
|
+
}, [manager, orientation]);
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!fullscreenOrientation) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
manager.setFullscreenOrientation(fullscreenOrientation);
|
|
143
|
+
return () => manager.setFullscreenOrientation(null);
|
|
144
|
+
}, [manager, fullscreenOrientation]);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (!pauseOnFocusLost) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const sub = AppState.addEventListener('change', (next) => {
|
|
151
|
+
// Only the player currently owning the engine reacts, so a video
|
|
152
|
+
// attached to another surface keeps playing untouched.
|
|
153
|
+
if (next !== 'active' && manager.store.getState().surfaceId === id) {
|
|
154
|
+
manager.pause();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return () => sub.remove();
|
|
158
|
+
}, [manager, id, pauseOnFocusLost]);
|
|
159
|
+
|
|
160
|
+
useVideoEvents({
|
|
161
|
+
onLoad: onLoadComplete,
|
|
162
|
+
onBuffer: (e) => onBuffering?.(e.buffering),
|
|
163
|
+
onError,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<View style={[styles.container, style]} {...rest}>
|
|
168
|
+
<VideoSurface surfaceId={id} style={styles.surface} />
|
|
169
|
+
{controls ? <VideoControls /> : null}
|
|
170
|
+
</View>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
VideoPlayer.displayName = 'VideoPlayer';
|
|
95
176
|
|
|
96
177
|
const styles = StyleSheet.create({
|
|
97
178
|
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,
|