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 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/grdp99/react-native-au-video.git", :tag => "#{s.version}" }
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
- Requires React Native 0.80+ with the New Architecture enabled.
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
 
@@ -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 function VideoPlayer({
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;AACjC,SAASC,UAAU,EAAEC,IAAI,QAAwB,cAAc;AAC/D,SAASC,eAAe,QAAQ,6BAA0B;AAE1D,SAASC,aAAa,QAAQ,oBAAiB;AAC/C,SAASC,YAAY,QAAQ,mBAAgB;AAAC,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AA8B9C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,WAAWA,CAAC;EAC1BC,MAAM;EACNC,QAAQ,GAAG,IAAI;EACfC,SAAS;EACTC,QAAQ,GAAG,IAAI;EACfC,UAAU;EACVC,WAAW;EACXC,qBAAqB;EACrBC,KAAK;EACL,GAAGC;AACa,CAAC,EAAE;EACnB,MAAMC,OAAO,GAAGjB,eAAe,CAAC,CAAC;EACjC,MAAMkB,EAAE,GAAGR,SAAS,IAAI,UAAUF,MAAM,CAACU,EAAE,EAAE;EAE7CrB,SAAS,CAAC,MAAM;IACdoB,OAAO,CAACE,SAAS,CAACX,MAAM,EAAE;MAAEC,QAAQ;MAAEC,SAAS,EAAEQ;IAAG,CAAC,CAAC;IACtD;IACA;IACA;EACF,CAAC,EAAE,CAACD,OAAO,EAAET,MAAM,CAACU,EAAE,EAAEA,EAAE,CAAC,CAAC;EAE5BrB,SAAS,CAAC,MAAM;IACd,IAAIe,UAAU,EAAE;MACdK,OAAO,CAACG,aAAa,CAACR,UAAU,CAAC;IACnC;EACF,CAAC,EAAE,CAACK,OAAO,EAAEL,UAAU,CAAC,CAAC;EAEzBf,SAAS,CAAC,MAAM;IACd,IAAI,CAACgB,WAAW,IAAIA,WAAW,KAAK,MAAM,EAAE;MAC1C;IACF;IACAI,OAAO,CAACI,cAAc,CAACR,WAAW,CAAC;IACnC,OAAO,MAAMI,OAAO,CAACI,cAAc,CAAC,MAAM,CAAC;EAC7C,CAAC,EAAE,CAACJ,OAAO,EAAEJ,WAAW,CAAC,CAAC;EAE1BhB,SAAS,CAAC,MAAM;IACd,IAAI,CAACiB,qBAAqB,EAAE;MAC1B;IACF;IACAG,OAAO,CAACK,wBAAwB,CAACR,qBAAqB,CAAC;IACvD,OAAO,MAAMG,OAAO,CAACK,wBAAwB,CAAC,IAAI,CAAC;EACrD,CAAC,EAAE,CAACL,OAAO,EAAEH,qBAAqB,CAAC,CAAC;EAEpC,oBACER,KAAA,CAACP,IAAI;IAACgB,KAAK,EAAE,CAACQ,MAAM,CAACC,SAAS,EAAET,KAAK,CAAE;IAAA,GAAKC,IAAI;IAAAS,QAAA,gBAC9CrB,IAAA,CAACF,YAAY;MAACQ,SAAS,EAAEQ,EAAG;MAACH,KAAK,EAAEQ,MAAM,CAACG;IAAQ,CAAE,CAAC,EACrDf,QAAQ,gBAAGP,IAAA,CAACH,aAAa,IAAE,CAAC,GAAG,IAAI;EAAA,CAChC,CAAC;AAEX;AAEA,MAAMsB,MAAM,GAAGzB,UAAU,CAAC6B,MAAM,CAAC;EAC/BH,SAAS,EAAE;IACTI,eAAe,EAAE,MAAM;IACvBC,QAAQ,EAAE;EACZ,CAAC;EACDH,OAAO,EAAE;IACPI,QAAQ,EAAE,UAAU;IACpBC,GAAG,EAAE,CAAC;IACNC,IAAI,EAAE,CAAC;IACPC,KAAK,EAAE,CAAC;IACRC,MAAM,EAAE;EACV;AACF,CAAC,CAAC","ignoreList":[]}
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":";;AAAA,SAASA,UAAU,EAAEC,SAAS,EAAEC,mBAAmB,EAAEC,MAAM,QAAQ,OAAO;AAC1E,SAASC,QAAQ,EAAEC,UAAU,EAAEC,IAAI,QAAwB,cAAc;AAEzE,SAASC,eAAe,QAAQ,6BAA0B;AAC1D,SAASC,cAAc,QAAQ,4BAAyB;AAQxD,SAASC,aAAa,QAAQ,oBAAiB;AAC/C,SAASC,YAAY,QAAQ,mBAAgB;AAAC,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AA4D9C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,WAAW,gBAAGf,UAAU,CACnC,CACE;EACEgB,MAAM;EACNC,QAAQ,GAAG,IAAI;EACfC,SAAS;EACTC,QAAQ,GAAG,IAAI;EACfC,UAAU;EACVC,MAAM;EACNC,KAAK;EACLC,WAAW;EACXC,qBAAqB;EACrBC,gBAAgB,GAAG,IAAI;EACvBC,SAAS;EACTC,cAAc;EACdC,WAAW;EACXC,OAAO;EACPC,KAAK;EACL,GAAGC;AACL,CAAC,EACDC,GAAG,KACA;EACH,MAAMC,OAAO,GAAG1B,eAAe,CAAC,CAAC;EACjC,MAAM2B,EAAE,GAAGhB,SAAS,IAAI,UAAUF,MAAM,CAACkB,EAAE,EAAE;;EAE7C;EACA;EACA,MAAMC,SAAS,GAAGhC,MAAM,CAACa,MAAM,CAAC;EAChCmB,SAAS,CAACC,OAAO,GAAGpB,MAAM;EAE1Bd,mBAAmB,CAAC8B,GAAG,EAAE,MAAMC,OAAO,EAAE,CAACA,OAAO,CAAC,CAAC;EAElDhC,SAAS,CAAC,MAAM;IACd;IACA;IACAgC,OAAO,CAACI,SAAS,CAACrB,MAAM,EAAE;MACxBC,QAAQ,EAAEA,QAAQ,IAAIS,SAAS,KAAK,KAAK;MACzCR,SAAS,EAAEgB;IACb,CAAC,CAAC;IACF;IACA;IACA;EACF,CAAC,EAAE,CAACD,OAAO,EAAEjB,MAAM,CAACkB,EAAE,EAAEA,EAAE,CAAC,CAAC;EAE5BjC,SAAS,CAAC,MAAM;IACd,IAAImB,UAAU,EAAE;MACda,OAAO,CAACK,aAAa,CAAClB,UAAU,CAAC;IACnC;EACF,CAAC,EAAE,CAACa,OAAO,EAAEb,UAAU,CAAC,CAAC;EAEzBnB,SAAS,CAAC,MAAM;IACd,IAAIoB,MAAM,KAAKkB,SAAS,EAAE;MACxB;IACF;IACAN,OAAO,CAACO,SAAS,CAACnB,MAAM,CAAC;EAC3B,CAAC,EAAE,CAACY,OAAO,EAAEZ,MAAM,CAAC,CAAC;EAErBpB,SAAS,CAAC,MAAM;IACd,IAAIqB,KAAK,KAAKiB,SAAS,EAAE;MACvB;IACF;IACA,IAAIjB,KAAK,EAAE;MACTW,OAAO,CAACQ,IAAI,CAAC,CAAC;IAChB,CAAC,MAAM;MACLR,OAAO,CAACS,MAAM,CAAC,CAAC;IAClB;EACF,CAAC,EAAE,CAACT,OAAO,EAAEX,KAAK,CAAC,CAAC;EAEpBrB,SAAS,CAAC,MAAM;IACd,IAAI,CAACsB,WAAW,IAAIA,WAAW,KAAK,MAAM,EAAE;MAC1C;IACF;IACAU,OAAO,CAACU,cAAc,CAACpB,WAAW,CAAC;IACnC,OAAO,MAAMU,OAAO,CAACU,cAAc,CAAC,MAAM,CAAC;EAC7C,CAAC,EAAE,CAACV,OAAO,EAAEV,WAAW,CAAC,CAAC;EAE1BtB,SAAS,CAAC,MAAM;IACd,IAAI,CAACuB,qBAAqB,EAAE;MAC1B;IACF;IACAS,OAAO,CAACW,wBAAwB,CAACpB,qBAAqB,CAAC;IACvD,OAAO,MAAMS,OAAO,CAACW,wBAAwB,CAAC,IAAI,CAAC;EACrD,CAAC,EAAE,CAACX,OAAO,EAAET,qBAAqB,CAAC,CAAC;EAEpCvB,SAAS,CAAC,MAAM;IACd,IAAI,CAACwB,gBAAgB,EAAE;MACrB;IACF;IACA,MAAMoB,GAAG,GAAGzC,QAAQ,CAAC0C,gBAAgB,CAAC,QAAQ,EAAGC,IAAI,IAAK;MACxD;MACA;MACA,IAAIA,IAAI,KAAK,QAAQ,IAAId,OAAO,CAACe,KAAK,CAACC,QAAQ,CAAC,CAAC,CAAC/B,SAAS,KAAKgB,EAAE,EAAE;QAClED,OAAO,CAACiB,KAAK,CAAC,CAAC;MACjB;IACF,CAAC,CAAC;IACF,OAAO,MAAML,GAAG,CAACM,MAAM,CAAC,CAAC;EAC3B,CAAC,EAAE,CAAClB,OAAO,EAAEC,EAAE,EAAET,gBAAgB,CAAC,CAAC;EAEnCxB,SAAS,CAAC,MAAM;IACd,IAAIyB,SAAS,KAAKa,SAAS,EAAE;MAC3B;IACF;IACA,MAAMa,KAAK,GAAGnB,OAAO,CAACe,KAAK,CAACC,QAAQ,CAAC,CAAC;IACtC,IAAIvB,SAAS,EAAE;MACb;MACA;MACA;MACA,IACE0B,KAAK,CAAClC,SAAS,KAAKgB,EAAE,IACtBkB,KAAK,CAACC,YAAY,EAAEnB,EAAE,KAAKC,SAAS,CAACC,OAAO,CAACF,EAAE,EAC/C;QACAD,OAAO,CAACqB,IAAI,CAAC,CAAC;MAChB,CAAC,MAAM;QACLrB,OAAO,CAACI,SAAS,CAACF,SAAS,CAACC,OAAO,EAAE;UACnCnB,QAAQ,EAAE,IAAI;UACdC,SAAS,EAAEgB;QACb,CAAC,CAAC;MACJ;IACF,CAAC,MAAM,IAAIkB,KAAK,CAAClC,SAAS,KAAKgB,EAAE,EAAE;MACjC;MACA;MACAD,OAAO,CAACiB,KAAK,CAAC,CAAC;IACjB;EACF,CAAC,EAAE,CAACjB,OAAO,EAAEC,EAAE,EAAER,SAAS,CAAC,CAAC;EAE5BlB,cAAc,CAAC;IACb+C,MAAM,EAAE5B,cAAc;IACtB6B,QAAQ,EAAGC,CAAC,IAAK7B,WAAW,GAAG6B,CAAC,CAACC,SAAS,CAAC;IAC3C7B;EACF,CAAC,CAAC;EAEF,oBACEf,KAAA,CAACR,IAAI;IAACwB,KAAK,EAAE,CAAC6B,MAAM,CAACC,SAAS,EAAE9B,KAAK,CAAE;IAAA,GAAKC,IAAI;IAAA8B,QAAA,gBAC9CjD,IAAA,CAACF,YAAY;MAACQ,SAAS,EAAEgB,EAAG;MAACJ,KAAK,EAAE6B,MAAM,CAACG;IAAQ,CAAE,CAAC,EACrD3C,QAAQ,gBAAGP,IAAA,CAACH,aAAa,IAAE,CAAC,GAAG,IAAI;EAAA,CAChC,CAAC;AAEX,CACF,CAAC;AAEDM,WAAW,CAACgD,WAAW,GAAG,aAAa;AAEvC,MAAMJ,MAAM,GAAGtD,UAAU,CAAC2D,MAAM,CAAC;EAC/BJ,SAAS,EAAE;IACTK,eAAe,EAAE,MAAM;IACvBC,QAAQ,EAAE;EACZ,CAAC;EACDJ,OAAO,EAAE;IACPK,QAAQ,EAAE,UAAU;IACpBC,GAAG,EAAE,CAAC;IACNC,IAAI,EAAE,CAAC;IACPC,KAAK,EAAE,CAAC;IACRC,MAAM,EAAE;EACV;AACF,CAAC,CAAC","ignoreList":[]}
@@ -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";
@@ -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 type { OrientationLock, ResizeMode, VideoSource } from '../types/video.js';
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 function VideoPlayer({ source, autoplay, surfaceId, controls, resizeMode, orientation, fullscreenOrientation, style, ...rest }: VideoPlayerProps): import("react").JSX.Element;
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,EAAoB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAEhE,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAgB,CAAC;AAI/E,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;;;;OAIG;IACH,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,eAAe,CAAC;CACzC;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,QAAe,EACf,SAAS,EACT,QAAe,EACf,UAAU,EACV,WAAW,EACX,qBAAqB,EACrB,KAAK,EACL,GAAG,IAAI,EACR,EAAE,gBAAgB,+BAuClB"}
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",
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/grdp99/react-native-au-video.git"
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/grdp99/react-native-au-video/issues"
56
+ "url": "https://github.com/gurdeep99/react-native-au-video/issues"
55
57
  },
56
- "homepage": "https://github.com/grdp99/react-native-au-video#readme",
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 type { OrientationLock, ResizeMode, VideoSource } from '../types/video';
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 function VideoPlayer({
46
- source,
47
- autoplay = true,
48
- surfaceId,
49
- controls = true,
50
- resizeMode,
51
- orientation,
52
- fullscreenOrientation,
53
- style,
54
- ...rest
55
- }: VideoPlayerProps) {
56
- const manager = useVideoManager();
57
- const id = surfaceId ?? `player:${source.id}`;
58
-
59
- useEffect(() => {
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]);
65
-
66
- useEffect(() => {
67
- if (resizeMode) {
68
- manager.setResizeMode(resizeMode);
69
- }
70
- }, [manager, resizeMode]);
71
-
72
- useEffect(() => {
73
- if (!orientation || orientation === 'auto') {
74
- return;
75
- }
76
- manager.setOrientation(orientation);
77
- return () => manager.setOrientation('auto');
78
- }, [manager, orientation]);
79
-
80
- useEffect(() => {
81
- if (!fullscreenOrientation) {
82
- return;
83
- }
84
- manager.setFullscreenOrientation(fullscreenOrientation);
85
- return () => manager.setFullscreenOrientation(null);
86
- }, [manager, fullscreenOrientation]);
87
-
88
- return (
89
- <View style={[styles.container, style]} {...rest}>
90
- <VideoSurface surfaceId={id} style={styles.surface} />
91
- {controls ? <VideoControls /> : null}
92
- </View>
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,