@venomousone/rn-videokit 0.1.0

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.
Files changed (145) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +248 -0
  3. package/lib/module/components/VideoKit.js +347 -0
  4. package/lib/module/components/VideoKit.js.map +1 -0
  5. package/lib/module/components/controls/FullscreenButton.js +38 -0
  6. package/lib/module/components/controls/FullscreenButton.js.map +1 -0
  7. package/lib/module/components/controls/Icon.js +71 -0
  8. package/lib/module/components/controls/Icon.js.map +1 -0
  9. package/lib/module/components/controls/PlayPauseButton.js +33 -0
  10. package/lib/module/components/controls/PlayPauseButton.js.map +1 -0
  11. package/lib/module/components/controls/Scrubber.js +146 -0
  12. package/lib/module/components/controls/Scrubber.js.map +1 -0
  13. package/lib/module/components/controls/SpeedButton.js +96 -0
  14. package/lib/module/components/controls/SpeedButton.js.map +1 -0
  15. package/lib/module/components/controls/TimeDisplay.js +28 -0
  16. package/lib/module/components/controls/TimeDisplay.js.map +1 -0
  17. package/lib/module/components/controls/VolumeButton.js +31 -0
  18. package/lib/module/components/controls/VolumeButton.js.map +1 -0
  19. package/lib/module/components/core/VideoPlayer.js +114 -0
  20. package/lib/module/components/core/VideoPlayer.js.map +1 -0
  21. package/lib/module/components/core/VideoPlayerContext.js +119 -0
  22. package/lib/module/components/core/VideoPlayerContext.js.map +1 -0
  23. package/lib/module/components/core/index.js +5 -0
  24. package/lib/module/components/core/index.js.map +1 -0
  25. package/lib/module/components/index.js +14 -0
  26. package/lib/module/components/index.js.map +1 -0
  27. package/lib/module/components/overlays/BufferingOverlay.js +24 -0
  28. package/lib/module/components/overlays/BufferingOverlay.js.map +1 -0
  29. package/lib/module/components/overlays/DoubleTapSeek.js +95 -0
  30. package/lib/module/components/overlays/DoubleTapSeek.js.map +1 -0
  31. package/lib/module/components/overlays/ErrorOverlay.js +60 -0
  32. package/lib/module/components/overlays/ErrorOverlay.js.map +1 -0
  33. package/lib/module/components/overlays/GestureIndicator.js +118 -0
  34. package/lib/module/components/overlays/GestureIndicator.js.map +1 -0
  35. package/lib/module/components/overlays/LoadingPoster.js +22 -0
  36. package/lib/module/components/overlays/LoadingPoster.js.map +1 -0
  37. package/lib/module/hooks/index.js +6 -0
  38. package/lib/module/hooks/index.js.map +1 -0
  39. package/lib/module/hooks/useVideoBrightness.js +33 -0
  40. package/lib/module/hooks/useVideoBrightness.js.map +1 -0
  41. package/lib/module/hooks/useVideoControls.js +64 -0
  42. package/lib/module/hooks/useVideoControls.js.map +1 -0
  43. package/lib/module/hooks/useVideoOrientation.js +24 -0
  44. package/lib/module/hooks/useVideoOrientation.js.map +1 -0
  45. package/lib/module/hooks/useVideoPlayer.js +59 -0
  46. package/lib/module/hooks/useVideoPlayer.js.map +1 -0
  47. package/lib/module/hooks/useVideoVolume.js +42 -0
  48. package/lib/module/hooks/useVideoVolume.js.map +1 -0
  49. package/lib/module/index.js +7 -0
  50. package/lib/module/index.js.map +1 -0
  51. package/lib/module/package.json +1 -0
  52. package/lib/module/types/index.js +4 -0
  53. package/lib/module/types/index.js.map +1 -0
  54. package/lib/module/utils/clamp.js +8 -0
  55. package/lib/module/utils/clamp.js.map +1 -0
  56. package/lib/module/utils/formatTime.js +12 -0
  57. package/lib/module/utils/formatTime.js.map +1 -0
  58. package/lib/module/utils/index.js +5 -0
  59. package/lib/module/utils/index.js.map +1 -0
  60. package/lib/typescript/package.json +1 -0
  61. package/lib/typescript/src/components/VideoKit.d.ts +3 -0
  62. package/lib/typescript/src/components/VideoKit.d.ts.map +1 -0
  63. package/lib/typescript/src/components/controls/FullscreenButton.d.ts +6 -0
  64. package/lib/typescript/src/components/controls/FullscreenButton.d.ts.map +1 -0
  65. package/lib/typescript/src/components/controls/Icon.d.ts +10 -0
  66. package/lib/typescript/src/components/controls/Icon.d.ts.map +1 -0
  67. package/lib/typescript/src/components/controls/PlayPauseButton.d.ts +2 -0
  68. package/lib/typescript/src/components/controls/PlayPauseButton.d.ts.map +1 -0
  69. package/lib/typescript/src/components/controls/Scrubber.d.ts +7 -0
  70. package/lib/typescript/src/components/controls/Scrubber.d.ts.map +1 -0
  71. package/lib/typescript/src/components/controls/SpeedButton.d.ts +2 -0
  72. package/lib/typescript/src/components/controls/SpeedButton.d.ts.map +1 -0
  73. package/lib/typescript/src/components/controls/TimeDisplay.d.ts +2 -0
  74. package/lib/typescript/src/components/controls/TimeDisplay.d.ts.map +1 -0
  75. package/lib/typescript/src/components/controls/VolumeButton.d.ts +2 -0
  76. package/lib/typescript/src/components/controls/VolumeButton.d.ts.map +1 -0
  77. package/lib/typescript/src/components/core/VideoPlayer.d.ts +14 -0
  78. package/lib/typescript/src/components/core/VideoPlayer.d.ts.map +1 -0
  79. package/lib/typescript/src/components/core/VideoPlayerContext.d.ts +48 -0
  80. package/lib/typescript/src/components/core/VideoPlayerContext.d.ts.map +1 -0
  81. package/lib/typescript/src/components/core/index.d.ts +3 -0
  82. package/lib/typescript/src/components/core/index.d.ts.map +1 -0
  83. package/lib/typescript/src/components/index.d.ts +6 -0
  84. package/lib/typescript/src/components/index.d.ts.map +1 -0
  85. package/lib/typescript/src/components/overlays/BufferingOverlay.d.ts +2 -0
  86. package/lib/typescript/src/components/overlays/BufferingOverlay.d.ts.map +1 -0
  87. package/lib/typescript/src/components/overlays/DoubleTapSeek.d.ts +5 -0
  88. package/lib/typescript/src/components/overlays/DoubleTapSeek.d.ts.map +1 -0
  89. package/lib/typescript/src/components/overlays/ErrorOverlay.d.ts +6 -0
  90. package/lib/typescript/src/components/overlays/ErrorOverlay.d.ts.map +1 -0
  91. package/lib/typescript/src/components/overlays/GestureIndicator.d.ts +9 -0
  92. package/lib/typescript/src/components/overlays/GestureIndicator.d.ts.map +1 -0
  93. package/lib/typescript/src/components/overlays/LoadingPoster.d.ts +6 -0
  94. package/lib/typescript/src/components/overlays/LoadingPoster.d.ts.map +1 -0
  95. package/lib/typescript/src/hooks/index.d.ts +4 -0
  96. package/lib/typescript/src/hooks/index.d.ts.map +1 -0
  97. package/lib/typescript/src/hooks/useVideoBrightness.d.ts +5 -0
  98. package/lib/typescript/src/hooks/useVideoBrightness.d.ts.map +1 -0
  99. package/lib/typescript/src/hooks/useVideoControls.d.ts +11 -0
  100. package/lib/typescript/src/hooks/useVideoControls.d.ts.map +1 -0
  101. package/lib/typescript/src/hooks/useVideoOrientation.d.ts +6 -0
  102. package/lib/typescript/src/hooks/useVideoOrientation.d.ts.map +1 -0
  103. package/lib/typescript/src/hooks/useVideoPlayer.d.ts +7 -0
  104. package/lib/typescript/src/hooks/useVideoPlayer.d.ts.map +1 -0
  105. package/lib/typescript/src/hooks/useVideoVolume.d.ts +6 -0
  106. package/lib/typescript/src/hooks/useVideoVolume.d.ts.map +1 -0
  107. package/lib/typescript/src/index.d.ts +6 -0
  108. package/lib/typescript/src/index.d.ts.map +1 -0
  109. package/lib/typescript/src/types/index.d.ts +96 -0
  110. package/lib/typescript/src/types/index.d.ts.map +1 -0
  111. package/lib/typescript/src/utils/clamp.d.ts +2 -0
  112. package/lib/typescript/src/utils/clamp.d.ts.map +1 -0
  113. package/lib/typescript/src/utils/formatTime.d.ts +2 -0
  114. package/lib/typescript/src/utils/formatTime.d.ts.map +1 -0
  115. package/lib/typescript/src/utils/index.d.ts +3 -0
  116. package/lib/typescript/src/utils/index.d.ts.map +1 -0
  117. package/package.json +191 -0
  118. package/src/components/VideoKit.tsx +415 -0
  119. package/src/components/controls/FullscreenButton.tsx +29 -0
  120. package/src/components/controls/Icon.tsx +71 -0
  121. package/src/components/controls/PlayPauseButton.tsx +25 -0
  122. package/src/components/controls/Scrubber.tsx +157 -0
  123. package/src/components/controls/SpeedButton.tsx +86 -0
  124. package/src/components/controls/TimeDisplay.tsx +21 -0
  125. package/src/components/controls/VolumeButton.tsx +23 -0
  126. package/src/components/core/VideoPlayer.tsx +148 -0
  127. package/src/components/core/VideoPlayerContext.tsx +133 -0
  128. package/src/components/core/index.ts +5 -0
  129. package/src/components/index.ts +25 -0
  130. package/src/components/overlays/BufferingOverlay.tsx +21 -0
  131. package/src/components/overlays/DoubleTapSeek.tsx +91 -0
  132. package/src/components/overlays/ErrorOverlay.tsx +49 -0
  133. package/src/components/overlays/GestureIndicator.tsx +114 -0
  134. package/src/components/overlays/LoadingPoster.tsx +21 -0
  135. package/src/hooks/index.ts +3 -0
  136. package/src/hooks/useVideoBrightness.ts +34 -0
  137. package/src/hooks/useVideoControls.ts +65 -0
  138. package/src/hooks/useVideoOrientation.ts +22 -0
  139. package/src/hooks/useVideoPlayer.ts +69 -0
  140. package/src/hooks/useVideoVolume.ts +36 -0
  141. package/src/index.ts +15 -0
  142. package/src/types/index.ts +137 -0
  143. package/src/utils/clamp.ts +4 -0
  144. package/src/utils/formatTime.ts +9 -0
  145. package/src/utils/index.ts +2 -0
