@movementinfra/expo-twostep-video 0.1.8 → 0.1.10
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/build/ExpoTwoStepVideo.types.d.ts +16 -0
- package/build/ExpoTwoStepVideo.types.d.ts.map +1 -1
- package/build/ExpoTwoStepVideo.types.js.map +1 -1
- package/build/TwoStepPlayerControllerView.d.ts +30 -0
- package/build/TwoStepPlayerControllerView.d.ts.map +1 -0
- package/build/TwoStepPlayerControllerView.js +54 -0
- package/build/TwoStepPlayerControllerView.js.map +1 -0
- package/build/VideoScrubber.d.ts +22 -0
- package/build/VideoScrubber.d.ts.map +1 -0
- package/build/VideoScrubber.js +489 -0
- package/build/VideoScrubber.js.map +1 -0
- package/build/VideoScrubber.types.d.ts +78 -0
- package/build/VideoScrubber.types.d.ts.map +1 -0
- package/build/VideoScrubber.types.js +2 -0
- package/build/VideoScrubber.types.js.map +1 -0
- package/build/hooks/useVideoScrubber.d.ts +86 -0
- package/build/hooks/useVideoScrubber.d.ts.map +1 -0
- package/build/hooks/useVideoScrubber.js +114 -0
- package/build/hooks/useVideoScrubber.js.map +1 -0
- package/build/index.d.ts +5 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/ios/ExpoTwoStepVideoModule.swift +72 -8
- package/ios/ExpoTwoStepVideoView.swift +103 -1
- package/ios/ExpoTwostepPlayerControllerView.swift +297 -0
- package/package.json +1 -1
|
@@ -62,4 +62,20 @@ export type TwoStepVideoViewRef = {
|
|
|
62
62
|
/** Restart playback from the beginning */
|
|
63
63
|
replay: () => Promise<void>;
|
|
64
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* Video player controller view props (with native controls)
|
|
67
|
+
*/
|
|
68
|
+
export type TwoStepPlayerControllerViewProps = TwoStepVideoViewProps & {
|
|
69
|
+
/** Show native playback controls (default: true) */
|
|
70
|
+
showsPlaybackControls?: boolean;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Ref handle for video player controller view (with fullscreen support)
|
|
74
|
+
*/
|
|
75
|
+
export type TwoStepPlayerControllerViewRef = TwoStepVideoViewRef & {
|
|
76
|
+
/** Enter fullscreen mode */
|
|
77
|
+
enterFullscreen: () => Promise<void>;
|
|
78
|
+
/** Exit fullscreen mode */
|
|
79
|
+
exitFullscreen: () => Promise<void>;
|
|
80
|
+
};
|
|
65
81
|
//# sourceMappingURL=ExpoTwoStepVideo.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoTwoStepVideo.types.d.ts","sourceRoot":"","sources":["../src/ExpoTwoStepVideo.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0CAA0C;IAC1C,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,mBAAmB,CAAA;KAAE,KAAK,IAAI,CAAC;IAC/E,iDAAiD;IACjD,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,aAAa,CAAA;KAAE,KAAK,IAAI,CAAC;IAC7D,wEAAwE;IACxE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;KAAE,KAAK,IAAI,CAAC;IAChE,kCAAkC;IAClC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,UAAU,CAAA;KAAE,KAAK,IAAI,CAAC;IACvD,iBAAiB;IACjB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,qBAAqB;IACrB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,qBAAqB;IACrB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,yCAAyC;IACzC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,0CAA0C;IAC1C,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B,CAAC"}
|
|
1
|
+
{"version":3,"file":"ExpoTwoStepVideo.types.d.ts","sourceRoot":"","sources":["../src/ExpoTwoStepVideo.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0CAA0C;IAC1C,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,mBAAmB,CAAA;KAAE,KAAK,IAAI,CAAC;IAC/E,iDAAiD;IACjD,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,aAAa,CAAA;KAAE,KAAK,IAAI,CAAC;IAC7D,wEAAwE;IACxE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;KAAE,KAAK,IAAI,CAAC;IAChE,kCAAkC;IAClC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,UAAU,CAAA;KAAE,KAAK,IAAI,CAAC;IACvD,iBAAiB;IACjB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,qBAAqB;IACrB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,qBAAqB;IACrB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,yCAAyC;IACzC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,0CAA0C;IAC1C,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gCAAgC,GAAG,qBAAqB,GAAG;IACrE,oDAAoD;IACpD,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,8BAA8B,GAAG,mBAAmB,GAAG;IACjE,4BAA4B;IAC5B,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,2BAA2B;IAC3B,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoTwoStepVideo.types.js","sourceRoot":"","sources":["../src/ExpoTwoStepVideo.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { StyleProp, ViewStyle } from 'react-native';\n\n/**\n * Playback status event payload\n */\nexport type PlaybackStatusEvent = {\n status: 'ready' | 'playing' | 'paused' | 'ended' | 'seeked';\n time?: number;\n};\n\n/**\n * Progress event payload\n */\nexport type ProgressEvent = {\n currentTime: number;\n duration: number;\n progress: number;\n};\n\n/**\n * Error event payload\n */\nexport type ErrorEvent = {\n error: string;\n};\n\n/**\n * Video player view props\n */\nexport type TwoStepVideoViewProps = {\n /** ID of a composition to play (from trimVideo, mirrorVideo, etc.) */\n compositionId?: string;\n /** ID of an asset to play (from loadAsset) */\n assetId?: string;\n /** Enable continuous looping - video will restart automatically when it ends */\n loop?: boolean;\n /** Called when playback status changes */\n onPlaybackStatusChange?: (event: { nativeEvent: PlaybackStatusEvent }) => void;\n /** Called periodically with playback progress */\n onProgress?: (event: { nativeEvent: ProgressEvent }) => void;\n /** Called when playback reaches the end (not called if loop is true) */\n onEnd?: (event: { nativeEvent: Record<string, never> }) => void;\n /** Called when an error occurs */\n onError?: (event: { nativeEvent: ErrorEvent }) => void;\n /** View style */\n style?: StyleProp<ViewStyle>;\n};\n\n/**\n * Ref handle for video player view\n */\nexport type TwoStepVideoViewRef = {\n /** Start playback */\n play: () => Promise<void>;\n /** Pause playback */\n pause: () => Promise<void>;\n /** Seek to a specific time in seconds */\n seek: (time: number) => Promise<void>;\n /** Restart playback from the beginning */\n replay: () => Promise<void>;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"ExpoTwoStepVideo.types.js","sourceRoot":"","sources":["../src/ExpoTwoStepVideo.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { StyleProp, ViewStyle } from 'react-native';\n\n/**\n * Playback status event payload\n */\nexport type PlaybackStatusEvent = {\n status: 'ready' | 'playing' | 'paused' | 'ended' | 'seeked';\n time?: number;\n};\n\n/**\n * Progress event payload\n */\nexport type ProgressEvent = {\n currentTime: number;\n duration: number;\n progress: number;\n};\n\n/**\n * Error event payload\n */\nexport type ErrorEvent = {\n error: string;\n};\n\n/**\n * Video player view props\n */\nexport type TwoStepVideoViewProps = {\n /** ID of a composition to play (from trimVideo, mirrorVideo, etc.) */\n compositionId?: string;\n /** ID of an asset to play (from loadAsset) */\n assetId?: string;\n /** Enable continuous looping - video will restart automatically when it ends */\n loop?: boolean;\n /** Called when playback status changes */\n onPlaybackStatusChange?: (event: { nativeEvent: PlaybackStatusEvent }) => void;\n /** Called periodically with playback progress */\n onProgress?: (event: { nativeEvent: ProgressEvent }) => void;\n /** Called when playback reaches the end (not called if loop is true) */\n onEnd?: (event: { nativeEvent: Record<string, never> }) => void;\n /** Called when an error occurs */\n onError?: (event: { nativeEvent: ErrorEvent }) => void;\n /** View style */\n style?: StyleProp<ViewStyle>;\n};\n\n/**\n * Ref handle for video player view\n */\nexport type TwoStepVideoViewRef = {\n /** Start playback */\n play: () => Promise<void>;\n /** Pause playback */\n pause: () => Promise<void>;\n /** Seek to a specific time in seconds */\n seek: (time: number) => Promise<void>;\n /** Restart playback from the beginning */\n replay: () => Promise<void>;\n};\n\n/**\n * Video player controller view props (with native controls)\n */\nexport type TwoStepPlayerControllerViewProps = TwoStepVideoViewProps & {\n /** Show native playback controls (default: true) */\n showsPlaybackControls?: boolean;\n};\n\n/**\n * Ref handle for video player controller view (with fullscreen support)\n */\nexport type TwoStepPlayerControllerViewRef = TwoStepVideoViewRef & {\n /** Enter fullscreen mode */\n enterFullscreen: () => Promise<void>;\n /** Exit fullscreen mode */\n exitFullscreen: () => Promise<void>;\n};\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { TwoStepPlayerControllerViewRef } from './ExpoTwoStepVideo.types';
|
|
3
|
+
/**
|
|
4
|
+
* Video player view with native iOS controls and fullscreen support
|
|
5
|
+
*
|
|
6
|
+
* Uses AVPlayerViewController under the hood for native playback controls,
|
|
7
|
+
* AirPlay support, picture-in-picture, and fullscreen mode.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const playerRef = useRef<TwoStepPlayerControllerViewRef>(null);
|
|
12
|
+
*
|
|
13
|
+
* // Play a video with native controls
|
|
14
|
+
* <TwoStepPlayerControllerView
|
|
15
|
+
* ref={playerRef}
|
|
16
|
+
* assetId={asset.id}
|
|
17
|
+
* showsPlaybackControls={true}
|
|
18
|
+
* onProgress={(e) => setProgress(e.nativeEvent.progress)}
|
|
19
|
+
* style={{ width: '100%', height: 300 }}
|
|
20
|
+
* />
|
|
21
|
+
*
|
|
22
|
+
* // Enter fullscreen mode
|
|
23
|
+
* playerRef.current?.enterFullscreen();
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
declare const TwoStepPlayerControllerView: React.ForwardRefExoticComponent<import("./ExpoTwoStepVideo.types").TwoStepVideoViewProps & {
|
|
27
|
+
showsPlaybackControls?: boolean;
|
|
28
|
+
} & React.RefAttributes<TwoStepPlayerControllerViewRef>>;
|
|
29
|
+
export default TwoStepPlayerControllerView;
|
|
30
|
+
//# sourceMappingURL=TwoStepPlayerControllerView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TwoStepPlayerControllerView.d.ts","sourceRoot":"","sources":["../src/TwoStepPlayerControllerView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,EAAoC,8BAA8B,EAAE,MAAM,0BAA0B,CAAC;AAmB5G;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,QAAA,MAAM,2BAA2B;;wDA2BhC,CAAC;AAIF,eAAe,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { requireNativeView } from 'expo';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
|
4
|
+
const NativeView = requireNativeView('ExpoTwoStepVideo_ExpoTwoStepPlayerControllerView');
|
|
5
|
+
/**
|
|
6
|
+
* Video player view with native iOS controls and fullscreen support
|
|
7
|
+
*
|
|
8
|
+
* Uses AVPlayerViewController under the hood for native playback controls,
|
|
9
|
+
* AirPlay support, picture-in-picture, and fullscreen mode.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const playerRef = useRef<TwoStepPlayerControllerViewRef>(null);
|
|
14
|
+
*
|
|
15
|
+
* // Play a video with native controls
|
|
16
|
+
* <TwoStepPlayerControllerView
|
|
17
|
+
* ref={playerRef}
|
|
18
|
+
* assetId={asset.id}
|
|
19
|
+
* showsPlaybackControls={true}
|
|
20
|
+
* onProgress={(e) => setProgress(e.nativeEvent.progress)}
|
|
21
|
+
* style={{ width: '100%', height: 300 }}
|
|
22
|
+
* />
|
|
23
|
+
*
|
|
24
|
+
* // Enter fullscreen mode
|
|
25
|
+
* playerRef.current?.enterFullscreen();
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
const TwoStepPlayerControllerView = forwardRef((props, ref) => {
|
|
29
|
+
const nativeRef = useRef(null);
|
|
30
|
+
useImperativeHandle(ref, () => ({
|
|
31
|
+
play: async () => {
|
|
32
|
+
await nativeRef.current?.play();
|
|
33
|
+
},
|
|
34
|
+
pause: async () => {
|
|
35
|
+
await nativeRef.current?.pause();
|
|
36
|
+
},
|
|
37
|
+
seek: async (time) => {
|
|
38
|
+
await nativeRef.current?.seek(time);
|
|
39
|
+
},
|
|
40
|
+
replay: async () => {
|
|
41
|
+
await nativeRef.current?.replay();
|
|
42
|
+
},
|
|
43
|
+
enterFullscreen: async () => {
|
|
44
|
+
await nativeRef.current?.enterFullscreen();
|
|
45
|
+
},
|
|
46
|
+
exitFullscreen: async () => {
|
|
47
|
+
await nativeRef.current?.exitFullscreen();
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
return <NativeView ref={nativeRef} {...props}/>;
|
|
51
|
+
});
|
|
52
|
+
TwoStepPlayerControllerView.displayName = 'TwoStepPlayerControllerView';
|
|
53
|
+
export default TwoStepPlayerControllerView;
|
|
54
|
+
//# sourceMappingURL=TwoStepPlayerControllerView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TwoStepPlayerControllerView.js","sourceRoot":"","sources":["../src/TwoStepPlayerControllerView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAmBhE,MAAM,UAAU,GAAyC,iBAAiB,CAAC,kDAAkD,CAAC,CAAC;AAE/H;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,2BAA2B,GAAG,UAAU,CAC5C,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;IACb,MAAM,SAAS,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAC;IAE9C,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;QAC9B,IAAI,EAAE,KAAK,IAAI,EAAE;YACf,MAAM,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QAClC,CAAC;QACD,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,MAAM,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;QACnC,CAAC;QACD,IAAI,EAAE,KAAK,EAAE,IAAY,EAAE,EAAE;YAC3B,MAAM,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACtC,CAAC;QACD,MAAM,EAAE,KAAK,IAAI,EAAE;YACjB,MAAM,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QACpC,CAAC;QACD,eAAe,EAAE,KAAK,IAAI,EAAE;YAC1B,MAAM,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,CAAC;QAC7C,CAAC;QACD,cAAc,EAAE,KAAK,IAAI,EAAE;YACzB,MAAM,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;QAC5C,CAAC;KACF,CAAC,CAAC,CAAC;IAEJ,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,KAAK,CAAC,EAAG,CAAC;AACnD,CAAC,CACF,CAAC;AAEF,2BAA2B,CAAC,WAAW,GAAG,6BAA6B,CAAC;AAExE,eAAe,2BAA2B,CAAC","sourcesContent":["import { requireNativeView } from 'expo';\nimport * as React from 'react';\nimport { forwardRef, useImperativeHandle, useRef } from 'react';\n\nimport { TwoStepPlayerControllerViewProps, TwoStepPlayerControllerViewRef } from './ExpoTwoStepVideo.types';\n\n// Internal props type that includes the native ref\ntype NativeViewProps = TwoStepPlayerControllerViewProps & {\n ref?: React.Ref<NativeViewRef>;\n};\n\n// Native view ref with native methods\ntype NativeViewRef = {\n play: () => Promise<void>;\n pause: () => Promise<void>;\n seek: (time: number) => Promise<void>;\n replay: () => Promise<void>;\n enterFullscreen: () => Promise<void>;\n exitFullscreen: () => Promise<void>;\n};\n\nconst NativeView: React.ComponentType<NativeViewProps> = requireNativeView('ExpoTwoStepVideo_ExpoTwoStepPlayerControllerView');\n\n/**\n * Video player view with native iOS controls and fullscreen support\n *\n * Uses AVPlayerViewController under the hood for native playback controls,\n * AirPlay support, picture-in-picture, and fullscreen mode.\n *\n * @example\n * ```tsx\n * const playerRef = useRef<TwoStepPlayerControllerViewRef>(null);\n *\n * // Play a video with native controls\n * <TwoStepPlayerControllerView\n * ref={playerRef}\n * assetId={asset.id}\n * showsPlaybackControls={true}\n * onProgress={(e) => setProgress(e.nativeEvent.progress)}\n * style={{ width: '100%', height: 300 }}\n * />\n *\n * // Enter fullscreen mode\n * playerRef.current?.enterFullscreen();\n * ```\n */\nconst TwoStepPlayerControllerView = forwardRef<TwoStepPlayerControllerViewRef, TwoStepPlayerControllerViewProps>(\n (props, ref) => {\n const nativeRef = useRef<NativeViewRef>(null);\n\n useImperativeHandle(ref, () => ({\n play: async () => {\n await nativeRef.current?.play();\n },\n pause: async () => {\n await nativeRef.current?.pause();\n },\n seek: async (time: number) => {\n await nativeRef.current?.seek(time);\n },\n replay: async () => {\n await nativeRef.current?.replay();\n },\n enterFullscreen: async () => {\n await nativeRef.current?.enterFullscreen();\n },\n exitFullscreen: async () => {\n await nativeRef.current?.exitFullscreen();\n },\n }));\n\n return <NativeView ref={nativeRef} {...props} />;\n }\n);\n\nTwoStepPlayerControllerView.displayName = 'TwoStepPlayerControllerView';\n\nexport default TwoStepPlayerControllerView;\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { VideoScrubberProps, VideoScrubberRef } from './VideoScrubber.types';
|
|
3
|
+
/**
|
|
4
|
+
* Video scrubber/trimmer component with thumbnail strip and draggable handles
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <VideoScrubber
|
|
9
|
+
* assetId={asset.id}
|
|
10
|
+
* duration={asset.duration}
|
|
11
|
+
* currentTime={currentTime}
|
|
12
|
+
* startTime={trimStart}
|
|
13
|
+
* endTime={trimEnd}
|
|
14
|
+
* onStartTimeChange={setTrimStart}
|
|
15
|
+
* onEndTimeChange={setTrimEnd}
|
|
16
|
+
* onScrubbing={(time) => playerRef.current?.seek(time)}
|
|
17
|
+
* />
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
declare const VideoScrubber: React.ForwardRefExoticComponent<VideoScrubberProps & React.RefAttributes<VideoScrubberRef>>;
|
|
21
|
+
export default VideoScrubber;
|
|
22
|
+
//# sourceMappingURL=VideoScrubber.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VideoScrubber.d.ts","sourceRoot":"","sources":["../src/VideoScrubber.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAqB/B,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAsB,MAAM,uBAAuB,CAAC;AAYjG;;;;;;;;;;;;;;;;GAgBG;AACH,QAAA,MAAM,aAAa,6FAkdjB,CAAC;AAuGH,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { View, Image, StyleSheet, PanResponder, Animated, } from 'react-native';
|
|
4
|
+
import ExpoTwoStepVideoModule from './ExpoTwoStepVideoModule';
|
|
5
|
+
const DEFAULT_THEME = {
|
|
6
|
+
handleColor: '#FFD700',
|
|
7
|
+
selectedRegionColor: 'rgba(255,215,0,0.2)',
|
|
8
|
+
playheadColor: '#FFFFFF',
|
|
9
|
+
backgroundColor: '#1c1c1e',
|
|
10
|
+
dimmedColor: 'rgba(0,0,0,0.6)',
|
|
11
|
+
borderColor: '#FFD700',
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Video scrubber/trimmer component with thumbnail strip and draggable handles
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <VideoScrubber
|
|
19
|
+
* assetId={asset.id}
|
|
20
|
+
* duration={asset.duration}
|
|
21
|
+
* currentTime={currentTime}
|
|
22
|
+
* startTime={trimStart}
|
|
23
|
+
* endTime={trimEnd}
|
|
24
|
+
* onStartTimeChange={setTrimStart}
|
|
25
|
+
* onEndTimeChange={setTrimEnd}
|
|
26
|
+
* onScrubbing={(time) => playerRef.current?.seek(time)}
|
|
27
|
+
* />
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
const VideoScrubber = forwardRef((props, ref) => {
|
|
31
|
+
const { assetId, duration, currentTime, startTime, endTime, thumbnailCount = 10, thumbnailHeight = 50, onStartTimeChange, onEndTimeChange, onScrub, onScrubbing, onScrubEnd, minDuration = 0.5, style, disabled = false, theme: userTheme, showPlayhead = true, handleWidth = 20, } = props;
|
|
32
|
+
const theme = useMemo(() => ({ ...DEFAULT_THEME, ...userTheme }), [userTheme]);
|
|
33
|
+
const [thumbnails, setThumbnails] = useState([]);
|
|
34
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
35
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
36
|
+
const [dragging, setDragging] = useState(null);
|
|
37
|
+
// Track current values during drag - synced with props when not dragging
|
|
38
|
+
const dragStartTimeRef = useRef(startTime);
|
|
39
|
+
const dragEndTimeRef = useRef(endTime);
|
|
40
|
+
const currentTimeRef = useRef(currentTime);
|
|
41
|
+
const durationRef = useRef(duration);
|
|
42
|
+
const containerWidthRef = useRef(containerWidth);
|
|
43
|
+
const handleWidthRef = useRef(handleWidth);
|
|
44
|
+
// Sync refs with props when they change from outside (e.g., new video loaded)
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!dragging) {
|
|
47
|
+
dragStartTimeRef.current = startTime;
|
|
48
|
+
dragEndTimeRef.current = endTime;
|
|
49
|
+
}
|
|
50
|
+
}, [startTime, endTime, dragging]);
|
|
51
|
+
// Keep other refs in sync
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
currentTimeRef.current = currentTime;
|
|
54
|
+
}, [currentTime]);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
durationRef.current = duration;
|
|
57
|
+
}, [duration]);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
containerWidthRef.current = containerWidth;
|
|
60
|
+
}, [containerWidth]);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
handleWidthRef.current = handleWidth;
|
|
63
|
+
}, [handleWidth]);
|
|
64
|
+
// Calculate thumbnail times
|
|
65
|
+
const thumbnailTimes = useMemo(() => {
|
|
66
|
+
if (duration <= 0)
|
|
67
|
+
return [];
|
|
68
|
+
const count = Math.min(thumbnailCount, Math.max(1, Math.ceil(duration)));
|
|
69
|
+
const interval = duration / count;
|
|
70
|
+
return Array.from({ length: count }, (_, i) => i * interval + interval / 2);
|
|
71
|
+
}, [duration, thumbnailCount]);
|
|
72
|
+
// Load thumbnails
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!assetId || thumbnailTimes.length === 0 || duration <= 0) {
|
|
75
|
+
setThumbnails([]);
|
|
76
|
+
setIsLoading(false);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
let cancelled = false;
|
|
80
|
+
setIsLoading(true);
|
|
81
|
+
ExpoTwoStepVideoModule.generateThumbnails(assetId, thumbnailTimes, { width: thumbnailHeight * 1.5, height: thumbnailHeight })
|
|
82
|
+
.then((thumbs) => {
|
|
83
|
+
if (!cancelled) {
|
|
84
|
+
setThumbnails(thumbs);
|
|
85
|
+
setIsLoading(false);
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
.catch((err) => {
|
|
89
|
+
if (!cancelled) {
|
|
90
|
+
console.error('Failed to generate thumbnails:', err);
|
|
91
|
+
setIsLoading(false);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return () => {
|
|
95
|
+
cancelled = true;
|
|
96
|
+
};
|
|
97
|
+
}, [assetId, thumbnailTimes, thumbnailHeight, duration]);
|
|
98
|
+
// The content area is between the handles
|
|
99
|
+
const contentWidth = containerWidth > 0 ? containerWidth - handleWidth * 2 : 0;
|
|
100
|
+
// Convert time to position within the content area (between handles)
|
|
101
|
+
const timeToPosition = useCallback((time) => {
|
|
102
|
+
if (duration <= 0 || contentWidth <= 0)
|
|
103
|
+
return handleWidth;
|
|
104
|
+
return handleWidth + (time / duration) * contentWidth;
|
|
105
|
+
}, [duration, contentWidth, handleWidth]);
|
|
106
|
+
// Calculate positions
|
|
107
|
+
const startPosition = timeToPosition(startTime);
|
|
108
|
+
const endPosition = timeToPosition(endTime);
|
|
109
|
+
const playheadPosition = timeToPosition(currentTime);
|
|
110
|
+
// Handle layout change
|
|
111
|
+
const handleLayout = useCallback((e) => {
|
|
112
|
+
setContainerWidth(e.nativeEvent.layout.width);
|
|
113
|
+
}, []);
|
|
114
|
+
// Store callbacks in refs to avoid recreating PanResponder on every render
|
|
115
|
+
const onStartTimeChangeRef = useRef(onStartTimeChange);
|
|
116
|
+
const onEndTimeChangeRef = useRef(onEndTimeChange);
|
|
117
|
+
const onScrubRef = useRef(onScrub);
|
|
118
|
+
const onScrubbingRef = useRef(onScrubbing);
|
|
119
|
+
const onScrubEndRef = useRef(onScrubEnd);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
onStartTimeChangeRef.current = onStartTimeChange;
|
|
122
|
+
onEndTimeChangeRef.current = onEndTimeChange;
|
|
123
|
+
onScrubRef.current = onScrub;
|
|
124
|
+
onScrubbingRef.current = onScrubbing;
|
|
125
|
+
onScrubEndRef.current = onScrubEnd;
|
|
126
|
+
}, [onStartTimeChange, onEndTimeChange, onScrub, onScrubbing, onScrubEnd]);
|
|
127
|
+
// Create pan responders for handles and playhead
|
|
128
|
+
// Using refs to avoid stale closures - PanResponder is only created once per type
|
|
129
|
+
const createPanResponder = useCallback((type) => {
|
|
130
|
+
let initialTime = 0;
|
|
131
|
+
return PanResponder.create({
|
|
132
|
+
onStartShouldSetPanResponder: () => !disabled,
|
|
133
|
+
onMoveShouldSetPanResponder: (_, gestureState) => {
|
|
134
|
+
// Claim the gesture if there's any horizontal movement
|
|
135
|
+
// This prevents ScrollView from capturing horizontal drags
|
|
136
|
+
return !disabled && Math.abs(gestureState.dx) > 2;
|
|
137
|
+
},
|
|
138
|
+
// Capture phase handlers - these fire before parent views
|
|
139
|
+
onStartShouldSetPanResponderCapture: () => false, // Don't capture on start, let it bubble
|
|
140
|
+
onMoveShouldSetPanResponderCapture: (_, gestureState) => {
|
|
141
|
+
// Only capture if horizontal movement is dominant
|
|
142
|
+
return !disabled && Math.abs(gestureState.dx) > 5 && Math.abs(gestureState.dx) > Math.abs(gestureState.dy);
|
|
143
|
+
},
|
|
144
|
+
onPanResponderGrant: () => {
|
|
145
|
+
// Read current values from refs at the moment the gesture starts
|
|
146
|
+
if (type === 'start') {
|
|
147
|
+
initialTime = dragStartTimeRef.current;
|
|
148
|
+
}
|
|
149
|
+
else if (type === 'end') {
|
|
150
|
+
initialTime = dragEndTimeRef.current;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
initialTime = currentTimeRef.current;
|
|
154
|
+
}
|
|
155
|
+
setDragging(type);
|
|
156
|
+
},
|
|
157
|
+
onPanResponderMove: (_e, gestureState) => {
|
|
158
|
+
const containerW = containerWidthRef.current;
|
|
159
|
+
const hw = handleWidthRef.current;
|
|
160
|
+
const dur = durationRef.current;
|
|
161
|
+
// Content width is where time is mapped (between handles)
|
|
162
|
+
const cw = containerW - hw * 2;
|
|
163
|
+
if (cw <= 0 || dur <= 0)
|
|
164
|
+
return;
|
|
165
|
+
const deltaX = gestureState.dx;
|
|
166
|
+
const deltaTime = (deltaX / cw) * dur;
|
|
167
|
+
let newTime = initialTime + deltaTime;
|
|
168
|
+
if (type === 'start') {
|
|
169
|
+
// Constrain start handle
|
|
170
|
+
newTime = Math.max(0, Math.min(newTime, dragEndTimeRef.current - minDuration));
|
|
171
|
+
dragStartTimeRef.current = newTime;
|
|
172
|
+
onStartTimeChangeRef.current?.(newTime);
|
|
173
|
+
onScrubbingRef.current?.(newTime);
|
|
174
|
+
}
|
|
175
|
+
else if (type === 'end') {
|
|
176
|
+
// Constrain end handle
|
|
177
|
+
newTime = Math.max(dragStartTimeRef.current + minDuration, Math.min(newTime, dur));
|
|
178
|
+
dragEndTimeRef.current = newTime;
|
|
179
|
+
onEndTimeChangeRef.current?.(newTime);
|
|
180
|
+
onScrubbingRef.current?.(newTime);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// Playhead - constrain to selection
|
|
184
|
+
newTime = Math.max(dragStartTimeRef.current, Math.min(newTime, dragEndTimeRef.current));
|
|
185
|
+
onScrubRef.current?.(newTime);
|
|
186
|
+
onScrubbingRef.current?.(newTime);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
onPanResponderRelease: (_e, gestureState) => {
|
|
190
|
+
setDragging(null);
|
|
191
|
+
if (type === 'start') {
|
|
192
|
+
onScrubEndRef.current?.(dragStartTimeRef.current);
|
|
193
|
+
}
|
|
194
|
+
else if (type === 'end') {
|
|
195
|
+
onScrubEndRef.current?.(dragEndTimeRef.current);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const containerW = containerWidthRef.current;
|
|
199
|
+
const hw = handleWidthRef.current;
|
|
200
|
+
const dur = durationRef.current;
|
|
201
|
+
const cw = containerW - hw * 2;
|
|
202
|
+
if (cw > 0 && dur > 0) {
|
|
203
|
+
const deltaTime = (gestureState.dx / cw) * dur;
|
|
204
|
+
const newTime = Math.max(dragStartTimeRef.current, Math.min(initialTime + deltaTime, dragEndTimeRef.current));
|
|
205
|
+
onScrubEndRef.current?.(newTime);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
onPanResponderTerminate: () => {
|
|
210
|
+
setDragging(null);
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}, [disabled, minDuration] // Only recreate if disabled or minDuration changes
|
|
214
|
+
);
|
|
215
|
+
const startPanResponder = useMemo(() => createPanResponder('start'), [createPanResponder]);
|
|
216
|
+
const endPanResponder = useMemo(() => createPanResponder('end'), [createPanResponder]);
|
|
217
|
+
const playheadPanResponder = useMemo(() => createPanResponder('playhead'), [createPanResponder]);
|
|
218
|
+
// Tap to seek - locationX is relative to the tap area which spans from startPosition to endPosition
|
|
219
|
+
const handleTap = useCallback((e) => {
|
|
220
|
+
if (disabled)
|
|
221
|
+
return;
|
|
222
|
+
const { locationX } = e.nativeEvent;
|
|
223
|
+
const startT = dragStartTimeRef.current;
|
|
224
|
+
const endT = dragEndTimeRef.current;
|
|
225
|
+
const selectionDuration = endT - startT;
|
|
226
|
+
if (selectionDuration <= 0)
|
|
227
|
+
return;
|
|
228
|
+
// The tap area spans the selection, so locationX=0 is startTime and locationX=width is endTime
|
|
229
|
+
// Calculate time proportionally within the selection
|
|
230
|
+
const width = containerWidthRef.current;
|
|
231
|
+
const dur = durationRef.current;
|
|
232
|
+
if (width <= 0 || dur <= 0)
|
|
233
|
+
return;
|
|
234
|
+
// Calculate the width of the tap area (endPosition - startPosition in content coordinates)
|
|
235
|
+
const cw = width - handleWidth * 2; // contentWidth
|
|
236
|
+
const tapAreaWidth = ((endT - startT) / dur) * cw;
|
|
237
|
+
if (tapAreaWidth <= 0)
|
|
238
|
+
return;
|
|
239
|
+
const proportion = Math.max(0, Math.min(1, locationX / tapAreaWidth));
|
|
240
|
+
const time = startT + proportion * selectionDuration;
|
|
241
|
+
onScrubRef.current?.(time);
|
|
242
|
+
}, [disabled, handleWidth]);
|
|
243
|
+
// Expose ref methods
|
|
244
|
+
useImperativeHandle(ref, () => ({
|
|
245
|
+
regenerateThumbnails: async () => {
|
|
246
|
+
if (!assetId || thumbnailTimes.length === 0)
|
|
247
|
+
return;
|
|
248
|
+
setIsLoading(true);
|
|
249
|
+
try {
|
|
250
|
+
const thumbs = await ExpoTwoStepVideoModule.generateThumbnails(assetId, thumbnailTimes, { width: thumbnailHeight * 1.5, height: thumbnailHeight });
|
|
251
|
+
setThumbnails(thumbs);
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
console.error('Failed to regenerate thumbnails:', err);
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
setIsLoading(false);
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
setPlayheadPosition: (time) => {
|
|
261
|
+
onScrub?.(Math.max(startTime, Math.min(time, endTime)));
|
|
262
|
+
},
|
|
263
|
+
getSelection: () => ({ startTime, endTime }),
|
|
264
|
+
}));
|
|
265
|
+
const thumbnailWidth = contentWidth > 0 && thumbnails.length > 0
|
|
266
|
+
? contentWidth / thumbnails.length
|
|
267
|
+
: 0;
|
|
268
|
+
return (<View style={[
|
|
269
|
+
styles.container,
|
|
270
|
+
{ backgroundColor: theme.backgroundColor },
|
|
271
|
+
style,
|
|
272
|
+
]} onLayout={handleLayout}>
|
|
273
|
+
{/* Thumbnail Strip - positioned between handles */}
|
|
274
|
+
<View style={[styles.thumbnailStrip, { height: thumbnailHeight, marginLeft: handleWidth, width: contentWidth }]}>
|
|
275
|
+
{thumbnails.map((thumb, index) => (<Image key={index} source={{ uri: `data:image/png;base64,${thumb}` }} style={[
|
|
276
|
+
styles.thumbnail,
|
|
277
|
+
{
|
|
278
|
+
width: thumbnailWidth,
|
|
279
|
+
height: thumbnailHeight,
|
|
280
|
+
},
|
|
281
|
+
]} resizeMode="cover"/>))}
|
|
282
|
+
{isLoading && thumbnails.length === 0 && (<View style={[styles.loadingPlaceholder, { height: thumbnailHeight }]}/>)}
|
|
283
|
+
</View>
|
|
284
|
+
|
|
285
|
+
{/* Dimmed region - left (covers trimmed content from left handle to start handle position) */}
|
|
286
|
+
<View style={[
|
|
287
|
+
styles.dimmedRegion,
|
|
288
|
+
{
|
|
289
|
+
left: handleWidth,
|
|
290
|
+
width: Math.max(0, startPosition - handleWidth),
|
|
291
|
+
height: thumbnailHeight,
|
|
292
|
+
backgroundColor: theme.dimmedColor,
|
|
293
|
+
},
|
|
294
|
+
]} pointerEvents="none"/>
|
|
295
|
+
|
|
296
|
+
{/* Dimmed region - right (covers trimmed content from end handle position to right handle) */}
|
|
297
|
+
<View style={[
|
|
298
|
+
styles.dimmedRegion,
|
|
299
|
+
{
|
|
300
|
+
left: endPosition,
|
|
301
|
+
width: Math.max(0, containerWidth - handleWidth - endPosition),
|
|
302
|
+
height: thumbnailHeight,
|
|
303
|
+
backgroundColor: theme.dimmedColor,
|
|
304
|
+
},
|
|
305
|
+
]} pointerEvents="none"/>
|
|
306
|
+
|
|
307
|
+
{/* Selection frame - top and bottom borders (span between handles) */}
|
|
308
|
+
<View style={[
|
|
309
|
+
styles.selectionBorder,
|
|
310
|
+
styles.selectionBorderTop,
|
|
311
|
+
{
|
|
312
|
+
left: startPosition,
|
|
313
|
+
width: Math.max(0, endPosition - startPosition),
|
|
314
|
+
backgroundColor: theme.borderColor,
|
|
315
|
+
},
|
|
316
|
+
]} pointerEvents="none"/>
|
|
317
|
+
<View style={[
|
|
318
|
+
styles.selectionBorder,
|
|
319
|
+
styles.selectionBorderBottom,
|
|
320
|
+
{
|
|
321
|
+
left: startPosition,
|
|
322
|
+
width: Math.max(0, endPosition - startPosition),
|
|
323
|
+
top: thumbnailHeight - 2,
|
|
324
|
+
backgroundColor: theme.borderColor,
|
|
325
|
+
},
|
|
326
|
+
]} pointerEvents="none"/>
|
|
327
|
+
|
|
328
|
+
{/* Start Handle - positioned to the LEFT of the trim point (outside content when startTime=0) */}
|
|
329
|
+
<Animated.View style={[
|
|
330
|
+
styles.handle,
|
|
331
|
+
styles.startHandle,
|
|
332
|
+
{
|
|
333
|
+
left: startPosition - handleWidth,
|
|
334
|
+
width: handleWidth,
|
|
335
|
+
height: thumbnailHeight,
|
|
336
|
+
backgroundColor: theme.handleColor,
|
|
337
|
+
opacity: dragging === 'start' ? 0.8 : 1,
|
|
338
|
+
},
|
|
339
|
+
]} hitSlop={{ top: 15, bottom: 15, left: 15, right: 10 }} {...startPanResponder.panHandlers}>
|
|
340
|
+
<View style={styles.handleChevron}>
|
|
341
|
+
<View style={[styles.chevronLine, styles.chevronLineTopLeft]}/>
|
|
342
|
+
<View style={[styles.chevronLine, styles.chevronLineBottomLeft]}/>
|
|
343
|
+
</View>
|
|
344
|
+
</Animated.View>
|
|
345
|
+
|
|
346
|
+
{/* End Handle - positioned to the RIGHT of the trim point (outside content when endTime=duration) */}
|
|
347
|
+
<Animated.View style={[
|
|
348
|
+
styles.handle,
|
|
349
|
+
styles.endHandle,
|
|
350
|
+
{
|
|
351
|
+
left: endPosition,
|
|
352
|
+
width: handleWidth,
|
|
353
|
+
height: thumbnailHeight,
|
|
354
|
+
backgroundColor: theme.handleColor,
|
|
355
|
+
opacity: dragging === 'end' ? 0.8 : 1,
|
|
356
|
+
},
|
|
357
|
+
]} hitSlop={{ top: 15, bottom: 15, left: 10, right: 15 }} {...endPanResponder.panHandlers}>
|
|
358
|
+
<View style={styles.handleChevron}>
|
|
359
|
+
<View style={[styles.chevronLine, styles.chevronLineTopRight]}/>
|
|
360
|
+
<View style={[styles.chevronLine, styles.chevronLineBottomRight]}/>
|
|
361
|
+
</View>
|
|
362
|
+
</Animated.View>
|
|
363
|
+
|
|
364
|
+
{/* Tap area for seeking - covers the selection between start and end trim points */}
|
|
365
|
+
<View style={[
|
|
366
|
+
styles.tapArea,
|
|
367
|
+
{
|
|
368
|
+
left: startPosition,
|
|
369
|
+
width: Math.max(0, endPosition - startPosition),
|
|
370
|
+
height: thumbnailHeight,
|
|
371
|
+
},
|
|
372
|
+
]} onTouchEnd={handleTap}/>
|
|
373
|
+
|
|
374
|
+
{/* Playhead */}
|
|
375
|
+
{showPlayhead && playheadPosition >= startPosition && playheadPosition <= endPosition && (<Animated.View style={[
|
|
376
|
+
styles.playhead,
|
|
377
|
+
{
|
|
378
|
+
left: playheadPosition - 1,
|
|
379
|
+
height: thumbnailHeight + 8,
|
|
380
|
+
top: -4,
|
|
381
|
+
backgroundColor: theme.playheadColor,
|
|
382
|
+
opacity: dragging === 'playhead' ? 0.8 : 1,
|
|
383
|
+
},
|
|
384
|
+
]} {...playheadPanResponder.panHandlers}>
|
|
385
|
+
<View style={[styles.playheadKnob, { backgroundColor: theme.playheadColor }]}/>
|
|
386
|
+
</Animated.View>)}
|
|
387
|
+
</View>);
|
|
388
|
+
});
|
|
389
|
+
VideoScrubber.displayName = 'VideoScrubber';
|
|
390
|
+
const styles = StyleSheet.create({
|
|
391
|
+
container: {
|
|
392
|
+
width: '100%',
|
|
393
|
+
borderRadius: 8,
|
|
394
|
+
overflow: 'hidden',
|
|
395
|
+
position: 'relative',
|
|
396
|
+
},
|
|
397
|
+
thumbnailStrip: {
|
|
398
|
+
flexDirection: 'row',
|
|
399
|
+
width: '100%',
|
|
400
|
+
},
|
|
401
|
+
thumbnail: {
|
|
402
|
+
backgroundColor: '#2c2c2e',
|
|
403
|
+
},
|
|
404
|
+
loadingPlaceholder: {
|
|
405
|
+
flex: 1,
|
|
406
|
+
backgroundColor: '#2c2c2e',
|
|
407
|
+
},
|
|
408
|
+
dimmedRegion: {
|
|
409
|
+
position: 'absolute',
|
|
410
|
+
top: 0,
|
|
411
|
+
},
|
|
412
|
+
selectionBorder: {
|
|
413
|
+
position: 'absolute',
|
|
414
|
+
height: 2,
|
|
415
|
+
},
|
|
416
|
+
selectionBorderTop: {
|
|
417
|
+
top: 0,
|
|
418
|
+
},
|
|
419
|
+
selectionBorderBottom: {
|
|
420
|
+
bottom: 0,
|
|
421
|
+
},
|
|
422
|
+
handle: {
|
|
423
|
+
position: 'absolute',
|
|
424
|
+
top: 0,
|
|
425
|
+
justifyContent: 'center',
|
|
426
|
+
alignItems: 'center',
|
|
427
|
+
borderRadius: 4,
|
|
428
|
+
},
|
|
429
|
+
startHandle: {
|
|
430
|
+
borderTopLeftRadius: 6,
|
|
431
|
+
borderBottomLeftRadius: 6,
|
|
432
|
+
borderTopRightRadius: 0,
|
|
433
|
+
borderBottomRightRadius: 0,
|
|
434
|
+
},
|
|
435
|
+
endHandle: {
|
|
436
|
+
borderTopLeftRadius: 0,
|
|
437
|
+
borderBottomLeftRadius: 0,
|
|
438
|
+
borderTopRightRadius: 6,
|
|
439
|
+
borderBottomRightRadius: 6,
|
|
440
|
+
},
|
|
441
|
+
handleChevron: {
|
|
442
|
+
width: 8,
|
|
443
|
+
height: 16,
|
|
444
|
+
justifyContent: 'center',
|
|
445
|
+
alignItems: 'center',
|
|
446
|
+
},
|
|
447
|
+
chevronLine: {
|
|
448
|
+
width: 2,
|
|
449
|
+
height: 8,
|
|
450
|
+
backgroundColor: '#000',
|
|
451
|
+
borderRadius: 1,
|
|
452
|
+
position: 'absolute',
|
|
453
|
+
},
|
|
454
|
+
chevronLineTopLeft: {
|
|
455
|
+
transform: [{ rotate: '20deg' }],
|
|
456
|
+
top: 0,
|
|
457
|
+
},
|
|
458
|
+
chevronLineBottomLeft: {
|
|
459
|
+
transform: [{ rotate: '-20deg' }],
|
|
460
|
+
bottom: 0,
|
|
461
|
+
},
|
|
462
|
+
chevronLineTopRight: {
|
|
463
|
+
transform: [{ rotate: '-20deg' }],
|
|
464
|
+
top: 0,
|
|
465
|
+
},
|
|
466
|
+
chevronLineBottomRight: {
|
|
467
|
+
transform: [{ rotate: '20deg' }],
|
|
468
|
+
bottom: 0,
|
|
469
|
+
},
|
|
470
|
+
tapArea: {
|
|
471
|
+
position: 'absolute',
|
|
472
|
+
top: 0,
|
|
473
|
+
},
|
|
474
|
+
playhead: {
|
|
475
|
+
position: 'absolute',
|
|
476
|
+
width: 2,
|
|
477
|
+
borderRadius: 1,
|
|
478
|
+
},
|
|
479
|
+
playheadKnob: {
|
|
480
|
+
position: 'absolute',
|
|
481
|
+
top: -4,
|
|
482
|
+
left: -4,
|
|
483
|
+
width: 10,
|
|
484
|
+
height: 10,
|
|
485
|
+
borderRadius: 5,
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
export default VideoScrubber;
|
|
489
|
+
//# sourceMappingURL=VideoScrubber.js.map
|