@movementinfra/expo-twostep-video 0.1.14 → 0.1.16
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/README.md +80 -7
- package/build/ExpoTwoStepVideo.types.d.ts +55 -0
- package/build/ExpoTwoStepVideo.types.d.ts.map +1 -1
- package/build/ExpoTwoStepVideo.types.js.map +1 -1
- package/build/ExpoTwoStepVideoModule.d.ts +17 -0
- package/build/ExpoTwoStepVideoModule.d.ts.map +1 -1
- package/build/ExpoTwoStepVideoModule.js.map +1 -1
- package/build/ExpoTwoStepVideoModule.web.d.ts +4 -0
- package/build/ExpoTwoStepVideoModule.web.d.ts.map +1 -1
- package/build/ExpoTwoStepVideoModule.web.js +15 -0
- package/build/ExpoTwoStepVideoModule.web.js.map +1 -1
- package/build/ExpoTwoStepVideoView.d.ts.map +1 -1
- package/build/ExpoTwoStepVideoView.js +112 -2
- package/build/ExpoTwoStepVideoView.js.map +1 -1
- package/build/components/DoubleTapSkip.d.ts +9 -0
- package/build/components/DoubleTapSkip.d.ts.map +1 -0
- package/build/components/DoubleTapSkip.js +139 -0
- package/build/components/DoubleTapSkip.js.map +1 -0
- package/build/components/PlayheadBar.d.ts +10 -0
- package/build/components/PlayheadBar.d.ts.map +1 -0
- package/build/components/PlayheadBar.js +156 -0
- package/build/components/PlayheadBar.js.map +1 -0
- package/build/index.d.ts +83 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +91 -0
- package/build/index.js.map +1 -1
- package/ios/ExpoTwoStepVideoModule.swift +77 -1
- package/ios/ExpoTwoStepVideoView.swift +155 -36
- package/ios/TwoStepVideo/Core/MediaPicker.swift +355 -0
- package/ios/TwoStepVideo/TwoStepVideo.swift +4 -0
- package/package.json +1 -1
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * TwoStepVideo - Professional video editing for React Native\n * Built on native AVFoundation with Expo Modules\n */\n\nimport { type EventSubscription } from 'expo-modules-core';\nimport { useState, useEffect } from 'react';\nimport ExpoTwoStepVideoModule from './ExpoTwoStepVideoModule';\nimport type { PanZoomVideoOptions } from './ExpoTwoStepVideo.types';\n\n// MARK: - Types\n\n/**\n * Quality presets for video export\n */\nexport type VideoQuality = 'low' | 'medium' | 'high' | 'highest';\n\n/**\n * Mirror axis options for video mirroring\n */\nexport type MirrorAxis = 'horizontal' | 'vertical' | 'both';\n\n/**\n * Video asset metadata returned after loading\n */\nexport interface VideoAsset {\n /** Unique identifier for this asset */\n id: string;\n /** Duration in seconds */\n duration: number;\n /** Video width in pixels */\n width: number;\n /** Video height in pixels */\n height: number;\n /** Frame rate (fps) */\n frameRate: number;\n /** Whether the video has audio */\n hasAudio: boolean;\n}\n\n/**\n * Video composition metadata\n */\nexport interface VideoComposition {\n /** Unique identifier for this composition */\n id: string;\n /** Total duration in seconds */\n duration: number;\n}\n\n/**\n * Time segment for multi-segment trimming\n */\nexport interface TimeSegment {\n /** Start time in seconds */\n start: number;\n /** End time in seconds */\n end: number;\n}\n\n/**\n * Export result containing output file information\n */\nexport interface ExportResult {\n /** File URI (e.g., \"file:///path/to/video.mp4\") */\n uri: string;\n /** Absolute file path */\n path: string;\n}\n\n/**\n * Export progress event\n */\nexport interface ExportProgressEvent {\n /** Progress from 0.0 to 1.0 */\n progress: number;\n /** Asset ID if exporting asset directly */\n assetId?: string;\n /** Composition ID if exporting composition */\n compositionId?: string;\n}\n\n/**\n * Options for loading a video asset\n */\nexport interface LoadAssetOptions {\n /** File URI (e.g., \"file:///path/to/video.mp4\") */\n uri: string;\n}\n\n/**\n * Options for trimming a video\n */\nexport interface TrimVideoOptions {\n /** Asset ID to trim */\n assetId: string;\n /** Start time in seconds */\n startTime: number;\n /** End time in seconds */\n endTime: number;\n}\n\n/**\n * Options for multi-segment trimming\n */\nexport interface TrimMultipleOptions {\n /** Asset ID to trim */\n assetId: string;\n /** Array of time segments to extract and concatenate */\n segments: TimeSegment[];\n}\n\n/**\n * Options for generating thumbnails\n */\nexport interface GenerateThumbnailsOptions {\n /** Asset ID to generate thumbnails from */\n assetId: string;\n /** Array of times (in seconds) to extract thumbnails */\n times: number[];\n /** Optional size for thumbnails */\n size?: {\n width: number;\n height: number;\n };\n}\n\n/**\n * Options for exporting a composition\n */\nexport interface ExportCompositionOptions {\n /** Composition ID to export */\n compositionId: string;\n /** Optional output URI (uses temp directory if not provided) */\n outputUri?: string;\n /** Quality preset (default: 'high') */\n quality?: VideoQuality;\n}\n\n/**\n * Options for exporting an asset directly\n */\nexport interface ExportAssetOptions {\n /** Asset ID to export */\n assetId: string;\n /** Optional output URI (uses temp directory if not provided) */\n outputUri?: string;\n /** Quality preset (default: 'high') */\n quality?: VideoQuality;\n}\n\n/**\n * Options for mirroring a video\n */\nexport interface MirrorVideoOptions {\n /** Asset ID to mirror */\n assetId: string;\n /** Axis to mirror on: 'horizontal', 'vertical', or 'both' */\n axis: MirrorAxis;\n /** Optional start time in seconds (for segment mirroring) */\n startTime?: number;\n /** Optional end time in seconds (for segment mirroring) */\n endTime?: number;\n}\n\n/**\n * Options for adjusting video speed\n */\nexport interface AdjustSpeedOptions {\n /** Asset ID to adjust */\n assetId: string;\n /** Speed multiplier (0.25 = 4x slower, 2.0 = 2x faster) */\n speed: number;\n /** Optional start time in seconds */\n startTime?: number;\n /** Optional end time in seconds */\n endTime?: number;\n}\n\n/**\n * Options for combined video transformation\n */\nexport interface TransformVideoOptions {\n /** Asset ID to transform */\n assetId: string;\n /** Speed multiplier (default: 1.0) */\n speed?: number;\n /** Optional mirror axis */\n mirrorAxis?: MirrorAxis;\n /** Optional start time in seconds (for trimming) */\n startTime?: number;\n /** Optional end time in seconds (for trimming) */\n endTime?: number;\n}\n\n/**\n * Options for looping a video segment\n */\nexport interface LoopSegmentOptions {\n /** Asset ID to loop */\n assetId: string;\n /** Start time of the segment to loop (in seconds) */\n startTime: number;\n /** End time of the segment to loop (in seconds) */\n endTime: number;\n /** Number of times to repeat (total plays = loopCount + 1) */\n loopCount: number;\n}\n\n/**\n * Result from looping a video segment\n */\nexport interface LoopResult {\n /** Unique identifier for this composition */\n id: string;\n /** Total duration in seconds */\n duration: number;\n /** Number of times the segment repeats */\n loopCount: number;\n /** Total number of times the segment plays */\n totalPlays: number;\n}\n\n// MARK: - Constants\n\n/**\n * Quality constants\n */\nexport const Quality = {\n LOW: ExpoTwoStepVideoModule.QUALITY_LOW,\n MEDIUM: ExpoTwoStepVideoModule.QUALITY_MEDIUM,\n HIGH: ExpoTwoStepVideoModule.QUALITY_HIGH,\n HIGHEST: ExpoTwoStepVideoModule.QUALITY_HIGHEST,\n} as const;\n\n/**\n * Mirror axis constants\n */\nexport const Mirror = {\n HORIZONTAL: ExpoTwoStepVideoModule.MIRROR_HORIZONTAL,\n VERTICAL: ExpoTwoStepVideoModule.MIRROR_VERTICAL,\n BOTH: ExpoTwoStepVideoModule.MIRROR_BOTH,\n} as const;\n\n// MARK: - API Functions\n\n/**\n * Load a video asset from a file URI\n *\n * @param options - Load options containing the file URI\n * @returns Promise resolving to video asset metadata\n *\n * @example\n * ```typescript\n * const asset = await TwoStepVideo.loadAsset({\n * uri: 'file:///path/to/video.mp4'\n * });\n * console.log(`Duration: ${asset.duration}s, Size: ${asset.width}x${asset.height}`);\n * ```\n */\nexport async function loadAsset(options: LoadAssetOptions): Promise<VideoAsset> {\n return await ExpoTwoStepVideoModule.loadAsset(options.uri);\n}\n\n/**\n * Load a video asset from the Photos library\n *\n * @param localIdentifier - PHAsset local identifier from the Photos library\n * @returns Promise resolving to video asset metadata\n *\n * @example\n * ```typescript\n * // After using expo-media-library to get a photo asset\n * const asset = await TwoStepVideo.loadAssetFromPhotos(photoAsset.id);\n * ```\n */\nexport async function loadAssetFromPhotos(localIdentifier: string): Promise<VideoAsset> {\n return await ExpoTwoStepVideoModule.loadAssetFromPhotos(localIdentifier);\n}\n\n/**\n * Validate a video file URI without loading the full asset\n * Useful for quick validation before processing\n *\n * @param uri - File URI to validate\n * @returns Promise resolving to true if valid video file\n *\n * @example\n * ```typescript\n * const isValid = await TwoStepVideo.validateVideoUri('file:///path/to/video.mp4');\n * if (isValid) {\n * const asset = await TwoStepVideo.loadAsset({ uri });\n * }\n * ```\n */\nexport async function validateVideoUri(uri: string): Promise<boolean> {\n return await ExpoTwoStepVideoModule.validateVideoUri(uri);\n}\n\n/**\n * Trim a video to a single time range\n *\n * @param options - Trim options including asset ID and time range\n * @returns Promise resolving to composition metadata\n *\n * @example\n * ```typescript\n * const asset = await TwoStepVideo.loadAsset({ uri });\n * const composition = await TwoStepVideo.trimVideo({\n * assetId: asset.id,\n * startTime: 5.0,\n * endTime: 15.0\n * });\n * console.log(`Trimmed to ${composition.duration}s`);\n * ```\n */\nexport async function trimVideo(options: TrimVideoOptions): Promise<VideoComposition> {\n return await ExpoTwoStepVideoModule.trimVideo(\n options.assetId,\n options.startTime,\n options.endTime\n );\n}\n\n/**\n * Trim a video to multiple segments and concatenate them\n *\n * @param options - Trim options including asset ID and segments\n * @returns Promise resolving to composition metadata\n *\n * @example\n * ```typescript\n * const composition = await TwoStepVideo.trimVideoMultiple({\n * assetId: asset.id,\n * segments: [\n * { start: 0, end: 3 }, // First 3 seconds\n * { start: 10, end: 13 }, // 3 seconds from middle\n * { start: 20, end: 23 } // 3 seconds from end\n * ]\n * });\n * ```\n */\nexport async function trimVideoMultiple(options: TrimMultipleOptions): Promise<VideoComposition> {\n return await ExpoTwoStepVideoModule.trimVideoMultiple(options.assetId, options.segments);\n}\n\n/**\n * Mirror a video horizontally, vertically, or both\n *\n * @param options - Mirror options including asset ID and axis\n * @returns Promise resolving to composition metadata\n *\n * @example\n * ```typescript\n * // Mirror horizontally (flip left-right, common for selfie videos)\n * const composition = await TwoStepVideo.mirrorVideo({\n * assetId: asset.id,\n * axis: 'horizontal'\n * });\n *\n * // Mirror a specific segment\n * const composition = await TwoStepVideo.mirrorVideo({\n * assetId: asset.id,\n * axis: 'vertical',\n * startTime: 5,\n * endTime: 10\n * });\n * ```\n */\nexport async function mirrorVideo(options: MirrorVideoOptions): Promise<VideoComposition> {\n return await ExpoTwoStepVideoModule.mirrorVideo(\n options.assetId,\n options.axis,\n options.startTime,\n options.endTime\n );\n}\n\n/**\n * Adjust the playback speed of a video\n *\n * @param options - Speed options including asset ID and speed multiplier\n * @returns Promise resolving to composition metadata\n *\n * @example\n * ```typescript\n * // Slow motion (0.5x speed = 2x slower)\n * const slowMo = await TwoStepVideo.adjustSpeed({\n * assetId: asset.id,\n * speed: 0.5\n * });\n *\n * // Fast forward (2x speed)\n * const fastForward = await TwoStepVideo.adjustSpeed({\n * assetId: asset.id,\n * speed: 2.0\n * });\n *\n * // Speed up a specific segment\n * const timelapse = await TwoStepVideo.adjustSpeed({\n * assetId: asset.id,\n * speed: 4.0,\n * startTime: 10,\n * endTime: 30\n * });\n * ```\n */\nexport async function adjustSpeed(options: AdjustSpeedOptions): Promise<VideoComposition> {\n return await ExpoTwoStepVideoModule.adjustSpeed(\n options.assetId,\n options.speed,\n options.startTime,\n options.endTime\n );\n}\n\n/**\n * Transform a video with combined trimming, mirroring, and speed adjustment\n * Use the player's `loop` prop for continuous playback looping\n *\n * @param options - Transform options including asset ID, speed, and mirror axis\n * @returns Promise resolving to composition metadata\n *\n * @example\n * ```typescript\n * // Mirror and slow down\n * const transformed = await TwoStepVideo.transformVideo({\n * assetId: asset.id,\n * speed: 0.5,\n * mirrorAxis: 'horizontal'\n * });\n *\n * // Just mirror (speed defaults to 1.0)\n * const mirrored = await TwoStepVideo.transformVideo({\n * assetId: asset.id,\n * mirrorAxis: 'both'\n * });\n *\n * // Transform a specific segment (trim + mirror + speed)\n * const segment = await TwoStepVideo.transformVideo({\n * assetId: asset.id,\n * speed: 2.0,\n * mirrorAxis: 'horizontal',\n * startTime: 0,\n * endTime: 5\n * });\n *\n * // Play in loop mode\n * <TwoStepVideoView compositionId={segment.id} loop />\n * ```\n */\nexport async function transformVideo(options: TransformVideoOptions): Promise<VideoComposition> {\n return await ExpoTwoStepVideoModule.transformVideo(\n options.assetId,\n options.speed,\n options.mirrorAxis,\n options.startTime,\n options.endTime\n );\n}\n\n/**\n * Loop a segment of a video multiple times\n *\n * @param options - Loop options including segment time range and repeat count\n * @returns Promise resolving to loop result with duration info\n *\n * @example\n * ```typescript\n * // Loop a 2-second segment 3 times (plays 4 times total)\n * const looped = await TwoStepVideo.loopSegment({\n * assetId: asset.id,\n * startTime: 5,\n * endTime: 7,\n * loopCount: 3\n * });\n * console.log(`Duration: ${looped.duration}s (plays ${looped.totalPlays} times)`);\n *\n * // Create a perfect loop for social media\n * const perfectLoop = await TwoStepVideo.loopSegment({\n * assetId: asset.id,\n * startTime: 0,\n * endTime: 3,\n * loopCount: 4 // 15 seconds total (3s * 5 plays)\n * });\n * ```\n */\nexport async function loopSegment(options: LoopSegmentOptions): Promise<LoopResult> {\n return await ExpoTwoStepVideoModule.loopSegment(\n options.assetId,\n options.startTime,\n options.endTime,\n options.loopCount\n );\n}\n\n// Re-export PanZoomVideoOptions from types\nexport type { PanZoomVideoOptions } from './ExpoTwoStepVideo.types';\n\n/**\n * Apply pan and zoom transformation to a video\n *\n * This creates a composition with the pan/zoom baked in for export.\n * For real-time preview, use the gesture controls on TwoStepVideoView.\n *\n * @param options - Pan/zoom options including asset ID and transform values\n * @returns Promise resolving to composition metadata\n *\n * @example\n * ```typescript\n * // Get current pan/zoom from player and apply to export\n * const panZoomState = await playerRef.current.getPanZoomState();\n * const composition = await TwoStepVideo.panZoomVideo({\n * assetId: asset.id,\n * panX: panZoomState.panX,\n * panY: panZoomState.panY,\n * zoomLevel: panZoomState.zoomLevel\n * });\n *\n * // Export the pan/zoomed video\n * const result = await TwoStepVideo.exportVideo({\n * compositionId: composition.id,\n * quality: 'high'\n * });\n *\n * // Apply zoom only (no pan, centered)\n * const zoomed = await TwoStepVideo.panZoomVideo({\n * assetId: asset.id,\n * zoomLevel: 2.0 // 2x zoom, centered\n * });\n * ```\n */\nexport async function panZoomVideo(options: PanZoomVideoOptions): Promise<VideoComposition> {\n return await ExpoTwoStepVideoModule.panZoomVideo(\n options.assetId,\n options.panX ?? 0,\n options.panY ?? 0,\n options.zoomLevel ?? 1.0,\n options.startTime,\n options.endTime\n );\n}\n\n/**\n * Generate thumbnail images from a video at specific times\n *\n * @param options - Thumbnail generation options\n * @returns Promise resolving to array of base64 encoded PNG images\n *\n * @example\n * ```typescript\n * const thumbnails = await TwoStepVideo.generateThumbnails({\n * assetId: asset.id,\n * times: [1, 5, 10, 15],\n * size: { width: 300, height: 300 }\n * });\n *\n * // Use in Image component\n * <Image source={{ uri: `data:image/png;base64,${thumbnails[0]}` }} />\n * ```\n */\nexport async function generateThumbnails(\n options: GenerateThumbnailsOptions\n): Promise<string[]> {\n return await ExpoTwoStepVideoModule.generateThumbnails(\n options.assetId,\n options.times,\n options.size\n );\n}\n\n/**\n * Export a composition to a video file\n *\n * @param options - Export options including composition ID and quality\n * @returns Promise resolving to export result with file URI\n *\n * @example\n * ```typescript\n * const result = await TwoStepVideo.exportVideo({\n * compositionId: composition.id,\n * quality: 'high'\n * });\n * console.log(`Exported to: ${result.uri}`);\n * ```\n */\nexport async function exportVideo(options: ExportCompositionOptions): Promise<ExportResult> {\n return await ExpoTwoStepVideoModule.exportVideo(\n options.compositionId,\n options.outputUri,\n options.quality\n );\n}\n\n/**\n * Export an asset directly without creating a composition\n * Useful for re-encoding or changing quality without trimming\n *\n * @param options - Export options including asset ID and quality\n * @returns Promise resolving to export result with file URI\n *\n * @example\n * ```typescript\n * const result = await TwoStepVideo.exportAsset({\n * assetId: asset.id,\n * quality: 'medium'\n * });\n * ```\n */\nexport async function exportAsset(options: ExportAssetOptions): Promise<ExportResult> {\n return await ExpoTwoStepVideoModule.exportAsset(\n options.assetId,\n options.outputUri,\n options.quality\n );\n}\n\n/**\n * Clean up a temporary file\n * Call this after you're done with exported files in the temp directory\n *\n * @param uri - File URI to clean up\n *\n * @example\n * ```typescript\n * const result = await TwoStepVideo.exportVideo({ ... });\n * // ... do something with result.uri\n * TwoStepVideo.cleanupFile(result.uri);\n * ```\n */\nexport function cleanupFile(uri: string): void {\n ExpoTwoStepVideoModule.cleanupFile(uri);\n}\n\n/**\n * Release an asset from memory\n * Call this when you're done with an asset to free up memory\n *\n * @param assetId - ID of the asset to release\n */\nexport function releaseAsset(assetId: string): void {\n ExpoTwoStepVideoModule.releaseAsset(assetId);\n}\n\n/**\n * Release a composition from memory\n *\n * @param compositionId - ID of the composition to release\n */\nexport function releaseComposition(compositionId: string): void {\n ExpoTwoStepVideoModule.releaseComposition(compositionId);\n}\n\n/**\n * Release all cached assets and compositions\n * Useful for cleanup when unmounting screens\n */\nexport function releaseAll(): void {\n ExpoTwoStepVideoModule.releaseAll();\n}\n\n// MARK: - Event Listeners\n\n/**\n * Add a listener for export progress events\n *\n * @param listener - Callback function receiving progress events\n * @returns Subscription object with remove() method\n *\n * @example\n * ```typescript\n * const subscription = TwoStepVideo.addExportProgressListener((event) => {\n * console.log(`Export progress: ${Math.round(event.progress * 100)}%`);\n * setProgress(event.progress);\n * });\n *\n * // Later, remove the listener\n * subscription.remove();\n * ```\n */\nexport function addExportProgressListener(\n listener: (event: ExportProgressEvent) => void\n): EventSubscription {\n return ExpoTwoStepVideoModule.addListener('onExportProgress', listener);\n}\n\n// MARK: - Helper Hook (for React)\n\n/**\n * React hook for managing export progress\n * Only works in React components\n *\n * @returns Current export progress (0.0 to 1.0)\n *\n * @example\n * ```typescript\n * function VideoEditor() {\n * const progress = useExportProgress();\n *\n * return (\n * <View>\n * <Text>Export Progress: {Math.round(progress * 100)}%</Text>\n * </View>\n * );\n * }\n * ```\n */\nexport function useExportProgress(): number {\n const [progress, setProgress] = useState(0);\n\n useEffect(() => {\n const subscription = addExportProgressListener((event) => {\n setProgress(event.progress);\n });\n\n return () => subscription.remove();\n }, []);\n\n return progress;\n}\n\n// MARK: - Exports\n\nexport default {\n // Constants\n Quality,\n Mirror,\n\n // Core functions\n loadAsset,\n loadAssetFromPhotos,\n validateVideoUri,\n trimVideo,\n trimVideoMultiple,\n generateThumbnails,\n exportVideo,\n exportAsset,\n\n // Transformation functions\n mirrorVideo,\n adjustSpeed,\n transformVideo,\n loopSegment,\n panZoomVideo,\n\n // Memory management\n cleanupFile,\n releaseAsset,\n releaseComposition,\n releaseAll,\n\n // Events\n addExportProgressListener,\n\n // React hook\n useExportProgress,\n};\n\n// MARK: - View Component Export\n\nexport { default as TwoStepVideoView } from './ExpoTwoStepVideoView';\nexport { default as TwoStepPlayerControllerView } from './TwoStepPlayerControllerView';\nexport { default as VideoScrubber } from './VideoScrubber';\nexport { useVideoScrubber } from './hooks/useVideoScrubber';\nexport type {\n BaseVideoViewRef,\n TwoStepVideoViewProps,\n TwoStepVideoViewRef,\n TwoStepPlayerControllerViewProps,\n TwoStepPlayerControllerViewRef,\n PlaybackStatusEvent,\n ProgressEvent,\n ErrorEvent,\n PanZoomState,\n PanZoomChangeEvent,\n} from './ExpoTwoStepVideo.types';\nexport type {\n VideoScrubberProps,\n VideoScrubberRef,\n VideoScrubberTheme,\n} from './VideoScrubber.types';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * TwoStepVideo - Professional video editing for React Native\n * Built on native AVFoundation with Expo Modules\n */\n\nimport { type EventSubscription } from 'expo-modules-core';\nimport { useState, useEffect } from 'react';\nimport ExpoTwoStepVideoModule from './ExpoTwoStepVideoModule';\nimport type { PanZoomVideoOptions, PickVideoOptions, PickedVideo } from './ExpoTwoStepVideo.types';\n\n// MARK: - Types\n\n/**\n * Quality presets for video export\n */\nexport type VideoQuality = 'low' | 'medium' | 'high' | 'highest';\n\n/**\n * Mirror axis options for video mirroring\n */\nexport type MirrorAxis = 'horizontal' | 'vertical' | 'both';\n\n/**\n * Video asset metadata returned after loading\n */\nexport interface VideoAsset {\n /** Unique identifier for this asset */\n id: string;\n /** Duration in seconds */\n duration: number;\n /** Video width in pixels */\n width: number;\n /** Video height in pixels */\n height: number;\n /** Frame rate (fps) */\n frameRate: number;\n /** Whether the video has audio */\n hasAudio: boolean;\n}\n\n/**\n * Video composition metadata\n */\nexport interface VideoComposition {\n /** Unique identifier for this composition */\n id: string;\n /** Total duration in seconds */\n duration: number;\n}\n\n/**\n * Time segment for multi-segment trimming\n */\nexport interface TimeSegment {\n /** Start time in seconds */\n start: number;\n /** End time in seconds */\n end: number;\n}\n\n/**\n * Export result containing output file information\n */\nexport interface ExportResult {\n /** File URI (e.g., \"file:///path/to/video.mp4\") */\n uri: string;\n /** Absolute file path */\n path: string;\n}\n\n/**\n * Export progress event\n */\nexport interface ExportProgressEvent {\n /** Progress from 0.0 to 1.0 */\n progress: number;\n /** Asset ID if exporting asset directly */\n assetId?: string;\n /** Composition ID if exporting composition */\n compositionId?: string;\n}\n\n/**\n * Options for loading a video asset\n */\nexport interface LoadAssetOptions {\n /** File URI (e.g., \"file:///path/to/video.mp4\") */\n uri: string;\n}\n\n/**\n * Options for trimming a video\n */\nexport interface TrimVideoOptions {\n /** Asset ID to trim */\n assetId: string;\n /** Start time in seconds */\n startTime: number;\n /** End time in seconds */\n endTime: number;\n}\n\n/**\n * Options for multi-segment trimming\n */\nexport interface TrimMultipleOptions {\n /** Asset ID to trim */\n assetId: string;\n /** Array of time segments to extract and concatenate */\n segments: TimeSegment[];\n}\n\n/**\n * Options for generating thumbnails\n */\nexport interface GenerateThumbnailsOptions {\n /** Asset ID to generate thumbnails from */\n assetId: string;\n /** Array of times (in seconds) to extract thumbnails */\n times: number[];\n /** Optional size for thumbnails */\n size?: {\n width: number;\n height: number;\n };\n}\n\n/**\n * Options for exporting a composition\n */\nexport interface ExportCompositionOptions {\n /** Composition ID to export */\n compositionId: string;\n /** Optional output URI (uses temp directory if not provided) */\n outputUri?: string;\n /** Quality preset (default: 'high') */\n quality?: VideoQuality;\n}\n\n/**\n * Options for exporting an asset directly\n */\nexport interface ExportAssetOptions {\n /** Asset ID to export */\n assetId: string;\n /** Optional output URI (uses temp directory if not provided) */\n outputUri?: string;\n /** Quality preset (default: 'high') */\n quality?: VideoQuality;\n}\n\n/**\n * Options for mirroring a video\n */\nexport interface MirrorVideoOptions {\n /** Asset ID to mirror */\n assetId: string;\n /** Axis to mirror on: 'horizontal', 'vertical', or 'both' */\n axis: MirrorAxis;\n /** Optional start time in seconds (for segment mirroring) */\n startTime?: number;\n /** Optional end time in seconds (for segment mirroring) */\n endTime?: number;\n}\n\n/**\n * Options for adjusting video speed\n */\nexport interface AdjustSpeedOptions {\n /** Asset ID to adjust */\n assetId: string;\n /** Speed multiplier (0.25 = 4x slower, 2.0 = 2x faster) */\n speed: number;\n /** Optional start time in seconds */\n startTime?: number;\n /** Optional end time in seconds */\n endTime?: number;\n}\n\n/**\n * Options for combined video transformation\n */\nexport interface TransformVideoOptions {\n /** Asset ID to transform */\n assetId: string;\n /** Speed multiplier (default: 1.0) */\n speed?: number;\n /** Optional mirror axis */\n mirrorAxis?: MirrorAxis;\n /** Optional start time in seconds (for trimming) */\n startTime?: number;\n /** Optional end time in seconds (for trimming) */\n endTime?: number;\n}\n\n/**\n * Options for looping a video segment\n */\nexport interface LoopSegmentOptions {\n /** Asset ID to loop */\n assetId: string;\n /** Start time of the segment to loop (in seconds) */\n startTime: number;\n /** End time of the segment to loop (in seconds) */\n endTime: number;\n /** Number of times to repeat (total plays = loopCount + 1) */\n loopCount: number;\n}\n\n/**\n * Result from looping a video segment\n */\nexport interface LoopResult {\n /** Unique identifier for this composition */\n id: string;\n /** Total duration in seconds */\n duration: number;\n /** Number of times the segment repeats */\n loopCount: number;\n /** Total number of times the segment plays */\n totalPlays: number;\n}\n\n// MARK: - Constants\n\n/**\n * Quality constants\n */\nexport const Quality = {\n LOW: ExpoTwoStepVideoModule.QUALITY_LOW,\n MEDIUM: ExpoTwoStepVideoModule.QUALITY_MEDIUM,\n HIGH: ExpoTwoStepVideoModule.QUALITY_HIGH,\n HIGHEST: ExpoTwoStepVideoModule.QUALITY_HIGHEST,\n} as const;\n\n/**\n * Mirror axis constants\n */\nexport const Mirror = {\n HORIZONTAL: ExpoTwoStepVideoModule.MIRROR_HORIZONTAL,\n VERTICAL: ExpoTwoStepVideoModule.MIRROR_VERTICAL,\n BOTH: ExpoTwoStepVideoModule.MIRROR_BOTH,\n} as const;\n\n// MARK: - API Functions\n\n/**\n * Load a video asset from a file URI\n *\n * @param options - Load options containing the file URI\n * @returns Promise resolving to video asset metadata\n *\n * @example\n * ```typescript\n * const asset = await TwoStepVideo.loadAsset({\n * uri: 'file:///path/to/video.mp4'\n * });\n * console.log(`Duration: ${asset.duration}s, Size: ${asset.width}x${asset.height}`);\n * ```\n */\nexport async function loadAsset(options: LoadAssetOptions): Promise<VideoAsset> {\n return await ExpoTwoStepVideoModule.loadAsset(options.uri);\n}\n\n/**\n * Load a video asset from the Photos library\n *\n * @param localIdentifier - PHAsset local identifier from the Photos library\n * @returns Promise resolving to video asset metadata\n *\n * @example\n * ```typescript\n * // After using expo-media-library to get a photo asset\n * const asset = await TwoStepVideo.loadAssetFromPhotos(photoAsset.id);\n * ```\n */\nexport async function loadAssetFromPhotos(localIdentifier: string): Promise<VideoAsset> {\n return await ExpoTwoStepVideoModule.loadAssetFromPhotos(localIdentifier);\n}\n\n/**\n * Validate a video file URI without loading the full asset\n * Useful for quick validation before processing\n *\n * @param uri - File URI to validate\n * @returns Promise resolving to true if valid video file\n *\n * @example\n * ```typescript\n * const isValid = await TwoStepVideo.validateVideoUri('file:///path/to/video.mp4');\n * if (isValid) {\n * const asset = await TwoStepVideo.loadAsset({ uri });\n * }\n * ```\n */\nexport async function validateVideoUri(uri: string): Promise<boolean> {\n return await ExpoTwoStepVideoModule.validateVideoUri(uri);\n}\n\n/**\n * Trim a video to a single time range\n *\n * @param options - Trim options including asset ID and time range\n * @returns Promise resolving to composition metadata\n *\n * @example\n * ```typescript\n * const asset = await TwoStepVideo.loadAsset({ uri });\n * const composition = await TwoStepVideo.trimVideo({\n * assetId: asset.id,\n * startTime: 5.0,\n * endTime: 15.0\n * });\n * console.log(`Trimmed to ${composition.duration}s`);\n * ```\n */\nexport async function trimVideo(options: TrimVideoOptions): Promise<VideoComposition> {\n return await ExpoTwoStepVideoModule.trimVideo(\n options.assetId,\n options.startTime,\n options.endTime\n );\n}\n\n/**\n * Trim a video to multiple segments and concatenate them\n *\n * @param options - Trim options including asset ID and segments\n * @returns Promise resolving to composition metadata\n *\n * @example\n * ```typescript\n * const composition = await TwoStepVideo.trimVideoMultiple({\n * assetId: asset.id,\n * segments: [\n * { start: 0, end: 3 }, // First 3 seconds\n * { start: 10, end: 13 }, // 3 seconds from middle\n * { start: 20, end: 23 } // 3 seconds from end\n * ]\n * });\n * ```\n */\nexport async function trimVideoMultiple(options: TrimMultipleOptions): Promise<VideoComposition> {\n return await ExpoTwoStepVideoModule.trimVideoMultiple(options.assetId, options.segments);\n}\n\n/**\n * Mirror a video horizontally, vertically, or both\n *\n * @param options - Mirror options including asset ID and axis\n * @returns Promise resolving to composition metadata\n *\n * @example\n * ```typescript\n * // Mirror horizontally (flip left-right, common for selfie videos)\n * const composition = await TwoStepVideo.mirrorVideo({\n * assetId: asset.id,\n * axis: 'horizontal'\n * });\n *\n * // Mirror a specific segment\n * const composition = await TwoStepVideo.mirrorVideo({\n * assetId: asset.id,\n * axis: 'vertical',\n * startTime: 5,\n * endTime: 10\n * });\n * ```\n */\nexport async function mirrorVideo(options: MirrorVideoOptions): Promise<VideoComposition> {\n return await ExpoTwoStepVideoModule.mirrorVideo(\n options.assetId,\n options.axis,\n options.startTime,\n options.endTime\n );\n}\n\n/**\n * Adjust the playback speed of a video\n *\n * @param options - Speed options including asset ID and speed multiplier\n * @returns Promise resolving to composition metadata\n *\n * @example\n * ```typescript\n * // Slow motion (0.5x speed = 2x slower)\n * const slowMo = await TwoStepVideo.adjustSpeed({\n * assetId: asset.id,\n * speed: 0.5\n * });\n *\n * // Fast forward (2x speed)\n * const fastForward = await TwoStepVideo.adjustSpeed({\n * assetId: asset.id,\n * speed: 2.0\n * });\n *\n * // Speed up a specific segment\n * const timelapse = await TwoStepVideo.adjustSpeed({\n * assetId: asset.id,\n * speed: 4.0,\n * startTime: 10,\n * endTime: 30\n * });\n * ```\n */\nexport async function adjustSpeed(options: AdjustSpeedOptions): Promise<VideoComposition> {\n return await ExpoTwoStepVideoModule.adjustSpeed(\n options.assetId,\n options.speed,\n options.startTime,\n options.endTime\n );\n}\n\n/**\n * Transform a video with combined trimming, mirroring, and speed adjustment\n * Use the player's `loop` prop for continuous playback looping\n *\n * @param options - Transform options including asset ID, speed, and mirror axis\n * @returns Promise resolving to composition metadata\n *\n * @example\n * ```typescript\n * // Mirror and slow down\n * const transformed = await TwoStepVideo.transformVideo({\n * assetId: asset.id,\n * speed: 0.5,\n * mirrorAxis: 'horizontal'\n * });\n *\n * // Just mirror (speed defaults to 1.0)\n * const mirrored = await TwoStepVideo.transformVideo({\n * assetId: asset.id,\n * mirrorAxis: 'both'\n * });\n *\n * // Transform a specific segment (trim + mirror + speed)\n * const segment = await TwoStepVideo.transformVideo({\n * assetId: asset.id,\n * speed: 2.0,\n * mirrorAxis: 'horizontal',\n * startTime: 0,\n * endTime: 5\n * });\n *\n * // Play in loop mode\n * <TwoStepVideoView compositionId={segment.id} loop />\n * ```\n */\nexport async function transformVideo(options: TransformVideoOptions): Promise<VideoComposition> {\n return await ExpoTwoStepVideoModule.transformVideo(\n options.assetId,\n options.speed,\n options.mirrorAxis,\n options.startTime,\n options.endTime\n );\n}\n\n/**\n * Loop a segment of a video multiple times\n *\n * @param options - Loop options including segment time range and repeat count\n * @returns Promise resolving to loop result with duration info\n *\n * @example\n * ```typescript\n * // Loop a 2-second segment 3 times (plays 4 times total)\n * const looped = await TwoStepVideo.loopSegment({\n * assetId: asset.id,\n * startTime: 5,\n * endTime: 7,\n * loopCount: 3\n * });\n * console.log(`Duration: ${looped.duration}s (plays ${looped.totalPlays} times)`);\n *\n * // Create a perfect loop for social media\n * const perfectLoop = await TwoStepVideo.loopSegment({\n * assetId: asset.id,\n * startTime: 0,\n * endTime: 3,\n * loopCount: 4 // 15 seconds total (3s * 5 plays)\n * });\n * ```\n */\nexport async function loopSegment(options: LoopSegmentOptions): Promise<LoopResult> {\n return await ExpoTwoStepVideoModule.loopSegment(\n options.assetId,\n options.startTime,\n options.endTime,\n options.loopCount\n );\n}\n\n// Re-export types from types file\nexport type { PanZoomVideoOptions, PickVideoOptions, PickedVideo, DoubleTapSkipEvent } from './ExpoTwoStepVideo.types';\n\n// MARK: - Media Picker Functions\n\n/**\n * Pick video(s) from the photo library using native PHPickerViewController\n *\n * This provides a native photo library picker that:\n * - Supports albums and favorites\n * - Handles iCloud video downloading automatically (with native progress UI)\n * - Returns comprehensive metadata including creation date\n * - No editing UI - just clean selection\n *\n * @param options - Picker options (optional)\n * @returns Promise resolving to array of picked videos (empty if cancelled)\n *\n * @example\n * ```typescript\n * // Pick a single video\n * const videos = await TwoStepVideo.pickVideo();\n * if (videos.length > 0) {\n * const video = videos[0];\n * console.log(`Selected: ${video.fileName}`);\n * console.log(`Duration: ${video.duration}s`);\n * console.log(`Size: ${video.width}x${video.height}`);\n * console.log(`File size: ${video.fileSize} bytes`);\n * console.log(`Created: ${video.creationDate}`);\n *\n * // Load the video for editing\n * const asset = await TwoStepVideo.loadAsset({ uri: video.uri });\n *\n * // Clean up the temp file when done\n * TwoStepVideo.cleanupPickedVideo(video.path);\n * }\n *\n * // Pick multiple videos\n * const videos = await TwoStepVideo.pickVideo({ selectionLimit: 5 });\n * ```\n */\nexport async function pickVideo(options?: PickVideoOptions): Promise<PickedVideo[]> {\n return await ExpoTwoStepVideoModule.pickVideo(options?.selectionLimit);\n}\n\n/**\n * Clean up a specific picked video file from temp storage\n * Call this when you're done with a video that was picked from the library\n *\n * @param path - File path to clean up (use the `path` property from PickedVideo)\n *\n * @example\n * ```typescript\n * const videos = await TwoStepVideo.pickVideo();\n * const video = videos[0];\n *\n * // ... use the video ...\n *\n * // Clean up when done\n * TwoStepVideo.cleanupPickedVideo(video.path);\n * ```\n */\nexport function cleanupPickedVideo(path: string): void {\n ExpoTwoStepVideoModule.cleanupPickedVideo(path);\n}\n\n/**\n * Clean up all picked video files from temp storage\n * Useful for cleanup when unmounting screens or releasing all resources\n *\n * @example\n * ```typescript\n * // In a cleanup effect\n * useEffect(() => {\n * return () => {\n * TwoStepVideo.cleanupAllPickedVideos();\n * TwoStepVideo.releaseAll();\n * };\n * }, []);\n * ```\n */\nexport function cleanupAllPickedVideos(): void {\n ExpoTwoStepVideoModule.cleanupAllPickedVideos();\n}\n\n/**\n * Get list of currently tracked picked video paths\n * Useful for debugging or manual cleanup\n *\n * @returns Array of file paths for picked videos in temp storage\n */\nexport function getPickedVideoPaths(): string[] {\n return ExpoTwoStepVideoModule.getPickedVideoPaths();\n}\n\n/**\n * Apply pan and zoom transformation to a video\n *\n * This creates a composition with the pan/zoom baked in for export.\n * For real-time preview, use the gesture controls on TwoStepVideoView.\n *\n * @param options - Pan/zoom options including asset ID and transform values\n * @returns Promise resolving to composition metadata\n *\n * @example\n * ```typescript\n * // Get current pan/zoom from player and apply to export\n * const panZoomState = await playerRef.current.getPanZoomState();\n * const composition = await TwoStepVideo.panZoomVideo({\n * assetId: asset.id,\n * panX: panZoomState.panX,\n * panY: panZoomState.panY,\n * zoomLevel: panZoomState.zoomLevel\n * });\n *\n * // Export the pan/zoomed video\n * const result = await TwoStepVideo.exportVideo({\n * compositionId: composition.id,\n * quality: 'high'\n * });\n *\n * // Apply zoom only (no pan, centered)\n * const zoomed = await TwoStepVideo.panZoomVideo({\n * assetId: asset.id,\n * zoomLevel: 2.0 // 2x zoom, centered\n * });\n * ```\n */\nexport async function panZoomVideo(options: PanZoomVideoOptions): Promise<VideoComposition> {\n return await ExpoTwoStepVideoModule.panZoomVideo(\n options.assetId,\n options.panX ?? 0,\n options.panY ?? 0,\n options.zoomLevel ?? 1.0,\n options.startTime,\n options.endTime\n );\n}\n\n/**\n * Generate thumbnail images from a video at specific times\n *\n * @param options - Thumbnail generation options\n * @returns Promise resolving to array of base64 encoded PNG images\n *\n * @example\n * ```typescript\n * const thumbnails = await TwoStepVideo.generateThumbnails({\n * assetId: asset.id,\n * times: [1, 5, 10, 15],\n * size: { width: 300, height: 300 }\n * });\n *\n * // Use in Image component\n * <Image source={{ uri: `data:image/png;base64,${thumbnails[0]}` }} />\n * ```\n */\nexport async function generateThumbnails(\n options: GenerateThumbnailsOptions\n): Promise<string[]> {\n return await ExpoTwoStepVideoModule.generateThumbnails(\n options.assetId,\n options.times,\n options.size\n );\n}\n\n/**\n * Export a composition to a video file\n *\n * @param options - Export options including composition ID and quality\n * @returns Promise resolving to export result with file URI\n *\n * @example\n * ```typescript\n * const result = await TwoStepVideo.exportVideo({\n * compositionId: composition.id,\n * quality: 'high'\n * });\n * console.log(`Exported to: ${result.uri}`);\n * ```\n */\nexport async function exportVideo(options: ExportCompositionOptions): Promise<ExportResult> {\n return await ExpoTwoStepVideoModule.exportVideo(\n options.compositionId,\n options.outputUri,\n options.quality\n );\n}\n\n/**\n * Export an asset directly without creating a composition\n * Useful for re-encoding or changing quality without trimming\n *\n * @param options - Export options including asset ID and quality\n * @returns Promise resolving to export result with file URI\n *\n * @example\n * ```typescript\n * const result = await TwoStepVideo.exportAsset({\n * assetId: asset.id,\n * quality: 'medium'\n * });\n * ```\n */\nexport async function exportAsset(options: ExportAssetOptions): Promise<ExportResult> {\n return await ExpoTwoStepVideoModule.exportAsset(\n options.assetId,\n options.outputUri,\n options.quality\n );\n}\n\n/**\n * Clean up a temporary file\n * Call this after you're done with exported files in the temp directory\n *\n * @param uri - File URI to clean up\n *\n * @example\n * ```typescript\n * const result = await TwoStepVideo.exportVideo({ ... });\n * // ... do something with result.uri\n * TwoStepVideo.cleanupFile(result.uri);\n * ```\n */\nexport function cleanupFile(uri: string): void {\n ExpoTwoStepVideoModule.cleanupFile(uri);\n}\n\n/**\n * Release an asset from memory\n * Call this when you're done with an asset to free up memory\n *\n * @param assetId - ID of the asset to release\n */\nexport function releaseAsset(assetId: string): void {\n ExpoTwoStepVideoModule.releaseAsset(assetId);\n}\n\n/**\n * Release a composition from memory\n *\n * @param compositionId - ID of the composition to release\n */\nexport function releaseComposition(compositionId: string): void {\n ExpoTwoStepVideoModule.releaseComposition(compositionId);\n}\n\n/**\n * Release all cached assets and compositions\n * Useful for cleanup when unmounting screens\n */\nexport function releaseAll(): void {\n ExpoTwoStepVideoModule.releaseAll();\n}\n\n// MARK: - Event Listeners\n\n/**\n * Add a listener for export progress events\n *\n * @param listener - Callback function receiving progress events\n * @returns Subscription object with remove() method\n *\n * @example\n * ```typescript\n * const subscription = TwoStepVideo.addExportProgressListener((event) => {\n * console.log(`Export progress: ${Math.round(event.progress * 100)}%`);\n * setProgress(event.progress);\n * });\n *\n * // Later, remove the listener\n * subscription.remove();\n * ```\n */\nexport function addExportProgressListener(\n listener: (event: ExportProgressEvent) => void\n): EventSubscription {\n return ExpoTwoStepVideoModule.addListener('onExportProgress', listener);\n}\n\n// MARK: - Helper Hook (for React)\n\n/**\n * React hook for managing export progress\n * Only works in React components\n *\n * @returns Current export progress (0.0 to 1.0)\n *\n * @example\n * ```typescript\n * function VideoEditor() {\n * const progress = useExportProgress();\n *\n * return (\n * <View>\n * <Text>Export Progress: {Math.round(progress * 100)}%</Text>\n * </View>\n * );\n * }\n * ```\n */\nexport function useExportProgress(): number {\n const [progress, setProgress] = useState(0);\n\n useEffect(() => {\n const subscription = addExportProgressListener((event) => {\n setProgress(event.progress);\n });\n\n return () => subscription.remove();\n }, []);\n\n return progress;\n}\n\n// MARK: - Exports\n\nexport default {\n // Constants\n Quality,\n Mirror,\n\n // Media picker\n pickVideo,\n cleanupPickedVideo,\n cleanupAllPickedVideos,\n getPickedVideoPaths,\n\n // Core functions\n loadAsset,\n loadAssetFromPhotos,\n validateVideoUri,\n trimVideo,\n trimVideoMultiple,\n generateThumbnails,\n exportVideo,\n exportAsset,\n\n // Transformation functions\n mirrorVideo,\n adjustSpeed,\n transformVideo,\n loopSegment,\n panZoomVideo,\n\n // Memory management\n cleanupFile,\n releaseAsset,\n releaseComposition,\n releaseAll,\n\n // Events\n addExportProgressListener,\n\n // React hook\n useExportProgress,\n};\n\n// MARK: - View Component Export\n\nexport { default as TwoStepVideoView } from './ExpoTwoStepVideoView';\nexport { default as TwoStepPlayerControllerView } from './TwoStepPlayerControllerView';\nexport { default as VideoScrubber } from './VideoScrubber';\nexport { useVideoScrubber } from './hooks/useVideoScrubber';\nexport type {\n BaseVideoViewRef,\n TwoStepVideoViewProps,\n TwoStepVideoViewRef,\n TwoStepPlayerControllerViewProps,\n TwoStepPlayerControllerViewRef,\n PlaybackStatusEvent,\n ProgressEvent,\n ErrorEvent,\n PanZoomState,\n PanZoomChangeEvent,\n} from './ExpoTwoStepVideo.types';\nexport type {\n VideoScrubberProps,\n VideoScrubberRef,\n VideoScrubberTheme,\n} from './VideoScrubber.types';\n"]}
|
|
@@ -667,11 +667,77 @@ public class ExpoTwoStepVideoModule: Module {
|
|
|
667
667
|
self.activeVideoCompositions.removeAll()
|
|
668
668
|
}
|
|
669
669
|
|
|
670
|
+
// MARK: - Media Picker Functions
|
|
671
|
+
|
|
672
|
+
/// Pick video(s) from the photo library using native PHPickerViewController
|
|
673
|
+
/// - Parameter selectionLimit: Maximum number of videos to select (default 1)
|
|
674
|
+
/// - Returns: Array of picked video metadata (or empty array if cancelled)
|
|
675
|
+
AsyncFunction("pickVideo") { (selectionLimit: Int?, promise: Promise) in
|
|
676
|
+
let options = PickerOptions(selectionLimit: selectionLimit ?? 1)
|
|
677
|
+
|
|
678
|
+
self.twoStep.mediaPicker.pickVideo(options: options) { result in
|
|
679
|
+
switch result {
|
|
680
|
+
case .success(let videos):
|
|
681
|
+
// Convert to JS-friendly dictionaries
|
|
682
|
+
let jsVideos = videos.map { video -> [String: Any] in
|
|
683
|
+
var dict: [String: Any] = [
|
|
684
|
+
"uri": video.uri,
|
|
685
|
+
"path": video.path,
|
|
686
|
+
"fileName": video.fileName,
|
|
687
|
+
"width": video.width,
|
|
688
|
+
"height": video.height,
|
|
689
|
+
"duration": video.duration,
|
|
690
|
+
"fileSize": video.fileSize,
|
|
691
|
+
"type": video.type
|
|
692
|
+
]
|
|
693
|
+
|
|
694
|
+
if let creationDate = video.creationDate {
|
|
695
|
+
dict["creationDate"] = creationDate
|
|
696
|
+
}
|
|
697
|
+
if let modificationDate = video.modificationDate {
|
|
698
|
+
dict["modificationDate"] = modificationDate
|
|
699
|
+
}
|
|
700
|
+
if let assetIdentifier = video.assetIdentifier {
|
|
701
|
+
dict["assetIdentifier"] = assetIdentifier
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return dict
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
promise.resolve(jsVideos)
|
|
708
|
+
|
|
709
|
+
case .failure(let error):
|
|
710
|
+
// Cancellation returns empty array, not an error
|
|
711
|
+
if (error as? MediaPickerError) == .cancelled {
|
|
712
|
+
promise.resolve([])
|
|
713
|
+
} else {
|
|
714
|
+
promise.reject("PICKER_ERROR", error.localizedDescription)
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/// Clean up a specific picked video file
|
|
721
|
+
/// - Parameter path: File path to clean up
|
|
722
|
+
Function("cleanupPickedVideo") { (path: String) in
|
|
723
|
+
self.twoStep.mediaPicker.cleanupPickedVideo(at: path)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/// Clean up all picked video files from temp storage
|
|
727
|
+
Function("cleanupAllPickedVideos") {
|
|
728
|
+
self.twoStep.mediaPicker.cleanupAllPickedVideos()
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/// Get list of currently tracked picked video paths
|
|
732
|
+
Function("getPickedVideoPaths") { () -> [String] in
|
|
733
|
+
return self.twoStep.mediaPicker.trackedPickedVideoPaths
|
|
734
|
+
}
|
|
735
|
+
|
|
670
736
|
// MARK: - Video Player View
|
|
671
737
|
|
|
672
738
|
View(ExpoTwoStepVideoView.self) {
|
|
673
739
|
// Events emitted by the view
|
|
674
|
-
Events("onPlaybackStatusChange", "onProgress", "onEnd", "onError", "onPanZoomChange")
|
|
740
|
+
Events("onPlaybackStatusChange", "onProgress", "onEnd", "onError", "onPanZoomChange", "onDoubleTapSkip")
|
|
675
741
|
|
|
676
742
|
// Prop to set composition ID - view will load it
|
|
677
743
|
Prop("compositionId") { (view: ExpoTwoStepVideoView, compositionId: String?) in
|
|
@@ -709,6 +775,16 @@ public class ExpoTwoStepVideoModule: Module {
|
|
|
709
775
|
view.maxZoom = CGFloat(maxZoom ?? 5.0)
|
|
710
776
|
}
|
|
711
777
|
|
|
778
|
+
// Prop to enable/disable native double-tap to skip
|
|
779
|
+
Prop("enableDoubleTapSkip") { (view: ExpoTwoStepVideoView, enable: Bool?) in
|
|
780
|
+
view.enableDoubleTapSkip = enable ?? true
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Prop to set double-tap skip interval in seconds
|
|
784
|
+
Prop("doubleTapSkipInterval") { (view: ExpoTwoStepVideoView, interval: Double?) in
|
|
785
|
+
view.doubleTapSkipInterval = interval ?? 5.0
|
|
786
|
+
}
|
|
787
|
+
|
|
712
788
|
// Playback control functions
|
|
713
789
|
AsyncFunction("play") { (view: ExpoTwoStepVideoView) in
|
|
714
790
|
view.play()
|
|
@@ -45,6 +45,19 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
45
45
|
/// Gesture recognizers
|
|
46
46
|
private var pinchGesture: UIPinchGestureRecognizer?
|
|
47
47
|
private var panGesture: UIPanGestureRecognizer?
|
|
48
|
+
private var doubleTapLeftGesture: UITapGestureRecognizer?
|
|
49
|
+
private var doubleTapRightGesture: UITapGestureRecognizer?
|
|
50
|
+
|
|
51
|
+
// MARK: - Double-Tap Skip Properties
|
|
52
|
+
|
|
53
|
+
/// Whether double-tap to skip is enabled
|
|
54
|
+
var enableDoubleTapSkip: Bool = true
|
|
55
|
+
|
|
56
|
+
/// Seconds to skip on double-tap
|
|
57
|
+
var doubleTapSkipInterval: Double = 5.0
|
|
58
|
+
|
|
59
|
+
/// Event dispatcher for double-tap skip feedback
|
|
60
|
+
let onDoubleTapSkip = EventDispatcher()
|
|
48
61
|
|
|
49
62
|
// MARK: - Initialization
|
|
50
63
|
|
|
@@ -159,6 +172,20 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
159
172
|
addGestureRecognizer(pan)
|
|
160
173
|
panGesture = pan
|
|
161
174
|
|
|
175
|
+
// Double-tap on left side to skip backward
|
|
176
|
+
let doubleTapLeft = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTapLeft(_:)))
|
|
177
|
+
doubleTapLeft.numberOfTapsRequired = 2
|
|
178
|
+
doubleTapLeft.delegate = self
|
|
179
|
+
addGestureRecognizer(doubleTapLeft)
|
|
180
|
+
doubleTapLeftGesture = doubleTapLeft
|
|
181
|
+
|
|
182
|
+
// Double-tap on right side to skip forward
|
|
183
|
+
let doubleTapRight = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTapRight(_:)))
|
|
184
|
+
doubleTapRight.numberOfTapsRequired = 2
|
|
185
|
+
doubleTapRight.delegate = self
|
|
186
|
+
addGestureRecognizer(doubleTapRight)
|
|
187
|
+
doubleTapRightGesture = doubleTapRight
|
|
188
|
+
|
|
162
189
|
isUserInteractionEnabled = true
|
|
163
190
|
}
|
|
164
191
|
|
|
@@ -183,18 +210,6 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
183
210
|
return AVMakeRect(aspectRatio: videoSize, insideRect: layerBounds)
|
|
184
211
|
}
|
|
185
212
|
|
|
186
|
-
/// Adjust an anchor point to be relative to the video content rect.
|
|
187
|
-
/// Clamps points outside the video area to the nearest edge.
|
|
188
|
-
private func adjustedAnchorPoint(for point: CGPoint) -> CGPoint {
|
|
189
|
-
let contentRect = videoContentRect()
|
|
190
|
-
|
|
191
|
-
// Clamp the point to the video content rect
|
|
192
|
-
let clampedX = min(max(point.x, contentRect.minX), contentRect.maxX)
|
|
193
|
-
let clampedY = min(max(point.y, contentRect.minY), contentRect.maxY)
|
|
194
|
-
|
|
195
|
-
return CGPoint(x: clampedX, y: clampedY)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
213
|
// MARK: - Gesture Handlers
|
|
199
214
|
|
|
200
215
|
/// Get the current visual transform, reading from the presentation layer if an animation is in progress
|
|
@@ -227,14 +242,39 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
227
242
|
beginGesture()
|
|
228
243
|
|
|
229
244
|
case .changed:
|
|
230
|
-
|
|
231
|
-
//
|
|
232
|
-
let anchor =
|
|
245
|
+
// Get the pinch center in the PARENT's coordinate system (self)
|
|
246
|
+
// This gives us a stable anchor point that doesn't change with the view's transform
|
|
247
|
+
let anchor = gesture.location(in: self)
|
|
248
|
+
|
|
249
|
+
// Get the incremental scale change
|
|
233
250
|
let scale = gesture.scale
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
251
|
+
gesture.scale = 1.0
|
|
252
|
+
|
|
253
|
+
// Check if new scale would exceed limits
|
|
254
|
+
let currentScale = currentTransform.scaleX
|
|
255
|
+
let proposedScale = currentScale * scale
|
|
256
|
+
|
|
257
|
+
// Skip if we'd go below min zoom
|
|
258
|
+
if proposedScale < minZoom && scale < 1.0 {
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Clamp scale factor if we'd exceed max zoom
|
|
263
|
+
var effectiveScale = scale
|
|
264
|
+
if proposedScale > maxZoom {
|
|
265
|
+
effectiveScale = maxZoom / currentScale
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Calculate new transform that scales around the anchor point
|
|
269
|
+
// Formula: The anchor point in screen coordinates should stay fixed
|
|
270
|
+
// newTx = anchor.x * (1 - scale) + currentTx * scale
|
|
271
|
+
// newTy = anchor.y * (1 - scale) + currentTy * scale
|
|
272
|
+
let newScale = currentScale * effectiveScale
|
|
273
|
+
let newTx = anchor.x * (1 - effectiveScale) + currentTransform.tx * effectiveScale
|
|
274
|
+
let newTy = anchor.y * (1 - effectiveScale) + currentTransform.ty * effectiveScale
|
|
275
|
+
|
|
276
|
+
currentTransform = CGAffineTransform(a: newScale, b: 0, c: 0, d: newScale, tx: newTx, ty: newTy)
|
|
277
|
+
playerContainerView.transform = currentTransform
|
|
238
278
|
|
|
239
279
|
case .ended, .cancelled:
|
|
240
280
|
onGestureEnded()
|
|
@@ -244,6 +284,57 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
244
284
|
}
|
|
245
285
|
}
|
|
246
286
|
|
|
287
|
+
// MARK: - Double-Tap Handlers
|
|
288
|
+
|
|
289
|
+
@objc private func handleDoubleTapLeft(_ gesture: UITapGestureRecognizer) {
|
|
290
|
+
guard enableDoubleTapSkip else { return }
|
|
291
|
+
|
|
292
|
+
let location = gesture.location(in: self)
|
|
293
|
+
// Only handle taps on the left half
|
|
294
|
+
guard location.x < bounds.width / 2 else { return }
|
|
295
|
+
|
|
296
|
+
guard let player = player,
|
|
297
|
+
let currentItem = player.currentItem else { return }
|
|
298
|
+
|
|
299
|
+
let currentTime = CMTimeGetSeconds(player.currentTime())
|
|
300
|
+
let newTime = max(0, currentTime - doubleTapSkipInterval)
|
|
301
|
+
let seekTime = CMTime(seconds: newTime, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
|
302
|
+
|
|
303
|
+
player.seek(to: seekTime, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
304
|
+
|
|
305
|
+
// Emit event for UI feedback
|
|
306
|
+
onDoubleTapSkip([
|
|
307
|
+
"direction": "backward",
|
|
308
|
+
"skipInterval": doubleTapSkipInterval,
|
|
309
|
+
"newTime": newTime
|
|
310
|
+
])
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
@objc private func handleDoubleTapRight(_ gesture: UITapGestureRecognizer) {
|
|
314
|
+
guard enableDoubleTapSkip else { return }
|
|
315
|
+
|
|
316
|
+
let location = gesture.location(in: self)
|
|
317
|
+
// Only handle taps on the right half
|
|
318
|
+
guard location.x >= bounds.width / 2 else { return }
|
|
319
|
+
|
|
320
|
+
guard let player = player,
|
|
321
|
+
let currentItem = player.currentItem else { return }
|
|
322
|
+
|
|
323
|
+
let duration = CMTimeGetSeconds(currentItem.duration)
|
|
324
|
+
let currentTime = CMTimeGetSeconds(player.currentTime())
|
|
325
|
+
let newTime = min(duration, currentTime + doubleTapSkipInterval)
|
|
326
|
+
let seekTime = CMTime(seconds: newTime, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
|
327
|
+
|
|
328
|
+
player.seek(to: seekTime, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
329
|
+
|
|
330
|
+
// Emit event for UI feedback
|
|
331
|
+
onDoubleTapSkip([
|
|
332
|
+
"direction": "forward",
|
|
333
|
+
"skipInterval": doubleTapSkipInterval,
|
|
334
|
+
"newTime": newTime
|
|
335
|
+
])
|
|
336
|
+
}
|
|
337
|
+
|
|
247
338
|
@objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
|
|
248
339
|
switch gesture.state {
|
|
249
340
|
case .began:
|
|
@@ -316,13 +407,16 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
316
407
|
}
|
|
317
408
|
|
|
318
409
|
// Constrain pan to keep content visible
|
|
410
|
+
// When scaled, the content extends beyond the view bounds.
|
|
411
|
+
// The maximum pan distance is half the excess on each side.
|
|
412
|
+
// For a 2x scale on 100px width: content is 200px, excess is 100px, max pan is ±50px
|
|
319
413
|
let contentSize = bounds.size
|
|
320
|
-
let
|
|
321
|
-
let
|
|
414
|
+
let maxPanX = contentSize.width * (capped.scaleX - 1) / 2
|
|
415
|
+
let maxPanY = contentSize.height * (capped.scaleY - 1) / 2
|
|
322
416
|
|
|
323
|
-
// tx/ty constraints:
|
|
324
|
-
capped.tx = min(max(capped.tx, -
|
|
325
|
-
capped.ty = min(max(capped.ty, -
|
|
417
|
+
// tx/ty constraints: allow panning in both directions, but keep content edges visible
|
|
418
|
+
capped.tx = min(max(capped.tx, -maxPanX), maxPanX)
|
|
419
|
+
capped.ty = min(max(capped.ty, -maxPanY), maxPanY)
|
|
326
420
|
|
|
327
421
|
return capped
|
|
328
422
|
}
|
|
@@ -330,12 +424,16 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
330
424
|
/// Emit the current pan/zoom state to JavaScript
|
|
331
425
|
private func emitPanZoomChange() {
|
|
332
426
|
let scale = currentTransform.scaleX
|
|
333
|
-
|
|
334
|
-
|
|
427
|
+
// Convert tx/ty to normalized -1 to 1 range
|
|
428
|
+
// maxPan = size * (scale - 1) / 2, so panX = tx / maxPan
|
|
429
|
+
let maxPanX = bounds.width * (scale - 1) / 2
|
|
430
|
+
let maxPanY = bounds.height * (scale - 1) / 2
|
|
431
|
+
let panX = (scale > 1.0 && maxPanX > 0) ? currentTransform.tx / maxPanX : 0
|
|
432
|
+
let panY = (scale > 1.0 && maxPanY > 0) ? currentTransform.ty / maxPanY : 0
|
|
335
433
|
|
|
336
434
|
onPanZoomChange([
|
|
337
|
-
"panX":
|
|
338
|
-
"panY":
|
|
435
|
+
"panX": panX, // -1 to 1, positive = content shifted right
|
|
436
|
+
"panY": panY, // -1 to 1, positive = content shifted down
|
|
339
437
|
"zoomLevel": scale
|
|
340
438
|
])
|
|
341
439
|
}
|
|
@@ -345,12 +443,15 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
345
443
|
/// Get the current pan/zoom state
|
|
346
444
|
func getPanZoomState() -> [String: CGFloat] {
|
|
347
445
|
let scale = currentTransform.scaleX
|
|
348
|
-
|
|
349
|
-
let
|
|
446
|
+
// Convert tx/ty to normalized -1 to 1 range
|
|
447
|
+
let maxPanX = bounds.width * (scale - 1) / 2
|
|
448
|
+
let maxPanY = bounds.height * (scale - 1) / 2
|
|
449
|
+
let panX = (scale > 1.0 && maxPanX > 0) ? currentTransform.tx / maxPanX : 0
|
|
450
|
+
let panY = (scale > 1.0 && maxPanY > 0) ? currentTransform.ty / maxPanY : 0
|
|
350
451
|
|
|
351
452
|
return [
|
|
352
|
-
"panX":
|
|
353
|
-
"panY":
|
|
453
|
+
"panX": panX, // -1 to 1, positive = content shifted right
|
|
454
|
+
"panY": panY, // -1 to 1, positive = content shifted down
|
|
354
455
|
"zoomLevel": scale
|
|
355
456
|
]
|
|
356
457
|
}
|
|
@@ -372,8 +473,12 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
372
473
|
if let x = panX, let y = panY {
|
|
373
474
|
let scale = newTransform.scaleX
|
|
374
475
|
if scale > 1.0 {
|
|
375
|
-
|
|
376
|
-
|
|
476
|
+
// Convert normalized -1 to 1 values to tx/ty
|
|
477
|
+
// maxPan = size * (scale - 1) / 2, so tx = panX * maxPan
|
|
478
|
+
let maxPanX = bounds.width * (scale - 1) / 2
|
|
479
|
+
let maxPanY = bounds.height * (scale - 1) / 2
|
|
480
|
+
newTransform.tx = x * maxPanX
|
|
481
|
+
newTransform.ty = y * maxPanY
|
|
377
482
|
}
|
|
378
483
|
}
|
|
379
484
|
|
|
@@ -590,7 +695,7 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
590
695
|
// MARK: - UIGestureRecognizerDelegate
|
|
591
696
|
|
|
592
697
|
extension ExpoTwoStepVideoView: UIGestureRecognizerDelegate {
|
|
593
|
-
/// Allow
|
|
698
|
+
/// Allow gestures to work simultaneously
|
|
594
699
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
595
700
|
// Allow our pinch and pan to work together
|
|
596
701
|
if (gestureRecognizer == pinchGesture && otherGestureRecognizer == panGesture) ||
|
|
@@ -601,15 +706,29 @@ extension ExpoTwoStepVideoView: UIGestureRecognizerDelegate {
|
|
|
601
706
|
if gestureRecognizer == pinchGesture {
|
|
602
707
|
return true
|
|
603
708
|
}
|
|
709
|
+
// Allow double-tap gestures to work with each other (left and right can coexist)
|
|
710
|
+
if (gestureRecognizer == doubleTapLeftGesture || gestureRecognizer == doubleTapRightGesture) &&
|
|
711
|
+
(otherGestureRecognizer == doubleTapLeftGesture || otherGestureRecognizer == doubleTapRightGesture) {
|
|
712
|
+
return true
|
|
713
|
+
}
|
|
604
714
|
return false
|
|
605
715
|
}
|
|
606
716
|
|
|
607
|
-
/// Only allow pan gesture when zoomed in
|
|
717
|
+
/// Only allow pan gesture when zoomed in, and filter double-tap by location
|
|
608
718
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
609
719
|
if gestureRecognizer == panGesture {
|
|
610
720
|
// Read from actual view transform to be accurate
|
|
611
721
|
return playerContainerView.transform.scaleX > 1.01
|
|
612
722
|
}
|
|
723
|
+
// Filter double-tap gestures by their location (left half vs right half)
|
|
724
|
+
if gestureRecognizer == doubleTapLeftGesture {
|
|
725
|
+
let location = gestureRecognizer.location(in: self)
|
|
726
|
+
return location.x < bounds.width / 2
|
|
727
|
+
}
|
|
728
|
+
if gestureRecognizer == doubleTapRightGesture {
|
|
729
|
+
let location = gestureRecognizer.location(in: self)
|
|
730
|
+
return location.x >= bounds.width / 2
|
|
731
|
+
}
|
|
613
732
|
return true
|
|
614
733
|
}
|
|
615
734
|
|