@movementinfra/expo-twostep-video 0.1.7 → 0.1.9

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,6FA0bjB,CAAC;AAuGH,eAAe,aAAa,CAAC"}
@@ -0,0 +1,467 @@
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
+ // Sync refs with props when they change from outside (e.g., new video loaded)
44
+ useEffect(() => {
45
+ if (!dragging) {
46
+ dragStartTimeRef.current = startTime;
47
+ dragEndTimeRef.current = endTime;
48
+ }
49
+ }, [startTime, endTime, dragging]);
50
+ // Keep other refs in sync
51
+ useEffect(() => {
52
+ currentTimeRef.current = currentTime;
53
+ }, [currentTime]);
54
+ useEffect(() => {
55
+ durationRef.current = duration;
56
+ }, [duration]);
57
+ useEffect(() => {
58
+ containerWidthRef.current = containerWidth;
59
+ }, [containerWidth]);
60
+ // Calculate thumbnail times
61
+ const thumbnailTimes = useMemo(() => {
62
+ if (duration <= 0)
63
+ return [];
64
+ const count = Math.min(thumbnailCount, Math.max(1, Math.ceil(duration)));
65
+ const interval = duration / count;
66
+ return Array.from({ length: count }, (_, i) => i * interval + interval / 2);
67
+ }, [duration, thumbnailCount]);
68
+ // Load thumbnails
69
+ useEffect(() => {
70
+ if (!assetId || thumbnailTimes.length === 0 || duration <= 0) {
71
+ setThumbnails([]);
72
+ setIsLoading(false);
73
+ return;
74
+ }
75
+ let cancelled = false;
76
+ setIsLoading(true);
77
+ ExpoTwoStepVideoModule.generateThumbnails(assetId, thumbnailTimes, { width: thumbnailHeight * 1.5, height: thumbnailHeight })
78
+ .then((thumbs) => {
79
+ if (!cancelled) {
80
+ setThumbnails(thumbs);
81
+ setIsLoading(false);
82
+ }
83
+ })
84
+ .catch((err) => {
85
+ if (!cancelled) {
86
+ console.error('Failed to generate thumbnails:', err);
87
+ setIsLoading(false);
88
+ }
89
+ });
90
+ return () => {
91
+ cancelled = true;
92
+ };
93
+ }, [assetId, thumbnailTimes, thumbnailHeight, duration]);
94
+ // Convert time to position and vice versa
95
+ const timeToPosition = useCallback((time) => {
96
+ if (duration <= 0 || containerWidth <= 0)
97
+ return 0;
98
+ return (time / duration) * containerWidth;
99
+ }, [duration, containerWidth]);
100
+ // Calculate positions
101
+ const startPosition = timeToPosition(startTime);
102
+ const endPosition = timeToPosition(endTime);
103
+ const playheadPosition = timeToPosition(currentTime);
104
+ // Handle layout change
105
+ const handleLayout = useCallback((e) => {
106
+ setContainerWidth(e.nativeEvent.layout.width);
107
+ }, []);
108
+ // Store callbacks in refs to avoid recreating PanResponder on every render
109
+ const onStartTimeChangeRef = useRef(onStartTimeChange);
110
+ const onEndTimeChangeRef = useRef(onEndTimeChange);
111
+ const onScrubRef = useRef(onScrub);
112
+ const onScrubbingRef = useRef(onScrubbing);
113
+ const onScrubEndRef = useRef(onScrubEnd);
114
+ useEffect(() => {
115
+ onStartTimeChangeRef.current = onStartTimeChange;
116
+ onEndTimeChangeRef.current = onEndTimeChange;
117
+ onScrubRef.current = onScrub;
118
+ onScrubbingRef.current = onScrubbing;
119
+ onScrubEndRef.current = onScrubEnd;
120
+ }, [onStartTimeChange, onEndTimeChange, onScrub, onScrubbing, onScrubEnd]);
121
+ // Create pan responders for handles and playhead
122
+ // Using refs to avoid stale closures - PanResponder is only created once per type
123
+ const createPanResponder = useCallback((type) => {
124
+ let initialTime = 0;
125
+ return PanResponder.create({
126
+ onStartShouldSetPanResponder: () => !disabled,
127
+ onMoveShouldSetPanResponder: (_, gestureState) => {
128
+ // Claim the gesture if there's any horizontal movement
129
+ // This prevents ScrollView from capturing horizontal drags
130
+ return !disabled && Math.abs(gestureState.dx) > 2;
131
+ },
132
+ // Capture phase handlers - these fire before parent views
133
+ onStartShouldSetPanResponderCapture: () => false, // Don't capture on start, let it bubble
134
+ onMoveShouldSetPanResponderCapture: (_, gestureState) => {
135
+ // Only capture if horizontal movement is dominant
136
+ return !disabled && Math.abs(gestureState.dx) > 5 && Math.abs(gestureState.dx) > Math.abs(gestureState.dy);
137
+ },
138
+ onPanResponderGrant: () => {
139
+ // Read current values from refs at the moment the gesture starts
140
+ if (type === 'start') {
141
+ initialTime = dragStartTimeRef.current;
142
+ }
143
+ else if (type === 'end') {
144
+ initialTime = dragEndTimeRef.current;
145
+ }
146
+ else {
147
+ initialTime = currentTimeRef.current;
148
+ }
149
+ setDragging(type);
150
+ },
151
+ onPanResponderMove: (_e, gestureState) => {
152
+ const width = containerWidthRef.current;
153
+ const dur = durationRef.current;
154
+ if (width <= 0 || dur <= 0)
155
+ return;
156
+ const deltaX = gestureState.dx;
157
+ const deltaTime = (deltaX / width) * dur;
158
+ let newTime = initialTime + deltaTime;
159
+ if (type === 'start') {
160
+ // Constrain start handle
161
+ newTime = Math.max(0, Math.min(newTime, dragEndTimeRef.current - minDuration));
162
+ dragStartTimeRef.current = newTime;
163
+ onStartTimeChangeRef.current?.(newTime);
164
+ onScrubbingRef.current?.(newTime);
165
+ }
166
+ else if (type === 'end') {
167
+ // Constrain end handle
168
+ newTime = Math.max(dragStartTimeRef.current + minDuration, Math.min(newTime, dur));
169
+ dragEndTimeRef.current = newTime;
170
+ onEndTimeChangeRef.current?.(newTime);
171
+ onScrubbingRef.current?.(newTime);
172
+ }
173
+ else {
174
+ // Playhead - constrain to selection
175
+ newTime = Math.max(dragStartTimeRef.current, Math.min(newTime, dragEndTimeRef.current));
176
+ onScrubRef.current?.(newTime);
177
+ onScrubbingRef.current?.(newTime);
178
+ }
179
+ },
180
+ onPanResponderRelease: (_e, gestureState) => {
181
+ setDragging(null);
182
+ if (type === 'start') {
183
+ onScrubEndRef.current?.(dragStartTimeRef.current);
184
+ }
185
+ else if (type === 'end') {
186
+ onScrubEndRef.current?.(dragEndTimeRef.current);
187
+ }
188
+ else {
189
+ const width = containerWidthRef.current;
190
+ const dur = durationRef.current;
191
+ if (width > 0 && dur > 0) {
192
+ const deltaTime = (gestureState.dx / width) * dur;
193
+ const newTime = Math.max(dragStartTimeRef.current, Math.min(initialTime + deltaTime, dragEndTimeRef.current));
194
+ onScrubEndRef.current?.(newTime);
195
+ }
196
+ }
197
+ },
198
+ onPanResponderTerminate: () => {
199
+ setDragging(null);
200
+ },
201
+ });
202
+ }, [disabled, minDuration] // Only recreate if disabled or minDuration changes
203
+ );
204
+ const startPanResponder = useMemo(() => createPanResponder('start'), [createPanResponder]);
205
+ const endPanResponder = useMemo(() => createPanResponder('end'), [createPanResponder]);
206
+ const playheadPanResponder = useMemo(() => createPanResponder('playhead'), [createPanResponder]);
207
+ // Tap to seek
208
+ const handleTap = useCallback((e) => {
209
+ if (disabled)
210
+ return;
211
+ const { locationX } = e.nativeEvent;
212
+ const width = containerWidthRef.current;
213
+ const dur = durationRef.current;
214
+ if (width <= 0 || dur <= 0)
215
+ return;
216
+ const time = Math.max(0, Math.min(dur, (locationX / width) * dur));
217
+ // Only seek within selection
218
+ const clampedTime = Math.max(dragStartTimeRef.current, Math.min(time, dragEndTimeRef.current));
219
+ onScrubRef.current?.(clampedTime);
220
+ }, [disabled]);
221
+ // Expose ref methods
222
+ useImperativeHandle(ref, () => ({
223
+ regenerateThumbnails: async () => {
224
+ if (!assetId || thumbnailTimes.length === 0)
225
+ return;
226
+ setIsLoading(true);
227
+ try {
228
+ const thumbs = await ExpoTwoStepVideoModule.generateThumbnails(assetId, thumbnailTimes, { width: thumbnailHeight * 1.5, height: thumbnailHeight });
229
+ setThumbnails(thumbs);
230
+ }
231
+ catch (err) {
232
+ console.error('Failed to regenerate thumbnails:', err);
233
+ }
234
+ finally {
235
+ setIsLoading(false);
236
+ }
237
+ },
238
+ setPlayheadPosition: (time) => {
239
+ onScrub?.(Math.max(startTime, Math.min(time, endTime)));
240
+ },
241
+ getSelection: () => ({ startTime, endTime }),
242
+ }));
243
+ const thumbnailWidth = containerWidth > 0 && thumbnails.length > 0
244
+ ? containerWidth / thumbnails.length
245
+ : 0;
246
+ return (<View style={[
247
+ styles.container,
248
+ { backgroundColor: theme.backgroundColor },
249
+ style,
250
+ ]} onLayout={handleLayout}>
251
+ {/* Thumbnail Strip */}
252
+ <View style={[styles.thumbnailStrip, { height: thumbnailHeight }]}>
253
+ {thumbnails.map((thumb, index) => (<Image key={index} source={{ uri: `data:image/png;base64,${thumb}` }} style={[
254
+ styles.thumbnail,
255
+ {
256
+ width: thumbnailWidth,
257
+ height: thumbnailHeight,
258
+ },
259
+ ]} resizeMode="cover"/>))}
260
+ {isLoading && thumbnails.length === 0 && (<View style={[styles.loadingPlaceholder, { height: thumbnailHeight }]}/>)}
261
+ </View>
262
+
263
+ {/* Dimmed region - left */}
264
+ <View style={[
265
+ styles.dimmedRegion,
266
+ {
267
+ left: 0,
268
+ width: startPosition,
269
+ height: thumbnailHeight,
270
+ backgroundColor: theme.dimmedColor,
271
+ },
272
+ ]} pointerEvents="none"/>
273
+
274
+ {/* Dimmed region - right */}
275
+ <View style={[
276
+ styles.dimmedRegion,
277
+ {
278
+ left: endPosition,
279
+ right: 0,
280
+ height: thumbnailHeight,
281
+ backgroundColor: theme.dimmedColor,
282
+ },
283
+ ]} pointerEvents="none"/>
284
+
285
+ {/* Selection frame - top and bottom borders */}
286
+ <View style={[
287
+ styles.selectionBorder,
288
+ styles.selectionBorderTop,
289
+ {
290
+ left: startPosition + handleWidth,
291
+ width: Math.max(0, endPosition - startPosition - handleWidth * 2),
292
+ backgroundColor: theme.borderColor,
293
+ },
294
+ ]} pointerEvents="none"/>
295
+ <View style={[
296
+ styles.selectionBorder,
297
+ styles.selectionBorderBottom,
298
+ {
299
+ left: startPosition + handleWidth,
300
+ width: Math.max(0, endPosition - startPosition - handleWidth * 2),
301
+ top: thumbnailHeight - 2,
302
+ backgroundColor: theme.borderColor,
303
+ },
304
+ ]} pointerEvents="none"/>
305
+
306
+ {/* Start Handle */}
307
+ <Animated.View style={[
308
+ styles.handle,
309
+ styles.startHandle,
310
+ {
311
+ left: startPosition,
312
+ width: handleWidth,
313
+ height: thumbnailHeight,
314
+ backgroundColor: theme.handleColor,
315
+ opacity: dragging === 'start' ? 0.8 : 1,
316
+ },
317
+ ]} hitSlop={{ top: 15, bottom: 15, left: 15, right: 10 }} {...startPanResponder.panHandlers}>
318
+ <View style={styles.handleChevron}>
319
+ <View style={[styles.chevronLine, styles.chevronLineTopLeft]}/>
320
+ <View style={[styles.chevronLine, styles.chevronLineBottomLeft]}/>
321
+ </View>
322
+ </Animated.View>
323
+
324
+ {/* End Handle */}
325
+ <Animated.View style={[
326
+ styles.handle,
327
+ styles.endHandle,
328
+ {
329
+ left: endPosition - handleWidth,
330
+ width: handleWidth,
331
+ height: thumbnailHeight,
332
+ backgroundColor: theme.handleColor,
333
+ opacity: dragging === 'end' ? 0.8 : 1,
334
+ },
335
+ ]} hitSlop={{ top: 15, bottom: 15, left: 10, right: 15 }} {...endPanResponder.panHandlers}>
336
+ <View style={styles.handleChevron}>
337
+ <View style={[styles.chevronLine, styles.chevronLineTopRight]}/>
338
+ <View style={[styles.chevronLine, styles.chevronLineBottomRight]}/>
339
+ </View>
340
+ </Animated.View>
341
+
342
+ {/* Tap area for seeking */}
343
+ <View style={[
344
+ styles.tapArea,
345
+ {
346
+ left: startPosition + handleWidth,
347
+ width: Math.max(0, endPosition - startPosition - handleWidth * 2),
348
+ height: thumbnailHeight,
349
+ },
350
+ ]} onTouchEnd={handleTap}/>
351
+
352
+ {/* Playhead */}
353
+ {showPlayhead && playheadPosition >= startPosition && playheadPosition <= endPosition && (<Animated.View style={[
354
+ styles.playhead,
355
+ {
356
+ left: playheadPosition - 1,
357
+ height: thumbnailHeight + 8,
358
+ top: -4,
359
+ backgroundColor: theme.playheadColor,
360
+ opacity: dragging === 'playhead' ? 0.8 : 1,
361
+ },
362
+ ]} {...playheadPanResponder.panHandlers}>
363
+ <View style={[styles.playheadKnob, { backgroundColor: theme.playheadColor }]}/>
364
+ </Animated.View>)}
365
+ </View>);
366
+ });
367
+ VideoScrubber.displayName = 'VideoScrubber';
368
+ const styles = StyleSheet.create({
369
+ container: {
370
+ width: '100%',
371
+ borderRadius: 8,
372
+ overflow: 'hidden',
373
+ position: 'relative',
374
+ },
375
+ thumbnailStrip: {
376
+ flexDirection: 'row',
377
+ width: '100%',
378
+ },
379
+ thumbnail: {
380
+ backgroundColor: '#2c2c2e',
381
+ },
382
+ loadingPlaceholder: {
383
+ flex: 1,
384
+ backgroundColor: '#2c2c2e',
385
+ },
386
+ dimmedRegion: {
387
+ position: 'absolute',
388
+ top: 0,
389
+ },
390
+ selectionBorder: {
391
+ position: 'absolute',
392
+ height: 2,
393
+ },
394
+ selectionBorderTop: {
395
+ top: 0,
396
+ },
397
+ selectionBorderBottom: {
398
+ bottom: 0,
399
+ },
400
+ handle: {
401
+ position: 'absolute',
402
+ top: 0,
403
+ justifyContent: 'center',
404
+ alignItems: 'center',
405
+ borderRadius: 4,
406
+ },
407
+ startHandle: {
408
+ borderTopLeftRadius: 6,
409
+ borderBottomLeftRadius: 6,
410
+ borderTopRightRadius: 0,
411
+ borderBottomRightRadius: 0,
412
+ },
413
+ endHandle: {
414
+ borderTopLeftRadius: 0,
415
+ borderBottomLeftRadius: 0,
416
+ borderTopRightRadius: 6,
417
+ borderBottomRightRadius: 6,
418
+ },
419
+ handleChevron: {
420
+ width: 8,
421
+ height: 16,
422
+ justifyContent: 'center',
423
+ alignItems: 'center',
424
+ },
425
+ chevronLine: {
426
+ width: 2,
427
+ height: 8,
428
+ backgroundColor: '#000',
429
+ borderRadius: 1,
430
+ position: 'absolute',
431
+ },
432
+ chevronLineTopLeft: {
433
+ transform: [{ rotate: '20deg' }],
434
+ top: 0,
435
+ },
436
+ chevronLineBottomLeft: {
437
+ transform: [{ rotate: '-20deg' }],
438
+ bottom: 0,
439
+ },
440
+ chevronLineTopRight: {
441
+ transform: [{ rotate: '-20deg' }],
442
+ top: 0,
443
+ },
444
+ chevronLineBottomRight: {
445
+ transform: [{ rotate: '20deg' }],
446
+ bottom: 0,
447
+ },
448
+ tapArea: {
449
+ position: 'absolute',
450
+ top: 0,
451
+ },
452
+ playhead: {
453
+ position: 'absolute',
454
+ width: 2,
455
+ borderRadius: 1,
456
+ },
457
+ playheadKnob: {
458
+ position: 'absolute',
459
+ top: -4,
460
+ left: -4,
461
+ width: 10,
462
+ height: 10,
463
+ borderRadius: 5,
464
+ },
465
+ });
466
+ export default VideoScrubber;
467
+ //# sourceMappingURL=VideoScrubber.js.map