@@ -0,0 +1,86 @@
1
+ import { useState } from 'react';
2
+ import { Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
3
+ import { useVideoStore } from '../core/VideoPlayerContext';
4
+
5
+ const SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 2];
6
+
7
+ export function SpeedButton() {
8
+ const speed = useVideoStore((s) => s.speed);
9
+ const { setSpeed } = useVideoStore((s) => s);
10
+ const [open, setOpen] = useState(false);
11
+
12
+ return (
13
+ <>
14
+ <TouchableOpacity
15
+ onPress={() => setOpen(true)}
16
+ style={styles.btn}
17
+ hitSlop={12}
18
+ >
19
+ <Text style={styles.label}>{speed === 1 ? '1×' : `${speed}×`}</Text>
20
+ </TouchableOpacity>
21
+
22
+ <Modal transparent visible={open} onRequestClose={() => setOpen(false)}>
23
+ <TouchableOpacity
24
+ style={styles.backdrop}
25
+ activeOpacity={1}
26
+ onPress={() => setOpen(false)}
27
+ >
28
+ <View style={styles.sheet}>
29
+ <Text style={styles.title}>Playback Speed</Text>
30
+ {SPEEDS.map((s) => (
31
+ <TouchableOpacity
32
+ key={s}
33
+ style={[styles.option, speed === s && styles.optionActive]}
34
+ onPress={() => {
35
+ setSpeed(s);
36
+ setOpen(false);
37
+ }}
38
+ >
39
+ <Text
40
+ style={[
41
+ styles.optionText,
42
+ speed === s && styles.optionTextActive,
43
+ ]}
44
+ >
45
+ {s === 1 ? 'Normal' : `${s}×`}
46
+ </Text>
47
+ </TouchableOpacity>
48
+ ))}
49
+ </View>
50
+ </TouchableOpacity>
51
+ </Modal>
52
+ </>
53
+ );
54
+ }
55
+
56
+ const styles = StyleSheet.create({
57
+ btn: { padding: 4 },
58
+ label: { color: '#fff', fontSize: 12, fontWeight: '700' },
59
+ backdrop: {
60
+ flex: 1,
61
+ backgroundColor: 'rgba(0,0,0,0.5)',
62
+ justifyContent: 'flex-end',
63
+ },
64
+ sheet: {
65
+ backgroundColor: '#1a1a1a',
66
+ borderTopLeftRadius: 16,
67
+ borderTopRightRadius: 16,
68
+ padding: 20,
69
+ paddingBottom: 36,
70
+ gap: 4,
71
+ },
72
+ title: {
73
+ color: '#aaa',
74
+ fontSize: 13,
75
+ fontWeight: '600',
76
+ marginBottom: 8,
77
+ },
78
+ option: {
79
+ paddingVertical: 12,
80
+ paddingHorizontal: 16,
81
+ borderRadius: 8,
82
+ },
83
+ optionActive: { backgroundColor: 'rgba(255,255,255,0.1)' },
84
+ optionText: { color: '#fff', fontSize: 15 },
85
+ optionTextActive: { fontWeight: '700' },
86
+ });
@@ -0,0 +1,21 @@
1
+ import { StyleSheet, Text } from 'react-native';
2
+ import { useVideoStore } from '../core/VideoPlayerContext';
3
+ import { formatTime } from '../../utils/formatTime';
4
+
5
+ export function TimeDisplay() {
6
+ const currentTime = useVideoStore((s) => s.currentTime);
7
+ const duration = useVideoStore((s) => s.duration);
8
+
9
+ return (
10
+ <Text style={styles.text}>
11
+ {formatTime(currentTime)}
12
+ <Text style={styles.separator}> / </Text>
13
+ {formatTime(duration)}
14
+ </Text>
15
+ );
16
+ }
17
+
18
+ const styles = StyleSheet.create({
19
+ text: { color: '#fff', fontSize: 12, fontWeight: '500' },
20
+ separator: { color: 'rgba(255,255,255,0.5)' },
21
+ });
@@ -0,0 +1,23 @@
1
+ import { StyleSheet, TouchableOpacity } from 'react-native';
2
+ import { useVideoStore } from '../core/VideoPlayerContext';
3
+ import { Icon } from './Icon';
4
+
5
+ export function VolumeButton() {
6
+ const muted = useVideoStore((s) => s.muted);
7
+ const { setMuted } = useVideoStore((s) => s);
8
+
9
+ return (
10
+ <TouchableOpacity
11
+ onPress={() => setMuted(!muted)}
12
+ style={styles.btn}
13
+ hitSlop={12}
14
+ >
15
+ <Icon size={22} name={muted ? 'mute' : 'volume'} />
16
+ </TouchableOpacity>
17
+ );
18
+ }
19
+
20
+ const styles = StyleSheet.create({
21
+ btn: { padding: 4 },
22
+ icon: { fontSize: 18, color: '#fff' },
23
+ });
@@ -0,0 +1,148 @@
1
+ import React, { useCallback, useRef } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import Video, {
4
+ type VideoRef,
5
+ type OnLoadData,
6
+ type OnProgressData,
7
+ type OnBufferData,
8
+ type OnVideoErrorData,
9
+ } from 'react-native-video';
10
+ import { useVideoPlayerContext, useVideoStore } from './VideoPlayerContext';
11
+ import type { VideoSource, VideoProgress } from '../../types';
12
+
13
+ interface Props {
14
+ source: VideoSource;
15
+ autoPlay: boolean;
16
+ loop: boolean;
17
+ poster?: string;
18
+ onProgress?: (p: VideoProgress) => void;
19
+ onEnd?: () => void;
20
+ onError?: (e: Error) => void;
21
+ onBuffer?: (buffering: boolean) => void;
22
+ }
23
+
24
+ export function VideoPlayer({
25
+ source,
26
+ autoPlay,
27
+ loop,
28
+ poster,
29
+ onProgress,
30
+ onEnd,
31
+ onError,
32
+ onBuffer,
33
+ }: Props) {
34
+ const { videoRef } = useVideoPlayerContext();
35
+ const {
36
+ setStatus,
37
+ setCurrentTime,
38
+ setDuration,
39
+ setBuffered,
40
+ setError,
41
+ status,
42
+ } = useVideoStore((s) => s); // state as reactive subscriptions
43
+ const muted = useVideoStore((s) => s.muted);
44
+ const speed = useVideoStore((s) => s.speed);
45
+ const isPaused = useVideoStore(
46
+ (s) => s.status === 'paused' || s.status === 'idle' || s.status === 'error'
47
+ );
48
+
49
+ const handleLoad = useCallback(
50
+ (data: OnLoadData) => {
51
+ setDuration(data.duration);
52
+ setStatus(autoPlay ? 'playing' : 'paused');
53
+ },
54
+ [autoPlay, setDuration, setStatus]
55
+ );
56
+
57
+ const handleProgress = useCallback(
58
+ (data: OnProgressData) => {
59
+ setCurrentTime(data.currentTime);
60
+ setBuffered(data.playableDuration);
61
+ onProgress?.({
62
+ currentTime: data.currentTime,
63
+ playableDuration: data.playableDuration,
64
+ seekableDuration: data.seekableDuration,
65
+ });
66
+ },
67
+ [onProgress, setCurrentTime, setBuffered]
68
+ );
69
+
70
+ const bufferTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
71
+
72
+ const handleBuffer = useCallback(
73
+ ({ isBuffering }: OnBufferData) => {
74
+ clearTimeout(bufferTimerRef.current);
75
+ if (isBuffering) {
76
+ setStatus('buffering');
77
+ onBuffer?.(true);
78
+ } else {
79
+ // debounce the recovery — ignore spurious false events
80
+ bufferTimerRef.current = setTimeout(() => {
81
+ setStatus('playing');
82
+ onBuffer?.(false);
83
+ }, 300);
84
+ }
85
+ },
86
+ [onBuffer, setStatus]
87
+ );
88
+
89
+ const handleError = useCallback(
90
+ (e: OnVideoErrorData) => {
91
+ const msg = e.error?.errorString ?? 'Playback error';
92
+ setError(msg);
93
+ setStatus('error');
94
+ onError?.(new Error(msg));
95
+ },
96
+ [onError, setError, setStatus]
97
+ );
98
+
99
+ const handleEnd = useCallback(() => {
100
+ setStatus('paused');
101
+ onEnd?.();
102
+ }, [onEnd, setStatus]);
103
+
104
+ return (
105
+ <Video
106
+ ref={videoRef as React.RefObject<VideoRef>}
107
+ source={{
108
+ bufferConfig: {
109
+ minBufferMs: 2500,
110
+ maxBufferMs: 10000,
111
+ bufferForPlaybackMs: 1000,
112
+ bufferForPlaybackAfterRebufferMs: 2000,
113
+ },
114
+ ...(source as any),
115
+ }}
116
+ poster={poster}
117
+ paused={isPaused}
118
+ muted={muted}
119
+ rate={speed}
120
+ repeat={loop}
121
+ style={StyleSheet.absoluteFill}
122
+ resizeMode="contain"
123
+ controls={false}
124
+ progressUpdateInterval={250}
125
+ onLoad={handleLoad}
126
+ onProgress={handleProgress}
127
+ onBuffer={handleBuffer}
128
+ onError={handleError}
129
+ onEnd={handleEnd}
130
+ pointerEvents="none"
131
+ onLoadStart={() => {
132
+ setStatus('loading');
133
+ }}
134
+ onReadyForDisplay={() => {
135
+ // only auto-play if we haven't been manually paused
136
+ if (status === 'loading' || status === 'buffering') {
137
+ setStatus(autoPlay ? 'playing' : 'paused');
138
+ }
139
+ }}
140
+ bufferConfig={{
141
+ minBufferMs: 2500,
142
+ maxBufferMs: 10000,
143
+ bufferForPlaybackMs: 1000,
144
+ bufferForPlaybackAfterRebufferMs: 2000,
145
+ }}
146
+ />
147
+ );
148
+ }
@@ -0,0 +1,133 @@
1
+ import React, { createContext, useContext, useRef } from 'react';
2
+ import { useRef as useStoreRef } from 'react';
3
+ import type { VideoRef } from 'react-native-video';
4
+ import { createStore, useStore } from 'zustand';
5
+ import type { PlaybackStatus, VideoKitTheme } from '../../types';
6
+
7
+ //Theme
8
+
9
+ const ThemeContext = createContext<VideoKitTheme>({});
10
+ export const useVideoTheme = () => useContext(ThemeContext);
11
+
12
+ // ─── Store shape ─────────────────────────────────────────────────────────────
13
+
14
+ interface VideoState {
15
+ status: PlaybackStatus;
16
+ currentTime: number;
17
+ duration: number;
18
+ buffered: number;
19
+ muted: boolean;
20
+ speed: number;
21
+ isFullscreen: boolean;
22
+ controlsVisible: boolean;
23
+ error: string | null;
24
+ setStatus: (status: PlaybackStatus) => void;
25
+ setCurrentTime: (t: number) => void;
26
+ setDuration: (d: number) => void;
27
+ setBuffered: (b: number) => void;
28
+ setMuted: (m: boolean) => void;
29
+ setSpeed: (s: number) => void;
30
+ setFullscreen: (f: boolean) => void;
31
+ setControlsVisible: (v: boolean) => void;
32
+ setError: (e: string | null) => void;
33
+ reset: () => void;
34
+ }
35
+
36
+ const initialState = {
37
+ status: 'idle' as PlaybackStatus,
38
+ currentTime: 0,
39
+ duration: 0,
40
+ buffered: 0,
41
+ muted: false,
42
+ speed: 1,
43
+ isFullscreen: false,
44
+ controlsVisible: true,
45
+ error: null,
46
+ };
47
+
48
+ export type VideoStore = ReturnType<typeof createVideoStore>;
49
+
50
+ export const createVideoStore = () =>
51
+ createStore<VideoState>((set) => ({
52
+ ...initialState,
53
+ setStatus: (status) => set({ status }),
54
+ setCurrentTime: (currentTime) => set({ currentTime }),
55
+ setDuration: (duration) => set({ duration }),
56
+ setBuffered: (buffered) => set({ buffered }),
57
+ setMuted: (muted) => set({ muted }),
58
+ setSpeed: (speed) => set({ speed }),
59
+ setFullscreen: (isFullscreen) => set({ isFullscreen }),
60
+ setControlsVisible: (controlsVisible) => set({ controlsVisible }),
61
+ setError: (error) => set({ error }),
62
+ reset: () => set(initialState),
63
+ }));
64
+
65
+ // ─── Context ─────────────────────────────────────────────────────────────────
66
+
67
+ interface VideoPlayerContextValue {
68
+ videoRef: React.RefObject<VideoRef | null>;
69
+ store: VideoStore;
70
+ }
71
+
72
+ const VideoPlayerContext = createContext<VideoPlayerContextValue | null>(null);
73
+
74
+ // ─── Provider ────────────────────────────────────────────────────────────────
75
+
76
+ export const VideoPlayerProvider = ({
77
+ children,
78
+ theme,
79
+ }: {
80
+ children: React.ReactNode;
81
+ theme?: VideoKitTheme;
82
+ }) => {
83
+ const videoRef = useRef<VideoRef | null>(null);
84
+ const storeRef = useStoreRef<VideoStore | null>(null);
85
+
86
+ // create the store once per provider instance
87
+ if (storeRef.current === null) {
88
+ storeRef.current = createVideoStore();
89
+ }
90
+
91
+ return (
92
+ <VideoPlayerContext.Provider value={{ videoRef, store: storeRef.current }}>
93
+ <ThemeContext.Provider value={theme ?? {}}>
94
+ {children}
95
+ </ThemeContext.Provider>
96
+ </VideoPlayerContext.Provider>
97
+ );
98
+ };
99
+
100
+ // ─── Hooks ───────────────────────────────────────────────────────────────────
101
+
102
+ export const useVideoPlayerContext = () => {
103
+ const ctx = useContext(VideoPlayerContext);
104
+ if (!ctx)
105
+ throw new Error(
106
+ 'useVideoPlayerContext must be used inside VideoPlayerProvider'
107
+ );
108
+ return ctx;
109
+ };
110
+
111
+ /**
112
+ * Scoped reactive store hook — replaces the old global useVideoStore.
113
+ * Use this everywhere instead of the old import.
114
+ */
115
+ export const useVideoStore: <T>(selector: (state: VideoState) => T) => T = (
116
+ selector
117
+ ) => {
118
+ const { store } = useVideoPlayerContext();
119
+ return useStore(store, selector);
120
+ };
121
+ // export function useVideoStore<T>(selector: (state: VideoState) => T): T {
122
+ // const { store } = useVideoPlayerContext();
123
+ // return useStore(store, selector);
124
+ // }
125
+
126
+ /**
127
+ * For imperative access (getState / setState) without subscribing.
128
+ * Use in callbacks and gesture handlers.
129
+ */
130
+ export const useVideoStoreApi = () => {
131
+ const { store } = useVideoPlayerContext();
132
+ return store;
133
+ };
@@ -0,0 +1,5 @@
1
+ export { useVideoStore } from './VideoPlayerContext';
2
+ export {
3
+ VideoPlayerProvider,
4
+ useVideoPlayerContext,
5
+ } from './VideoPlayerContext';
@@ -0,0 +1,25 @@
1
+ // ─── Main component ───────────────────────────────────────────────────────────
2
+ export { VideoPlayer } from './core/VideoPlayer';
3
+ export { VideoKit } from './VideoKit';
4
+
5
+ // ─── Headless hook ────────────────────────────────────────────────────────────
6
+ export { useVideoPlayer } from '../hooks/useVideoPlayer';
7
+
8
+ // ─── Context & store (for advanced usage) ────────────────────────────────────
9
+ export {
10
+ VideoPlayerProvider,
11
+ useVideoPlayerContext,
12
+ useVideoStore,
13
+ } from './core/VideoPlayerContext';
14
+
15
+ // ─── Types ────────────────────────────────────────────────────────────────────
16
+ export type {
17
+ VideoKitProps,
18
+ VideoKitTheme,
19
+ VideoKitControlsConfig,
20
+ VideoSource,
21
+ VideoPlayerAPI,
22
+ VideoPlayerState,
23
+ VideoProgress,
24
+ PlaybackStatus,
25
+ } from '../types';
@@ -0,0 +1,21 @@
1
+ import { ActivityIndicator, StyleSheet, View } from 'react-native';
2
+ import { useVideoStore } from '../core/VideoPlayerContext';
3
+
4
+ export function BufferingOverlay() {
5
+ const status = useVideoStore((s) => s.status);
6
+ if (status !== 'buffering' && status !== 'loading') return null;
7
+
8
+ return (
9
+ <View style={styles.container}>
10
+ <ActivityIndicator size="large" color="#fff" />
11
+ </View>
12
+ );
13
+ }
14
+
15
+ const styles = StyleSheet.create({
16
+ container: {
17
+ ...StyleSheet.absoluteFill,
18
+ alignItems: 'center',
19
+ justifyContent: 'center',
20
+ },
21
+ });
@@ -0,0 +1,91 @@
1
+ import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+ import Animated, {
4
+ useSharedValue,
5
+ useAnimatedStyle,
6
+ withSequence,
7
+ withTiming,
8
+ } from 'react-native-reanimated';
9
+ import { scheduleOnRN } from 'react-native-worklets';
10
+
11
+ export interface DoubleTapSeekHandle {
12
+ show: (side: 'left' | 'right', seconds: number) => void;
13
+ }
14
+
15
+ export const DoubleTapSeek = forwardRef<DoubleTapSeekHandle>((_, ref) => {
16
+ const [state, setState] = useState<{
17
+ side: 'left' | 'right';
18
+ seconds: number;
19
+ } | null>(null);
20
+
21
+ const opacity = useSharedValue(0);
22
+ const scale = useSharedValue(0.8);
23
+ const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
24
+
25
+ const hide = () => setState(null);
26
+
27
+ useImperativeHandle(ref, () => ({
28
+ show: (side, seconds) => {
29
+ clearTimeout(timerRef.current);
30
+ setState({ side, seconds });
31
+ opacity.value = withSequence(
32
+ withTiming(1, { duration: 100 }),
33
+ withTiming(1, { duration: 400 }),
34
+ withTiming(0, { duration: 200 }, (done) => {
35
+ if (done) scheduleOnRN(hide);
36
+ })
37
+ );
38
+ scale.value = withSequence(
39
+ withTiming(1, { duration: 150 }),
40
+ withTiming(0.9, { duration: 350 })
41
+ );
42
+ },
43
+ }));
44
+
45
+ const animStyle = useAnimatedStyle(() => ({
46
+ opacity: opacity.value,
47
+ transform: [{ scale: scale.value }],
48
+ }));
49
+
50
+ if (!state) return null;
51
+
52
+ const isLeft = state.side === 'left';
53
+
54
+ return (
55
+ <View
56
+ style={[styles.container, isLeft ? styles.left : styles.right]}
57
+ pointerEvents="none"
58
+ >
59
+ <Animated.View style={[styles.ripple, animStyle]}>
60
+ <Text style={styles.arrow}>{isLeft ? '«' : '»'}</Text>
61
+ <Text style={styles.label}>
62
+ {isLeft ? `-${state.seconds}s` : `+${state.seconds}s`}
63
+ </Text>
64
+ </Animated.View>
65
+ </View>
66
+ );
67
+ });
68
+
69
+ const styles = StyleSheet.create({
70
+ container: {
71
+ position: 'absolute',
72
+ top: 0,
73
+ bottom: 0,
74
+ width: '40%',
75
+ alignItems: 'center',
76
+ justifyContent: 'center',
77
+ },
78
+ left: { left: 0 },
79
+ right: { right: 0 },
80
+ ripple: {
81
+ width: 80,
82
+ height: 80,
83
+ borderRadius: 40,
84
+ backgroundColor: 'rgba(255,255,255,0.15)',
85
+ alignItems: 'center',
86
+ justifyContent: 'center',
87
+ gap: 2,
88
+ },
89
+ arrow: { color: '#fff', fontSize: 20, fontWeight: '700' },
90
+ label: { color: '#fff', fontSize: 11, fontWeight: '600' },
91
+ });
@@ -0,0 +1,49 @@
1
+ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
2
+ import { useVideoStore } from '../core/VideoPlayerContext';
3
+
4
+ interface Props {
5
+ onRetry?: () => void;
6
+ }
7
+
8
+ export function ErrorOverlay({ onRetry }: Props) {
9
+ const status = useVideoStore((s) => s.status);
10
+ const error = useVideoStore((s) => s.error);
11
+ if (status !== 'error') return null;
12
+
13
+ return (
14
+ <View style={styles.container}>
15
+ <Text style={styles.icon}>⚠️</Text>
16
+ <Text style={styles.message}>{error ?? 'Something went wrong'}</Text>
17
+ {onRetry && (
18
+ <TouchableOpacity style={styles.retryBtn} onPress={onRetry}>
19
+ <Text style={styles.retryText}>Retry</Text>
20
+ </TouchableOpacity>
21
+ )}
22
+ </View>
23
+ );
24
+ }
25
+
26
+ const styles = StyleSheet.create({
27
+ container: {
28
+ ...StyleSheet.absoluteFill,
29
+ alignItems: 'center',
30
+ justifyContent: 'center',
31
+ backgroundColor: 'rgba(0,0,0,0.75)',
32
+ gap: 12,
33
+ },
34
+ icon: { fontSize: 36 },
35
+ message: {
36
+ color: '#fff',
37
+ fontSize: 14,
38
+ textAlign: 'center',
39
+ paddingHorizontal: 24,
40
+ },
41
+ retryBtn: {
42
+ marginTop: 8,
43
+ paddingHorizontal: 24,
44
+ paddingVertical: 10,
45
+ backgroundColor: '#fff',
46
+ borderRadius: 20,
47
+ },
48
+ retryText: { color: '#000', fontWeight: '700', fontSize: 14 },
49
+ });