@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.
@@ -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