@movementinfra/expo-twostep-video 0.1.10 → 0.1.12
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 +221 -0
- package/build/ExpoTwoStepVideo.types.d.ts +46 -0
- package/build/ExpoTwoStepVideo.types.d.ts.map +1 -1
- package/build/ExpoTwoStepVideo.types.js.map +1 -1
- package/build/ExpoTwoStepVideoView.d.ts.map +1 -1
- package/build/ExpoTwoStepVideoView.js +10 -0
- package/build/ExpoTwoStepVideoView.js.map +1 -1
- package/build/TwoStepPlayerControllerView.d.ts.map +1 -1
- package/build/TwoStepPlayerControllerView.js +10 -0
- package/build/TwoStepPlayerControllerView.js.map +1 -1
- package/build/index.d.ts +53 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +37 -0
- package/build/index.js.map +1 -1
- package/ios/ExpoTwoStepVideoModule.swift +75 -1
- package/ios/ExpoTwoStepVideoView.swift +216 -0
- package/ios/TwoStepVideo/Core/VideoTransformer.swift +167 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,6 +126,7 @@ function VideoEditor() {
|
|
|
126
126
|
| `adjustSpeed({ assetId, speed, startTime?, endTime? })` | Change speed | `VideoComposition` |
|
|
127
127
|
| `loopSegment({ assetId, startTime, endTime, loopCount })` | Loop a segment | `LoopResult` |
|
|
128
128
|
| `transformVideo({ assetId, speed?, mirrorAxis?, startTime?, endTime? })` | Combined transform | `VideoComposition` |
|
|
129
|
+
| `panZoomVideo({ assetId, panX, panY, zoomLevel })` | Apply pan/zoom crop | `VideoComposition` |
|
|
129
130
|
| `generateThumbnails({ assetId, times, size? })` | Extract frames | `string[]` (base64) |
|
|
130
131
|
| `exportVideo({ compositionId, quality? })` | Export composition | `ExportResult` |
|
|
131
132
|
| `exportAsset({ assetId, quality? })` | Export asset directly | `ExportResult` |
|
|
@@ -420,6 +421,7 @@ function VideoPlayer({ compositionId }: { compositionId: string }) {
|
|
|
420
421
|
| `onProgress` | `function` | Called periodically with `{ currentTime, duration, progress }` |
|
|
421
422
|
| `onEnd` | `function` | Called when video ends (not called if `loop` is true) |
|
|
422
423
|
| `onError` | `function` | Called on playback error |
|
|
424
|
+
| `onPanZoomChange` | `function` | Called when user pinches/pans with `{ panX, panY, zoomLevel }` |
|
|
423
425
|
|
|
424
426
|
#### Player Methods (via ref)
|
|
425
427
|
|
|
@@ -439,6 +441,205 @@ await playerRef.current?.seek(10.5);
|
|
|
439
441
|
await playerRef.current?.replay();
|
|
440
442
|
```
|
|
441
443
|
|
|
444
|
+
### Pan & Zoom
|
|
445
|
+
|
|
446
|
+
The `TwoStepVideoView` component supports interactive pan and zoom gestures, allowing users to zoom into video content and pan around while zoomed.
|
|
447
|
+
|
|
448
|
+
#### Gesture Controls
|
|
449
|
+
|
|
450
|
+
| Gesture | Action |
|
|
451
|
+
|---------|--------|
|
|
452
|
+
| Pinch (2 fingers) | Zoom in/out (1x to 5x) |
|
|
453
|
+
| Drag (2 fingers) | Pan around when zoomed in |
|
|
454
|
+
|
|
455
|
+
#### Pan/Zoom Events
|
|
456
|
+
|
|
457
|
+
Listen for pan/zoom state changes with the `onPanZoomChange` prop:
|
|
458
|
+
|
|
459
|
+
```tsx
|
|
460
|
+
import { TwoStepVideoView, PanZoomState } from 'expo-twostep-video';
|
|
461
|
+
|
|
462
|
+
function VideoWithZoom() {
|
|
463
|
+
const [panZoom, setPanZoom] = useState<PanZoomState>({
|
|
464
|
+
panX: 0,
|
|
465
|
+
panY: 0,
|
|
466
|
+
zoomLevel: 1,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
return (
|
|
470
|
+
<View>
|
|
471
|
+
<TwoStepVideoView
|
|
472
|
+
assetId={asset.id}
|
|
473
|
+
onPanZoomChange={(e) => setPanZoom(e.nativeEvent)}
|
|
474
|
+
style={{ width: '100%', height: 300 }}
|
|
475
|
+
/>
|
|
476
|
+
|
|
477
|
+
{panZoom.zoomLevel > 1 && (
|
|
478
|
+
<Text>
|
|
479
|
+
Zoom: {panZoom.zoomLevel.toFixed(1)}x |
|
|
480
|
+
Pan: ({panZoom.panX.toFixed(2)}, {panZoom.panY.toFixed(2)})
|
|
481
|
+
</Text>
|
|
482
|
+
)}
|
|
483
|
+
</View>
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
#### Pan/Zoom Methods (via ref)
|
|
489
|
+
|
|
490
|
+
Control pan/zoom programmatically using the player ref:
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
const playerRef = useRef<TwoStepVideoViewRef>(null);
|
|
494
|
+
|
|
495
|
+
// Get current pan/zoom state
|
|
496
|
+
const state = await playerRef.current?.getPanZoomState();
|
|
497
|
+
// Returns: { panX: number, panY: number, zoomLevel: number }
|
|
498
|
+
|
|
499
|
+
// Set pan/zoom programmatically
|
|
500
|
+
await playerRef.current?.setPanZoomState({
|
|
501
|
+
panX: 0.5, // Pan right (range: -1 to 1)
|
|
502
|
+
panY: -0.3, // Pan up (range: -1 to 1)
|
|
503
|
+
zoomLevel: 2.0 // 2x zoom (range: 1 to 5)
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Reset to default (no zoom, centered)
|
|
507
|
+
await playerRef.current?.resetPanZoom();
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
#### Baking Pan/Zoom into Export
|
|
511
|
+
|
|
512
|
+
To permanently apply the pan/zoom transform to a video for export, use `panZoomVideo`:
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
import * as TwoStepVideo from 'expo-twostep-video';
|
|
516
|
+
|
|
517
|
+
// Apply pan/zoom as a permanent transformation
|
|
518
|
+
const composition = await TwoStepVideo.panZoomVideo({
|
|
519
|
+
assetId: asset.id,
|
|
520
|
+
panX: 0.3, // Pan position (-1 to 1)
|
|
521
|
+
panY: -0.2,
|
|
522
|
+
zoomLevel: 1.5, // Zoom level (1 to 5)
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// Export the cropped/zoomed video
|
|
526
|
+
const result = await TwoStepVideo.exportVideo({
|
|
527
|
+
compositionId: composition.id,
|
|
528
|
+
quality: 'high',
|
|
529
|
+
});
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### Complete Pan/Zoom Example
|
|
533
|
+
|
|
534
|
+
A full implementation with gesture preview and export:
|
|
535
|
+
|
|
536
|
+
```tsx
|
|
537
|
+
import React, { useState, useRef } from 'react';
|
|
538
|
+
import { View, Button, Text, StyleSheet } from 'react-native';
|
|
539
|
+
import * as TwoStepVideo from 'expo-twostep-video';
|
|
540
|
+
import {
|
|
541
|
+
TwoStepVideoView,
|
|
542
|
+
TwoStepVideoViewRef,
|
|
543
|
+
PanZoomState,
|
|
544
|
+
} from 'expo-twostep-video';
|
|
545
|
+
|
|
546
|
+
function PanZoomEditor({ assetId }: { assetId: string }) {
|
|
547
|
+
const playerRef = useRef<TwoStepVideoViewRef>(null);
|
|
548
|
+
const [panZoom, setPanZoom] = useState<PanZoomState>({
|
|
549
|
+
panX: 0,
|
|
550
|
+
panY: 0,
|
|
551
|
+
zoomLevel: 1,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const hasTransform = panZoom.zoomLevel > 1 ||
|
|
555
|
+
panZoom.panX !== 0 ||
|
|
556
|
+
panZoom.panY !== 0;
|
|
557
|
+
|
|
558
|
+
const handleReset = async () => {
|
|
559
|
+
await playerRef.current?.resetPanZoom();
|
|
560
|
+
setPanZoom({ panX: 0, panY: 0, zoomLevel: 1 });
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const handleApplyAndExport = async () => {
|
|
564
|
+
// Bake the pan/zoom into a composition
|
|
565
|
+
const composition = await TwoStepVideo.panZoomVideo({
|
|
566
|
+
assetId,
|
|
567
|
+
panX: panZoom.panX,
|
|
568
|
+
panY: panZoom.panY,
|
|
569
|
+
zoomLevel: panZoom.zoomLevel,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Export the result
|
|
573
|
+
const result = await TwoStepVideo.exportVideo({
|
|
574
|
+
compositionId: composition.id,
|
|
575
|
+
quality: 'high',
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// Reset the preview since transform is now baked in
|
|
579
|
+
await handleReset();
|
|
580
|
+
|
|
581
|
+
console.log('Exported to:', result.uri);
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
return (
|
|
585
|
+
<View style={styles.container}>
|
|
586
|
+
{/* Video Preview with Gestures */}
|
|
587
|
+
<TwoStepVideoView
|
|
588
|
+
ref={playerRef}
|
|
589
|
+
assetId={assetId}
|
|
590
|
+
onPanZoomChange={(e) => setPanZoom(e.nativeEvent)}
|
|
591
|
+
style={styles.video}
|
|
592
|
+
/>
|
|
593
|
+
|
|
594
|
+
{/* Gesture Hint */}
|
|
595
|
+
<Text style={styles.hint}>
|
|
596
|
+
Pinch to zoom, drag with 2 fingers to pan
|
|
597
|
+
</Text>
|
|
598
|
+
|
|
599
|
+
{/* Transform Info */}
|
|
600
|
+
{hasTransform && (
|
|
601
|
+
<View style={styles.info}>
|
|
602
|
+
<Text>Zoom: {panZoom.zoomLevel.toFixed(2)}x</Text>
|
|
603
|
+
<Text>Pan X: {panZoom.panX.toFixed(3)}</Text>
|
|
604
|
+
<Text>Pan Y: {panZoom.panY.toFixed(3)}</Text>
|
|
605
|
+
</View>
|
|
606
|
+
)}
|
|
607
|
+
|
|
608
|
+
{/* Controls */}
|
|
609
|
+
<View style={styles.buttons}>
|
|
610
|
+
<Button
|
|
611
|
+
title="Reset"
|
|
612
|
+
onPress={handleReset}
|
|
613
|
+
disabled={!hasTransform}
|
|
614
|
+
/>
|
|
615
|
+
<Button
|
|
616
|
+
title="Apply & Export"
|
|
617
|
+
onPress={handleApplyAndExport}
|
|
618
|
+
disabled={!hasTransform}
|
|
619
|
+
/>
|
|
620
|
+
</View>
|
|
621
|
+
</View>
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const styles = StyleSheet.create({
|
|
626
|
+
container: { flex: 1, padding: 16 },
|
|
627
|
+
video: { width: '100%', height: 300, backgroundColor: '#000' },
|
|
628
|
+
hint: { textAlign: 'center', color: '#666', marginTop: 8 },
|
|
629
|
+
info: { padding: 12, backgroundColor: '#f0f0f0', borderRadius: 8, marginTop: 12 },
|
|
630
|
+
buttons: { flexDirection: 'row', justifyContent: 'space-around', marginTop: 16 },
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
export default PanZoomEditor;
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
#### Pan/Zoom Notes
|
|
637
|
+
|
|
638
|
+
- **Zoom range**: 1x (no zoom) to 5x by default
|
|
639
|
+
- **Pan range**: -1 to 1 on both axes (only effective when zoomed in)
|
|
640
|
+
- **Pan constraint**: Pan is automatically constrained to keep video content visible at the current zoom level
|
|
641
|
+
- **Simulator testing**: Multi-touch gestures work best on real devices. The iOS Simulator uses Option+drag for pinch, which can be unreliable
|
|
642
|
+
|
|
442
643
|
### Events
|
|
443
644
|
|
|
444
645
|
#### `addExportProgressListener(callback)`
|
|
@@ -570,6 +771,7 @@ interface TwoStepVideoViewProps {
|
|
|
570
771
|
onProgress?: (event: { nativeEvent: ProgressEvent }) => void;
|
|
571
772
|
onEnd?: (event: { nativeEvent: {} }) => void;
|
|
572
773
|
onError?: (event: { nativeEvent: ErrorEvent }) => void;
|
|
774
|
+
onPanZoomChange?: (event: { nativeEvent: PanZoomState }) => void;
|
|
573
775
|
style?: ViewStyle;
|
|
574
776
|
}
|
|
575
777
|
|
|
@@ -578,6 +780,25 @@ interface TwoStepVideoViewRef {
|
|
|
578
780
|
pause: () => Promise<void>;
|
|
579
781
|
seek: (time: number) => Promise<void>;
|
|
580
782
|
replay: () => Promise<void>;
|
|
783
|
+
getPanZoomState: () => Promise<PanZoomState>;
|
|
784
|
+
setPanZoomState: (state: Partial<PanZoomState>) => Promise<void>;
|
|
785
|
+
resetPanZoom: () => Promise<void>;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Pan/Zoom types
|
|
789
|
+
interface PanZoomState {
|
|
790
|
+
panX: number; // Horizontal pan (-1 to 1, 0 = center)
|
|
791
|
+
panY: number; // Vertical pan (-1 to 1, 0 = center)
|
|
792
|
+
zoomLevel: number; // Zoom level (1 to 5, 1 = no zoom)
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
interface PanZoomVideoOptions {
|
|
796
|
+
assetId: string;
|
|
797
|
+
panX: number;
|
|
798
|
+
panY: number;
|
|
799
|
+
zoomLevel: number;
|
|
800
|
+
startTime?: number;
|
|
801
|
+
endTime?: number;
|
|
581
802
|
}
|
|
582
803
|
```
|
|
583
804
|
|
|
@@ -20,6 +20,38 @@ export type ProgressEvent = {
|
|
|
20
20
|
export type ErrorEvent = {
|
|
21
21
|
error: string;
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Pan/Zoom state representing the current view transform
|
|
25
|
+
*/
|
|
26
|
+
export type PanZoomState = {
|
|
27
|
+
/** Horizontal pan position (-1.0 to 1.0, 0 = center) */
|
|
28
|
+
panX: number;
|
|
29
|
+
/** Vertical pan position (-1.0 to 1.0, 0 = center) */
|
|
30
|
+
panY: number;
|
|
31
|
+
/** Zoom level (1.0 = no zoom, 2.0 = 2x zoom, etc.) */
|
|
32
|
+
zoomLevel: number;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Pan/Zoom change event payload
|
|
36
|
+
*/
|
|
37
|
+
export type PanZoomChangeEvent = PanZoomState;
|
|
38
|
+
/**
|
|
39
|
+
* Options for creating a pan/zoom composition
|
|
40
|
+
*/
|
|
41
|
+
export type PanZoomVideoOptions = {
|
|
42
|
+
/** ID of the loaded asset */
|
|
43
|
+
assetId: string;
|
|
44
|
+
/** Horizontal pan position (-1.0 to 1.0, 0 = center) */
|
|
45
|
+
panX?: number;
|
|
46
|
+
/** Vertical pan position (-1.0 to 1.0, 0 = center) */
|
|
47
|
+
panY?: number;
|
|
48
|
+
/** Zoom level (1.0 = no zoom, 2.0 = 2x zoom, etc.) */
|
|
49
|
+
zoomLevel?: number;
|
|
50
|
+
/** Optional start time in seconds */
|
|
51
|
+
startTime?: number;
|
|
52
|
+
/** Optional end time in seconds */
|
|
53
|
+
endTime?: number;
|
|
54
|
+
};
|
|
23
55
|
/**
|
|
24
56
|
* Video player view props
|
|
25
57
|
*/
|
|
@@ -30,6 +62,10 @@ export type TwoStepVideoViewProps = {
|
|
|
30
62
|
assetId?: string;
|
|
31
63
|
/** Enable continuous looping - video will restart automatically when it ends */
|
|
32
64
|
loop?: boolean;
|
|
65
|
+
/** Minimum zoom level (default: 1.0) */
|
|
66
|
+
minZoom?: number;
|
|
67
|
+
/** Maximum zoom level (default: 5.0) */
|
|
68
|
+
maxZoom?: number;
|
|
33
69
|
/** Called when playback status changes */
|
|
34
70
|
onPlaybackStatusChange?: (event: {
|
|
35
71
|
nativeEvent: PlaybackStatusEvent;
|
|
@@ -46,6 +82,10 @@ export type TwoStepVideoViewProps = {
|
|
|
46
82
|
onError?: (event: {
|
|
47
83
|
nativeEvent: ErrorEvent;
|
|
48
84
|
}) => void;
|
|
85
|
+
/** Called when pan/zoom gesture changes */
|
|
86
|
+
onPanZoomChange?: (event: {
|
|
87
|
+
nativeEvent: PanZoomChangeEvent;
|
|
88
|
+
}) => void;
|
|
49
89
|
/** View style */
|
|
50
90
|
style?: StyleProp<ViewStyle>;
|
|
51
91
|
};
|
|
@@ -61,6 +101,12 @@ export type TwoStepVideoViewRef = {
|
|
|
61
101
|
seek: (time: number) => Promise<void>;
|
|
62
102
|
/** Restart playback from the beginning */
|
|
63
103
|
replay: () => Promise<void>;
|
|
104
|
+
/** Get the current pan/zoom state */
|
|
105
|
+
getPanZoomState: () => Promise<PanZoomState>;
|
|
106
|
+
/** Set the pan/zoom state programmatically */
|
|
107
|
+
setPanZoomState: (state: Partial<PanZoomState>) => Promise<void>;
|
|
108
|
+
/** Reset pan/zoom to default state (zoom=1, pan=0,0) */
|
|
109
|
+
resetPanZoom: () => Promise<void>;
|
|
64
110
|
};
|
|
65
111
|
/**
|
|
66
112
|
* Video player controller view props (with native controls)
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"ExpoTwoStepVideo.types.d.ts","sourceRoot":"","sources":["../src/ExpoTwoStepVideo.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -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\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"]}
|
|
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 * Pan/Zoom state representing the current view transform\n */\nexport type PanZoomState = {\n /** Horizontal pan position (-1.0 to 1.0, 0 = center) */\n panX: number;\n /** Vertical pan position (-1.0 to 1.0, 0 = center) */\n panY: number;\n /** Zoom level (1.0 = no zoom, 2.0 = 2x zoom, etc.) */\n zoomLevel: number;\n};\n\n/**\n * Pan/Zoom change event payload\n */\nexport type PanZoomChangeEvent = PanZoomState;\n\n/**\n * Options for creating a pan/zoom composition\n */\nexport type PanZoomVideoOptions = {\n /** ID of the loaded asset */\n assetId: string;\n /** Horizontal pan position (-1.0 to 1.0, 0 = center) */\n panX?: number;\n /** Vertical pan position (-1.0 to 1.0, 0 = center) */\n panY?: number;\n /** Zoom level (1.0 = no zoom, 2.0 = 2x zoom, etc.) */\n zoomLevel?: number;\n /** Optional start time in seconds */\n startTime?: number;\n /** Optional end time in seconds */\n endTime?: number;\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 /** Minimum zoom level (default: 1.0) */\n minZoom?: number;\n /** Maximum zoom level (default: 5.0) */\n maxZoom?: number;\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 /** Called when pan/zoom gesture changes */\n onPanZoomChange?: (event: { nativeEvent: PanZoomChangeEvent }) => 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 /** Get the current pan/zoom state */\n getPanZoomState: () => Promise<PanZoomState>;\n /** Set the pan/zoom state programmatically */\n setPanZoomState: (state: Partial<PanZoomState>) => Promise<void>;\n /** Reset pan/zoom to default state (zoom=1, pan=0,0) */\n resetPanZoom: () => 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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoTwoStepVideoView.d.ts","sourceRoot":"","sources":["../src/ExpoTwoStepVideoView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,
|
|
1
|
+
{"version":3,"file":"ExpoTwoStepVideoView.d.ts","sourceRoot":"","sources":["../src/ExpoTwoStepVideoView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,EAAgB,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAoBpG;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,QAAA,MAAM,gBAAgB,mGA+BrB,CAAC;AAIF,eAAe,gBAAgB,CAAC"}
|
|
@@ -39,6 +39,16 @@ const TwoStepVideoView = forwardRef((props, ref) => {
|
|
|
39
39
|
replay: async () => {
|
|
40
40
|
await nativeRef.current?.replay();
|
|
41
41
|
},
|
|
42
|
+
getPanZoomState: async () => {
|
|
43
|
+
const state = await nativeRef.current?.getPanZoomState();
|
|
44
|
+
return state ?? { panX: 0, panY: 0, zoomLevel: 1 };
|
|
45
|
+
},
|
|
46
|
+
setPanZoomState: async (state) => {
|
|
47
|
+
await nativeRef.current?.setPanZoomState(state.panX, state.panY, state.zoomLevel);
|
|
48
|
+
},
|
|
49
|
+
resetPanZoom: async () => {
|
|
50
|
+
await nativeRef.current?.resetPanZoom();
|
|
51
|
+
},
|
|
42
52
|
}));
|
|
43
53
|
return <NativeView ref={nativeRef} {...props}/>;
|
|
44
54
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoTwoStepVideoView.js","sourceRoot":"","sources":["../src/ExpoTwoStepVideoView.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;
|
|
1
|
+
{"version":3,"file":"ExpoTwoStepVideoView.js","sourceRoot":"","sources":["../src/ExpoTwoStepVideoView.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;AAoBhE,MAAM,UAAU,GAAyC,iBAAiB,CAAC,kBAAkB,CAAC,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,gBAAgB,GAAG,UAAU,CACjC,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,KAAK,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,CAAC;YACzD,OAAO,KAAK,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;QACrD,CAAC;QACD,eAAe,EAAE,KAAK,EAAE,KAA4B,EAAE,EAAE;YACtD,MAAM,SAAS,CAAC,OAAO,EAAE,eAAe,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;QACpF,CAAC;QACD,YAAY,EAAE,KAAK,IAAI,EAAE;YACvB,MAAM,SAAS,CAAC,OAAO,EAAE,YAAY,EAAE,CAAC;QAC1C,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,gBAAgB,CAAC,WAAW,GAAG,kBAAkB,CAAC;AAElD,eAAe,gBAAgB,CAAC","sourcesContent":["import { requireNativeView } from 'expo';\nimport * as React from 'react';\nimport { forwardRef, useImperativeHandle, useRef } from 'react';\n\nimport { PanZoomState, TwoStepVideoViewProps, TwoStepVideoViewRef } from './ExpoTwoStepVideo.types';\n\n// Internal props type that includes the native ref\ntype NativeViewProps = TwoStepVideoViewProps & {\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 getPanZoomState: () => Promise<PanZoomState>;\n setPanZoomState: (panX?: number, panY?: number, zoomLevel?: number) => Promise<void>;\n resetPanZoom: () => Promise<void>;\n};\n\nconst NativeView: React.ComponentType<NativeViewProps> = requireNativeView('ExpoTwoStepVideo');\n\n/**\n * Video player view that can play compositions and assets directly\n *\n * @example\n * ```tsx\n * const playerRef = useRef<TwoStepVideoViewRef>(null);\n *\n * // Play a composition (from mirrorVideo, trimVideo, etc.)\n * <TwoStepVideoView\n * ref={playerRef}\n * compositionId={composition.id}\n * onPlaybackStatusChange={(e) => console.log(e.nativeEvent.status)}\n * onProgress={(e) => setProgress(e.nativeEvent.progress)}\n * style={{ width: '100%', height: 300 }}\n * />\n *\n * // Control playback\n * playerRef.current?.play();\n * playerRef.current?.pause();\n * playerRef.current?.seek(5.0);\n * ```\n */\nconst TwoStepVideoView = forwardRef<TwoStepVideoViewRef, TwoStepVideoViewProps>(\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 getPanZoomState: async () => {\n const state = await nativeRef.current?.getPanZoomState();\n return state ?? { panX: 0, panY: 0, zoomLevel: 1 };\n },\n setPanZoomState: async (state: Partial<PanZoomState>) => {\n await nativeRef.current?.setPanZoomState(state.panX, state.panY, state.zoomLevel);\n },\n resetPanZoom: async () => {\n await nativeRef.current?.resetPanZoom();\n },\n }));\n\n return <NativeView ref={nativeRef} {...props} />;\n }\n);\n\nTwoStepVideoView.displayName = 'TwoStepVideoView';\n\nexport default TwoStepVideoView;\n"]}
|
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"TwoStepPlayerControllerView.d.ts","sourceRoot":"","sources":["../src/TwoStepPlayerControllerView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,EAAkD,8BAA8B,EAAE,MAAM,0BAA0B,CAAC;AAmB1H;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,QAAA,MAAM,2BAA2B;;wDAqChC,CAAC;AAIF,eAAe,2BAA2B,CAAC"}
|
|
@@ -46,6 +46,16 @@ const TwoStepPlayerControllerView = forwardRef((props, ref) => {
|
|
|
46
46
|
exitFullscreen: async () => {
|
|
47
47
|
await nativeRef.current?.exitFullscreen();
|
|
48
48
|
},
|
|
49
|
+
// Pan/zoom not supported on controller view (uses AVPlayerViewController)
|
|
50
|
+
getPanZoomState: async () => {
|
|
51
|
+
return { panX: 0, panY: 0, zoomLevel: 1 };
|
|
52
|
+
},
|
|
53
|
+
setPanZoomState: async (_state) => {
|
|
54
|
+
// Not supported on controller view
|
|
55
|
+
},
|
|
56
|
+
resetPanZoom: async () => {
|
|
57
|
+
// Not supported on controller view
|
|
58
|
+
},
|
|
49
59
|
}));
|
|
50
60
|
return <NativeView ref={nativeRef} {...props}/>;
|
|
51
61
|
});
|
|
@@ -1 +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"]}
|
|
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;QACD,0EAA0E;QAC1E,eAAe,EAAE,KAAK,IAA2B,EAAE;YACjD,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;QAC5C,CAAC;QACD,eAAe,EAAE,KAAK,EAAE,MAA6B,EAAE,EAAE;YACvD,mCAAmC;QACrC,CAAC;QACD,YAAY,EAAE,KAAK,IAAI,EAAE;YACvB,mCAAmC;QACrC,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 { PanZoomState, 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 // Pan/zoom not supported on controller view (uses AVPlayerViewController)\n getPanZoomState: async (): Promise<PanZoomState> => {\n return { panX: 0, panY: 0, zoomLevel: 1 };\n },\n setPanZoomState: async (_state: Partial<PanZoomState>) => {\n // Not supported on controller view\n },\n resetPanZoom: async () => {\n // Not supported on controller view\n },\n }));\n\n return <NativeView ref={nativeRef} {...props} />;\n }\n);\n\nTwoStepPlayerControllerView.displayName = 'TwoStepPlayerControllerView';\n\nexport default TwoStepPlayerControllerView;\n"]}
|
package/build/index.d.ts
CHANGED
|
@@ -411,6 +411,57 @@ export declare function transformVideo(options: TransformVideoOptions): Promise<
|
|
|
411
411
|
* ```
|
|
412
412
|
*/
|
|
413
413
|
export declare function loopSegment(options: LoopSegmentOptions): Promise<LoopResult>;
|
|
414
|
+
/**
|
|
415
|
+
* Options for pan and zoom transformation
|
|
416
|
+
*/
|
|
417
|
+
export interface PanZoomVideoOptions {
|
|
418
|
+
/** Asset ID to transform */
|
|
419
|
+
assetId: string;
|
|
420
|
+
/** Horizontal pan position (-1.0 to 1.0, 0 = center) */
|
|
421
|
+
panX?: number;
|
|
422
|
+
/** Vertical pan position (-1.0 to 1.0, 0 = center) */
|
|
423
|
+
panY?: number;
|
|
424
|
+
/** Zoom level (1.0 = no zoom, 2.0 = 2x zoom, etc.) */
|
|
425
|
+
zoomLevel?: number;
|
|
426
|
+
/** Optional start time in seconds (for trimming) */
|
|
427
|
+
startTime?: number;
|
|
428
|
+
/** Optional end time in seconds (for trimming) */
|
|
429
|
+
endTime?: number;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Apply pan and zoom transformation to a video
|
|
433
|
+
*
|
|
434
|
+
* This creates a composition with the pan/zoom baked in for export.
|
|
435
|
+
* For real-time preview, use the gesture controls on TwoStepVideoView.
|
|
436
|
+
*
|
|
437
|
+
* @param options - Pan/zoom options including asset ID and transform values
|
|
438
|
+
* @returns Promise resolving to composition metadata
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```typescript
|
|
442
|
+
* // Get current pan/zoom from player and apply to export
|
|
443
|
+
* const panZoomState = await playerRef.current.getPanZoomState();
|
|
444
|
+
* const composition = await TwoStepVideo.panZoomVideo({
|
|
445
|
+
* assetId: asset.id,
|
|
446
|
+
* panX: panZoomState.panX,
|
|
447
|
+
* panY: panZoomState.panY,
|
|
448
|
+
* zoomLevel: panZoomState.zoomLevel
|
|
449
|
+
* });
|
|
450
|
+
*
|
|
451
|
+
* // Export the pan/zoomed video
|
|
452
|
+
* const result = await TwoStepVideo.exportVideo({
|
|
453
|
+
* compositionId: composition.id,
|
|
454
|
+
* quality: 'high'
|
|
455
|
+
* });
|
|
456
|
+
*
|
|
457
|
+
* // Apply zoom only (no pan, centered)
|
|
458
|
+
* const zoomed = await TwoStepVideo.panZoomVideo({
|
|
459
|
+
* assetId: asset.id,
|
|
460
|
+
* zoomLevel: 2.0 // 2x zoom, centered
|
|
461
|
+
* });
|
|
462
|
+
* ```
|
|
463
|
+
*/
|
|
464
|
+
export declare function panZoomVideo(options: PanZoomVideoOptions): Promise<VideoComposition>;
|
|
414
465
|
/**
|
|
415
466
|
* Generate thumbnail images from a video at specific times
|
|
416
467
|
*
|
|
@@ -556,6 +607,7 @@ declare const _default: {
|
|
|
556
607
|
adjustSpeed: typeof adjustSpeed;
|
|
557
608
|
transformVideo: typeof transformVideo;
|
|
558
609
|
loopSegment: typeof loopSegment;
|
|
610
|
+
panZoomVideo: typeof panZoomVideo;
|
|
559
611
|
cleanupFile: typeof cleanupFile;
|
|
560
612
|
releaseAsset: typeof releaseAsset;
|
|
561
613
|
releaseComposition: typeof releaseComposition;
|
|
@@ -568,6 +620,6 @@ export { default as TwoStepVideoView } from './ExpoTwoStepVideoView';
|
|
|
568
620
|
export { default as TwoStepPlayerControllerView } from './TwoStepPlayerControllerView';
|
|
569
621
|
export { default as VideoScrubber } from './VideoScrubber';
|
|
570
622
|
export { useVideoScrubber } from './hooks/useVideoScrubber';
|
|
571
|
-
export type { TwoStepVideoViewProps, TwoStepVideoViewRef, TwoStepPlayerControllerViewProps, TwoStepPlayerControllerViewRef, PlaybackStatusEvent, ProgressEvent, ErrorEvent, } from './ExpoTwoStepVideo.types';
|
|
623
|
+
export type { TwoStepVideoViewProps, TwoStepVideoViewRef, TwoStepPlayerControllerViewProps, TwoStepPlayerControllerViewRef, PlaybackStatusEvent, ProgressEvent, ErrorEvent, PanZoomState, PanZoomChangeEvent, } from './ExpoTwoStepVideo.types';
|
|
572
624
|
export type { VideoScrubberProps, VideoScrubberRef, VideoScrubberTheme, } from './VideoScrubber.types';
|
|
573
625
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/build/index.js
CHANGED
|
@@ -238,6 +238,42 @@ export async function transformVideo(options) {
|
|
|
238
238
|
export async function loopSegment(options) {
|
|
239
239
|
return await ExpoTwoStepVideoModule.loopSegment(options.assetId, options.startTime, options.endTime, options.loopCount);
|
|
240
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Apply pan and zoom transformation to a video
|
|
243
|
+
*
|
|
244
|
+
* This creates a composition with the pan/zoom baked in for export.
|
|
245
|
+
* For real-time preview, use the gesture controls on TwoStepVideoView.
|
|
246
|
+
*
|
|
247
|
+
* @param options - Pan/zoom options including asset ID and transform values
|
|
248
|
+
* @returns Promise resolving to composition metadata
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```typescript
|
|
252
|
+
* // Get current pan/zoom from player and apply to export
|
|
253
|
+
* const panZoomState = await playerRef.current.getPanZoomState();
|
|
254
|
+
* const composition = await TwoStepVideo.panZoomVideo({
|
|
255
|
+
* assetId: asset.id,
|
|
256
|
+
* panX: panZoomState.panX,
|
|
257
|
+
* panY: panZoomState.panY,
|
|
258
|
+
* zoomLevel: panZoomState.zoomLevel
|
|
259
|
+
* });
|
|
260
|
+
*
|
|
261
|
+
* // Export the pan/zoomed video
|
|
262
|
+
* const result = await TwoStepVideo.exportVideo({
|
|
263
|
+
* compositionId: composition.id,
|
|
264
|
+
* quality: 'high'
|
|
265
|
+
* });
|
|
266
|
+
*
|
|
267
|
+
* // Apply zoom only (no pan, centered)
|
|
268
|
+
* const zoomed = await TwoStepVideo.panZoomVideo({
|
|
269
|
+
* assetId: asset.id,
|
|
270
|
+
* zoomLevel: 2.0 // 2x zoom, centered
|
|
271
|
+
* });
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
export async function panZoomVideo(options) {
|
|
275
|
+
return await ExpoTwoStepVideoModule.panZoomVideo(options.assetId, options.panX ?? 0, options.panY ?? 0, options.zoomLevel ?? 1.0, options.startTime, options.endTime);
|
|
276
|
+
}
|
|
241
277
|
/**
|
|
242
278
|
* Generate thumbnail images from a video at specific times
|
|
243
279
|
*
|
|
@@ -415,6 +451,7 @@ export default {
|
|
|
415
451
|
adjustSpeed,
|
|
416
452
|
transformVideo,
|
|
417
453
|
loopSegment,
|
|
454
|
+
panZoomVideo,
|
|
418
455
|
// Memory management
|
|
419
456
|
cleanupFile,
|
|
420
457
|
releaseAsset,
|
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 ExpoTwoStepVideoModule from './ExpoTwoStepVideoModule';\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/**\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] = React.useState(0);\n\n React.useEffect(() => {\n const subscription = addExportProgressListener((event) => {\n setProgress(event.progress);\n });\n\n return () => subscription.remove();\n }, []);\n\n return progress;\n}\n\n// Note: React import needed for the hook\n// If React is not available, the hook won't be usable but\n// the rest of the module will work fine\nlet React: any;\ntry {\n React = require('react');\n} catch {\n // React not available, hook won't work\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\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 TwoStepVideoViewProps,\n TwoStepVideoViewRef,\n TwoStepPlayerControllerViewProps,\n TwoStepPlayerControllerViewRef,\n PlaybackStatusEvent,\n ProgressEvent,\n ErrorEvent,\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":"AAAA;;;GAGG;AAGH,OAAO,sBAAsB,MAAM,0BAA0B,CAAC;AAuN9D,oBAAoB;AAEpB;;GAEG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,GAAG,EAAE,sBAAsB,CAAC,WAAW;IACvC,MAAM,EAAE,sBAAsB,CAAC,cAAc;IAC7C,IAAI,EAAE,sBAAsB,CAAC,YAAY;IACzC,OAAO,EAAE,sBAAsB,CAAC,eAAe;CACvC,CAAC;AAEX;;GAEG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,UAAU,EAAE,sBAAsB,CAAC,iBAAiB;IACpD,QAAQ,EAAE,sBAAsB,CAAC,eAAe;IAChD,IAAI,EAAE,sBAAsB,CAAC,WAAW;CAChC,CAAC;AAEX,wBAAwB;AAExB;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAyB;IACvD,OAAO,MAAM,sBAAsB,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,eAAuB;IAC/D,OAAO,MAAM,sBAAsB,CAAC,mBAAmB,CAAC,eAAe,CAAC,CAAC;AAC3E,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,GAAW;IAChD,OAAO,MAAM,sBAAsB,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAyB;IACvD,OAAO,MAAM,sBAAsB,CAAC,SAAS,CAC3C,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,OAAO,CAChB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,OAA4B;IAClE,OAAO,MAAM,sBAAsB,CAAC,iBAAiB,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;AAC3F,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAA2B;IAC3D,OAAO,MAAM,sBAAsB,CAAC,WAAW,CAC7C,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,OAAO,CAChB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAA2B;IAC3D,OAAO,MAAM,sBAAsB,CAAC,WAAW,CAC7C,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,KAAK,EACb,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,OAAO,CAChB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAA8B;IACjE,OAAO,MAAM,sBAAsB,CAAC,cAAc,CAChD,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,KAAK,EACb,OAAO,CAAC,UAAU,EAClB,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,OAAO,CAChB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAA2B;IAC3D,OAAO,MAAM,sBAAsB,CAAC,WAAW,CAC7C,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,SAAS,CAClB,CAAC;AACJ,CAAC;AAoBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAA4B;IAC7D,OAAO,MAAM,sBAAsB,CAAC,YAAY,CAC9C,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,IAAI,IAAI,CAAC,EACjB,OAAO,CAAC,IAAI,IAAI,CAAC,EACjB,OAAO,CAAC,SAAS,IAAI,GAAG,EACxB,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,OAAO,CAChB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAAkC;IAElC,OAAO,MAAM,sBAAsB,CAAC,kBAAkB,CACpD,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,KAAK,EACb,OAAO,CAAC,IAAI,CACb,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAiC;IACjE,OAAO,MAAM,sBAAsB,CAAC,WAAW,CAC7C,OAAO,CAAC,aAAa,EACrB,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,OAAO,CAChB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAA2B;IAC3D,OAAO,MAAM,sBAAsB,CAAC,WAAW,CAC7C,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,OAAO,CAChB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,sBAAsB,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,sBAAsB,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;AAC/C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,aAAqB;IACtD,sBAAsB,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;AAC3D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU;IACxB,sBAAsB,CAAC,UAAU,EAAE,CAAC;AACtC,CAAC;AAED,0BAA0B;AAE1B;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,yBAAyB,CACvC,QAA8C;IAE9C,OAAO,sBAAsB,CAAC,WAAW,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAC;AAC1E,CAAC;AAED,kCAAkC;AAElC;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAElD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,YAAY,GAAG,yBAAyB,CAAC,CAAC,KAAK,EAAE,EAAE;YACvD,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC;IACrC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,yCAAyC;AACzC,0DAA0D;AAC1D,wCAAwC;AACxC,IAAI,KAAU,CAAC;AACf,IAAI,CAAC;IACH,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAC3B,CAAC;AAAC,MAAM,CAAC;IACP,uCAAuC;AACzC,CAAC;AAED,kBAAkB;AAElB,eAAe;IACb,YAAY;IACZ,OAAO;IACP,MAAM;IAEN,iBAAiB;IACjB,SAAS;IACT,mBAAmB;IACnB,gBAAgB;IAChB,SAAS;IACT,iBAAiB;IACjB,kBAAkB;IAClB,WAAW;IACX,WAAW;IAEX,2BAA2B;IAC3B,WAAW;IACX,WAAW;IACX,cAAc;IACd,WAAW;IACX,YAAY;IAEZ,oBAAoB;IACpB,WAAW;IACX,YAAY;IACZ,kBAAkB;IAClB,UAAU;IAEV,SAAS;IACT,yBAAyB;IAEzB,aAAa;IACb,iBAAiB;CAClB,CAAC;AAEF,gCAAgC;AAEhC,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC","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 ExpoTwoStepVideoModule from './ExpoTwoStepVideoModule';\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/**\n * Options for pan and zoom transformation\n */\nexport interface PanZoomVideoOptions {\n /** Asset ID to transform */\n assetId: string;\n /** Horizontal pan position (-1.0 to 1.0, 0 = center) */\n panX?: number;\n /** Vertical pan position (-1.0 to 1.0, 0 = center) */\n panY?: number;\n /** Zoom level (1.0 = no zoom, 2.0 = 2x zoom, etc.) */\n zoomLevel?: number;\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 * 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] = React.useState(0);\n\n React.useEffect(() => {\n const subscription = addExportProgressListener((event) => {\n setProgress(event.progress);\n });\n\n return () => subscription.remove();\n }, []);\n\n return progress;\n}\n\n// Note: React import needed for the hook\n// If React is not available, the hook won't be usable but\n// the rest of the module will work fine\nlet React: any;\ntry {\n React = require('react');\n} catch {\n // React not available, hook won't work\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 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"]}
|
|
@@ -404,6 +404,53 @@ public class ExpoTwoStepVideoModule: Module {
|
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
/// Apply pan and zoom to a video
|
|
408
|
+
/// - Parameters:
|
|
409
|
+
/// - assetId: ID of the loaded asset
|
|
410
|
+
/// - panX: Horizontal pan (-1.0 to 1.0, 0 = center)
|
|
411
|
+
/// - panY: Vertical pan (-1.0 to 1.0, 0 = center)
|
|
412
|
+
/// - zoomLevel: Zoom level (1.0 = 100%, 2.0 = 200%, etc.)
|
|
413
|
+
/// - startTime: Optional start time in seconds
|
|
414
|
+
/// - endTime: Optional end time in seconds
|
|
415
|
+
/// - Returns: Composition ID
|
|
416
|
+
AsyncFunction("panZoomVideo") { (assetId: String, panX: Double, panY: Double, zoomLevel: Double, startTime: Double?, endTime: Double?, promise: Promise) in
|
|
417
|
+
Task {
|
|
418
|
+
do {
|
|
419
|
+
guard let asset = self.activeAssets[assetId] else {
|
|
420
|
+
promise.reject("ASSET_NOT_FOUND", "Asset not found: \(assetId)")
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Parse optional time range
|
|
425
|
+
var timeRange: TimeRange? = nil
|
|
426
|
+
if let start = startTime, let end = endTime {
|
|
427
|
+
timeRange = try TimeRange.fromSeconds(start: start, end: end)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let (composition, videoComposition) = try await self.twoStep.videoTransformer.panZoom(
|
|
431
|
+
asset: asset,
|
|
432
|
+
panX: CGFloat(panX),
|
|
433
|
+
panY: CGFloat(panY),
|
|
434
|
+
zoomLevel: CGFloat(zoomLevel),
|
|
435
|
+
timeRange: timeRange
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
let compositionId = UUID().uuidString
|
|
439
|
+
self.activeCompositions[compositionId] = composition
|
|
440
|
+
self.activeVideoCompositions[compositionId] = videoComposition
|
|
441
|
+
|
|
442
|
+
promise.resolve([
|
|
443
|
+
"id": compositionId,
|
|
444
|
+
"duration": CMTimeGetSeconds(composition.duration)
|
|
445
|
+
])
|
|
446
|
+
} catch let error as VideoEditingError {
|
|
447
|
+
promise.reject("PAN_ZOOM_FAILED", error.localizedDescription)
|
|
448
|
+
} catch {
|
|
449
|
+
promise.reject("UNKNOWN_ERROR", error.localizedDescription)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
407
454
|
/// Generate thumbnails from a video at specific times
|
|
408
455
|
/// - Parameters:
|
|
409
456
|
/// - assetId: ID of the loaded asset
|
|
@@ -624,7 +671,7 @@ public class ExpoTwoStepVideoModule: Module {
|
|
|
624
671
|
|
|
625
672
|
View(ExpoTwoStepVideoView.self) {
|
|
626
673
|
// Events emitted by the view
|
|
627
|
-
Events("onPlaybackStatusChange", "onProgress", "onEnd", "onError")
|
|
674
|
+
Events("onPlaybackStatusChange", "onProgress", "onEnd", "onError", "onPanZoomChange")
|
|
628
675
|
|
|
629
676
|
// Prop to set composition ID - view will load it
|
|
630
677
|
Prop("compositionId") { (view: ExpoTwoStepVideoView, compositionId: String?) in
|
|
@@ -652,6 +699,16 @@ public class ExpoTwoStepVideoModule: Module {
|
|
|
652
699
|
view.shouldLoop = loop ?? false
|
|
653
700
|
}
|
|
654
701
|
|
|
702
|
+
// Prop to set minimum zoom level
|
|
703
|
+
Prop("minZoom") { (view: ExpoTwoStepVideoView, minZoom: Double?) in
|
|
704
|
+
view.minZoom = CGFloat(minZoom ?? 1.0)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Prop to set maximum zoom level
|
|
708
|
+
Prop("maxZoom") { (view: ExpoTwoStepVideoView, maxZoom: Double?) in
|
|
709
|
+
view.maxZoom = CGFloat(maxZoom ?? 5.0)
|
|
710
|
+
}
|
|
711
|
+
|
|
655
712
|
// Playback control functions
|
|
656
713
|
AsyncFunction("play") { (view: ExpoTwoStepVideoView) in
|
|
657
714
|
view.play()
|
|
@@ -668,6 +725,23 @@ public class ExpoTwoStepVideoModule: Module {
|
|
|
668
725
|
AsyncFunction("replay") { (view: ExpoTwoStepVideoView) in
|
|
669
726
|
view.replay()
|
|
670
727
|
}
|
|
728
|
+
|
|
729
|
+
// Pan/Zoom control functions
|
|
730
|
+
AsyncFunction("getPanZoomState") { (view: ExpoTwoStepVideoView) -> [String: CGFloat] in
|
|
731
|
+
return view.getPanZoomState()
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
AsyncFunction("setPanZoomState") { (view: ExpoTwoStepVideoView, panX: Double?, panY: Double?, zoomLevel: Double?) in
|
|
735
|
+
view.setPanZoomState(
|
|
736
|
+
panX: panX.map { CGFloat($0) },
|
|
737
|
+
panY: panY.map { CGFloat($0) },
|
|
738
|
+
zoomLevel: zoomLevel.map { CGFloat($0) }
|
|
739
|
+
)
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
AsyncFunction("resetPanZoom") { (view: ExpoTwoStepVideoView) in
|
|
743
|
+
view.resetPanZoom()
|
|
744
|
+
}
|
|
671
745
|
}
|
|
672
746
|
|
|
673
747
|
// MARK: - Video Player Controller View (with native controls)
|
|
@@ -18,6 +18,7 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
18
18
|
let onProgress = EventDispatcher()
|
|
19
19
|
let onEnd = EventDispatcher()
|
|
20
20
|
let onError = EventDispatcher()
|
|
21
|
+
let onPanZoomChange = EventDispatcher()
|
|
21
22
|
|
|
22
23
|
/// Current composition ID being played
|
|
23
24
|
private var currentCompositionId: String?
|
|
@@ -26,6 +27,52 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
26
27
|
/// Whether to loop playback continuously
|
|
27
28
|
var shouldLoop: Bool = false
|
|
28
29
|
|
|
30
|
+
// MARK: - Pan/Zoom Properties
|
|
31
|
+
|
|
32
|
+
/// Current zoom level (1.0 = no zoom, 2.0 = 2x zoom, etc.)
|
|
33
|
+
private var currentZoom: CGFloat = 1.0
|
|
34
|
+
|
|
35
|
+
/// Current horizontal pan (-1.0 to 1.0, 0 = center)
|
|
36
|
+
private var currentPanX: CGFloat = 0.0
|
|
37
|
+
|
|
38
|
+
/// Current vertical pan (-1.0 to 1.0, 0 = center)
|
|
39
|
+
private var currentPanY: CGFloat = 0.0
|
|
40
|
+
|
|
41
|
+
/// Last pinch scale for incremental zoom calculation
|
|
42
|
+
private var lastPinchScale: CGFloat = 1.0
|
|
43
|
+
|
|
44
|
+
/// Starting pan position when gesture begins
|
|
45
|
+
private var panStartPosition: CGPoint = .zero
|
|
46
|
+
|
|
47
|
+
/// Minimum zoom level (configurable)
|
|
48
|
+
var minZoom: CGFloat = 1.0
|
|
49
|
+
|
|
50
|
+
/// Maximum zoom level (configurable)
|
|
51
|
+
var maxZoom: CGFloat = 5.0
|
|
52
|
+
|
|
53
|
+
/// Gesture recognizers
|
|
54
|
+
private var pinchGesture: UIPinchGestureRecognizer?
|
|
55
|
+
private var panGesture: UIPanGestureRecognizer?
|
|
56
|
+
|
|
57
|
+
// MARK: - Pan/Zoom Helpers
|
|
58
|
+
|
|
59
|
+
/// Maximum pan amount allowed at current zoom level
|
|
60
|
+
/// At zoom 2x, this is 0.5 (can pan halfway). At zoom 1x, this is 0.
|
|
61
|
+
private var maxPanAmount: CGFloat {
|
|
62
|
+
guard currentZoom > 1.0 else { return 0 }
|
|
63
|
+
return (currentZoom - 1.0) / currentZoom
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Clamp a value between min and max bounds
|
|
67
|
+
private func clamp(_ value: CGFloat, min minValue: CGFloat, max maxValue: CGFloat) -> CGFloat {
|
|
68
|
+
return Swift.min(Swift.max(value, minValue), maxValue)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Whether pan/zoom is at the default (untransformed) state
|
|
72
|
+
private var isAtDefaultTransform: Bool {
|
|
73
|
+
return currentZoom == 1.0 && currentPanX == 0 && currentPanY == 0
|
|
74
|
+
}
|
|
75
|
+
|
|
29
76
|
// MARK: - Initialization
|
|
30
77
|
|
|
31
78
|
required init(appContext: AppContext? = nil) {
|
|
@@ -35,6 +82,7 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
35
82
|
setupPlayerLayer()
|
|
36
83
|
setupAudioSession()
|
|
37
84
|
setupAudioSessionObservers()
|
|
85
|
+
setupGestureRecognizers()
|
|
38
86
|
}
|
|
39
87
|
|
|
40
88
|
private func setupPlayerLayer() {
|
|
@@ -127,9 +175,163 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
127
175
|
}
|
|
128
176
|
}
|
|
129
177
|
|
|
178
|
+
// MARK: - Gesture Setup
|
|
179
|
+
|
|
180
|
+
private func setupGestureRecognizers() {
|
|
181
|
+
// Pinch to zoom
|
|
182
|
+
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:)))
|
|
183
|
+
pinch.delegate = self
|
|
184
|
+
addGestureRecognizer(pinch)
|
|
185
|
+
pinchGesture = pinch
|
|
186
|
+
|
|
187
|
+
// Pan with 2 fingers (to avoid conflict with single-finger scrubbing)
|
|
188
|
+
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
|
|
189
|
+
pan.minimumNumberOfTouches = 2
|
|
190
|
+
pan.maximumNumberOfTouches = 2
|
|
191
|
+
pan.delegate = self
|
|
192
|
+
addGestureRecognizer(pan)
|
|
193
|
+
panGesture = pan
|
|
194
|
+
|
|
195
|
+
// Enable user interaction
|
|
196
|
+
isUserInteractionEnabled = true
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@objc private func handlePinchGesture(_ gesture: UIPinchGestureRecognizer) {
|
|
200
|
+
switch gesture.state {
|
|
201
|
+
case .began:
|
|
202
|
+
lastPinchScale = 1.0
|
|
203
|
+
case .changed:
|
|
204
|
+
// Calculate incremental scale change from last gesture update
|
|
205
|
+
let scaleChange = gesture.scale / lastPinchScale
|
|
206
|
+
lastPinchScale = gesture.scale
|
|
207
|
+
|
|
208
|
+
// Apply zoom with clamping to valid range
|
|
209
|
+
currentZoom = clamp(currentZoom * scaleChange, min: minZoom, max: maxZoom)
|
|
210
|
+
|
|
211
|
+
// Constrain pan (may need adjustment when zoom decreases)
|
|
212
|
+
constrainPan()
|
|
213
|
+
applyTransformAndNotify()
|
|
214
|
+
case .ended, .cancelled:
|
|
215
|
+
lastPinchScale = 1.0
|
|
216
|
+
default:
|
|
217
|
+
break
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
|
|
222
|
+
// Only allow panning when zoomed in
|
|
223
|
+
guard currentZoom > 1.0 else { return }
|
|
224
|
+
|
|
225
|
+
switch gesture.state {
|
|
226
|
+
case .began:
|
|
227
|
+
panStartPosition = CGPoint(x: currentPanX, y: currentPanY)
|
|
228
|
+
case .changed:
|
|
229
|
+
let translation = gesture.translation(in: self)
|
|
230
|
+
|
|
231
|
+
// Convert screen translation to normalized pan delta
|
|
232
|
+
// Dragging full width changes pan by 2x the available pan range
|
|
233
|
+
let panDelta = CGPoint(
|
|
234
|
+
x: (translation.x / bounds.width) * 2 * maxPanAmount,
|
|
235
|
+
y: (translation.y / bounds.height) * 2 * maxPanAmount
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
// Apply delta from start position (negative because dragging right shows left content)
|
|
239
|
+
currentPanX = panStartPosition.x - panDelta.x
|
|
240
|
+
currentPanY = panStartPosition.y - panDelta.y
|
|
241
|
+
|
|
242
|
+
constrainPan()
|
|
243
|
+
applyTransformAndNotify()
|
|
244
|
+
case .ended, .cancelled:
|
|
245
|
+
break
|
|
246
|
+
default:
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// Constrain pan values to keep content visible at current zoom level
|
|
252
|
+
private func constrainPan() {
|
|
253
|
+
let limit = maxPanAmount
|
|
254
|
+
currentPanX = clamp(currentPanX, min: -limit, max: limit)
|
|
255
|
+
currentPanY = clamp(currentPanY, min: -limit, max: limit)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// Apply transform to layer and notify JS of the change
|
|
259
|
+
private func applyTransformAndNotify() {
|
|
260
|
+
updateLayerTransform()
|
|
261
|
+
emitPanZoomChange()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// Apply the current pan/zoom transform to the player layer
|
|
265
|
+
private func updateLayerTransform() {
|
|
266
|
+
guard let layer = playerLayer else { return }
|
|
267
|
+
|
|
268
|
+
// Start with identity and apply zoom
|
|
269
|
+
var transform = CATransform3DScale(CATransform3DIdentity, currentZoom, currentZoom, 1.0)
|
|
270
|
+
|
|
271
|
+
// Apply pan translation (scaled for zoomed coordinate space)
|
|
272
|
+
// Formula: translate by half the extra visible area in the zoom direction
|
|
273
|
+
let translateX = -currentPanX * bounds.width * maxPanAmount / 2.0
|
|
274
|
+
let translateY = -currentPanY * bounds.height * maxPanAmount / 2.0
|
|
275
|
+
transform = CATransform3DTranslate(transform, translateX, translateY, 0)
|
|
276
|
+
|
|
277
|
+
// Apply without implicit animations for responsive feel
|
|
278
|
+
CATransaction.begin()
|
|
279
|
+
CATransaction.setDisableActions(true)
|
|
280
|
+
layer.transform = transform
|
|
281
|
+
CATransaction.commit()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// Emit the current pan/zoom state to JavaScript
|
|
285
|
+
private func emitPanZoomChange() {
|
|
286
|
+
onPanZoomChange([
|
|
287
|
+
"panX": currentPanX,
|
|
288
|
+
"panY": currentPanY,
|
|
289
|
+
"zoomLevel": currentZoom
|
|
290
|
+
])
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// MARK: - Public Pan/Zoom Methods
|
|
294
|
+
|
|
295
|
+
/// Get the current pan/zoom state
|
|
296
|
+
func getPanZoomState() -> [String: CGFloat] {
|
|
297
|
+
return [
|
|
298
|
+
"panX": currentPanX,
|
|
299
|
+
"panY": currentPanY,
|
|
300
|
+
"zoomLevel": currentZoom
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// Set the pan/zoom state programmatically
|
|
305
|
+
func setPanZoomState(panX: CGFloat?, panY: CGFloat?, zoomLevel: CGFloat?) {
|
|
306
|
+
if let zoom = zoomLevel {
|
|
307
|
+
currentZoom = clamp(zoom, min: minZoom, max: maxZoom)
|
|
308
|
+
}
|
|
309
|
+
if let x = panX {
|
|
310
|
+
currentPanX = x
|
|
311
|
+
}
|
|
312
|
+
if let y = panY {
|
|
313
|
+
currentPanY = y
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
constrainPan()
|
|
317
|
+
applyTransformAndNotify()
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/// Reset pan/zoom to default state
|
|
321
|
+
func resetPanZoom() {
|
|
322
|
+
currentZoom = 1.0
|
|
323
|
+
currentPanX = 0.0
|
|
324
|
+
currentPanY = 0.0
|
|
325
|
+
applyTransformAndNotify()
|
|
326
|
+
}
|
|
327
|
+
|
|
130
328
|
override func layoutSubviews() {
|
|
131
329
|
super.layoutSubviews()
|
|
132
330
|
playerLayer?.frame = bounds
|
|
331
|
+
// Reapply transform after frame changes (only if not at default)
|
|
332
|
+
if !isAtDefaultTransform {
|
|
333
|
+
updateLayerTransform()
|
|
334
|
+
}
|
|
133
335
|
}
|
|
134
336
|
|
|
135
337
|
// MARK: - Public Methods (called from module)
|
|
@@ -323,3 +525,17 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
323
525
|
playerLayer = nil
|
|
324
526
|
}
|
|
325
527
|
}
|
|
528
|
+
|
|
529
|
+
// MARK: - UIGestureRecognizerDelegate
|
|
530
|
+
|
|
531
|
+
extension ExpoTwoStepVideoView: UIGestureRecognizerDelegate {
|
|
532
|
+
/// Allow pinch and pan gestures to work simultaneously
|
|
533
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
534
|
+
// Allow pinch and pan to work together
|
|
535
|
+
if (gestureRecognizer == pinchGesture && otherGestureRecognizer == panGesture) ||
|
|
536
|
+
(gestureRecognizer == panGesture && otherGestureRecognizer == pinchGesture) {
|
|
537
|
+
return true
|
|
538
|
+
}
|
|
539
|
+
return false
|
|
540
|
+
}
|
|
541
|
+
}
|
|
@@ -333,8 +333,175 @@ public class VideoTransformer {
|
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
// MARK: - Pan and Zoom
|
|
337
|
+
|
|
338
|
+
/// Create a composition with pan and zoom applied
|
|
339
|
+
/// - Parameters:
|
|
340
|
+
/// - asset: The video asset to transform
|
|
341
|
+
/// - panX: Horizontal pan (-1.0 to 1.0, 0 = center)
|
|
342
|
+
/// - panY: Vertical pan (-1.0 to 1.0, 0 = center)
|
|
343
|
+
/// - zoomLevel: Zoom level (1.0 = 100%, 2.0 = 200%, etc.)
|
|
344
|
+
/// - timeRange: Optional time range to apply (nil = full video)
|
|
345
|
+
/// - Returns: A tuple of (composition, videoComposition) for export
|
|
346
|
+
/// - Throws: VideoEditingError if pan/zoom fails
|
|
347
|
+
public func panZoom(
|
|
348
|
+
asset: VideoAsset,
|
|
349
|
+
panX: CGFloat,
|
|
350
|
+
panY: CGFloat,
|
|
351
|
+
zoomLevel: CGFloat,
|
|
352
|
+
timeRange: TimeRange? = nil
|
|
353
|
+
) async throws -> (AVMutableComposition, AVMutableVideoComposition) {
|
|
354
|
+
// Validate zoom level
|
|
355
|
+
guard zoomLevel >= 1.0 && zoomLevel <= 5.0 else {
|
|
356
|
+
throw VideoEditingError.invalidConfiguration(
|
|
357
|
+
reason: "Zoom level must be between 1.0 and 5.0"
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Validate pan values
|
|
362
|
+
guard panX >= -1.0 && panX <= 1.0 && panY >= -1.0 && panY <= 1.0 else {
|
|
363
|
+
throw VideoEditingError.invalidConfiguration(
|
|
364
|
+
reason: "Pan values must be between -1.0 and 1.0"
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let videoTracks = try await asset.avAsset.loadTracks(withMediaType: .video)
|
|
369
|
+
guard let sourceVideoTrack = videoTracks.first else {
|
|
370
|
+
throw VideoEditingError.noVideoTrack
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
let composition = AVMutableComposition()
|
|
374
|
+
|
|
375
|
+
// Determine time range
|
|
376
|
+
let effectiveRange: CMTimeRange
|
|
377
|
+
if let timeRange = timeRange {
|
|
378
|
+
try timeRange.validate(against: asset)
|
|
379
|
+
effectiveRange = timeRange.cmTimeRange
|
|
380
|
+
} else {
|
|
381
|
+
effectiveRange = CMTimeRange(start: .zero, duration: asset.duration)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Add video track
|
|
385
|
+
guard let compositionVideoTrack = composition.addMutableTrack(
|
|
386
|
+
withMediaType: .video,
|
|
387
|
+
preferredTrackID: kCMPersistentTrackID_Invalid
|
|
388
|
+
) else {
|
|
389
|
+
throw VideoEditingError.compositionFailed(
|
|
390
|
+
reason: "Failed to add video track to composition"
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
do {
|
|
395
|
+
try compositionVideoTrack.insertTimeRange(
|
|
396
|
+
effectiveRange,
|
|
397
|
+
of: sourceVideoTrack,
|
|
398
|
+
at: .zero
|
|
399
|
+
)
|
|
400
|
+
} catch {
|
|
401
|
+
throw VideoEditingError.compositionFailed(
|
|
402
|
+
reason: "Failed to insert video track: \(error.localizedDescription)"
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Add audio track if present
|
|
407
|
+
let audioTracks = try? await asset.avAsset.loadTracks(withMediaType: .audio)
|
|
408
|
+
if let sourceAudioTrack = audioTracks?.first {
|
|
409
|
+
if let compositionAudioTrack = composition.addMutableTrack(
|
|
410
|
+
withMediaType: .audio,
|
|
411
|
+
preferredTrackID: kCMPersistentTrackID_Invalid
|
|
412
|
+
) {
|
|
413
|
+
try? compositionAudioTrack.insertTimeRange(
|
|
414
|
+
effectiveRange,
|
|
415
|
+
of: sourceAudioTrack,
|
|
416
|
+
at: .zero
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Get video properties
|
|
422
|
+
let preferredTransform = try await sourceVideoTrack.load(.preferredTransform)
|
|
423
|
+
let naturalSize = try await sourceVideoTrack.load(.naturalSize)
|
|
424
|
+
|
|
425
|
+
// Calculate the actual render size accounting for the transform
|
|
426
|
+
let transformedSize = naturalSize.applying(preferredTransform)
|
|
427
|
+
let renderSize = CGSize(
|
|
428
|
+
width: abs(transformedSize.width),
|
|
429
|
+
height: abs(transformedSize.height)
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
// Create video composition for pan/zoom
|
|
433
|
+
let videoComposition = AVMutableVideoComposition()
|
|
434
|
+
videoComposition.renderSize = renderSize
|
|
435
|
+
|
|
436
|
+
// Get frame rate
|
|
437
|
+
let frameRate = try await sourceVideoTrack.load(.nominalFrameRate)
|
|
438
|
+
videoComposition.frameDuration = CMTime(value: 1, timescale: CMTimeScale(frameRate > 0 ? frameRate : 30))
|
|
439
|
+
|
|
440
|
+
// Create instruction
|
|
441
|
+
let instruction = AVMutableVideoCompositionInstruction()
|
|
442
|
+
instruction.timeRange = CMTimeRange(start: .zero, duration: composition.duration)
|
|
443
|
+
|
|
444
|
+
// Create layer instruction with pan/zoom transform
|
|
445
|
+
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionVideoTrack)
|
|
446
|
+
|
|
447
|
+
let panZoomTransform = createPanZoomTransform(
|
|
448
|
+
panX: panX,
|
|
449
|
+
panY: panY,
|
|
450
|
+
zoomLevel: zoomLevel,
|
|
451
|
+
size: renderSize,
|
|
452
|
+
originalTransform: preferredTransform
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
layerInstruction.setTransform(panZoomTransform, at: .zero)
|
|
456
|
+
|
|
457
|
+
instruction.layerInstructions = [layerInstruction]
|
|
458
|
+
videoComposition.instructions = [instruction]
|
|
459
|
+
|
|
460
|
+
return (composition, videoComposition)
|
|
461
|
+
}
|
|
462
|
+
|
|
336
463
|
// MARK: - Private Helpers
|
|
337
464
|
|
|
465
|
+
/// Create the transform matrix for pan and zoom
|
|
466
|
+
/// Scales around the center and then applies pan translation
|
|
467
|
+
private func createPanZoomTransform(
|
|
468
|
+
panX: CGFloat,
|
|
469
|
+
panY: CGFloat,
|
|
470
|
+
zoomLevel: CGFloat,
|
|
471
|
+
size: CGSize,
|
|
472
|
+
originalTransform: CGAffineTransform
|
|
473
|
+
) -> CGAffineTransform {
|
|
474
|
+
// Start with the original transform (handles rotation/orientation)
|
|
475
|
+
var transform = originalTransform
|
|
476
|
+
|
|
477
|
+
// Calculate the center of the video
|
|
478
|
+
let centerX = size.width / 2
|
|
479
|
+
let centerY = size.height / 2
|
|
480
|
+
|
|
481
|
+
// Step 1: Translate so the center is at the origin
|
|
482
|
+
transform = transform.concatenating(CGAffineTransform(translationX: -centerX, y: -centerY))
|
|
483
|
+
|
|
484
|
+
// Step 2: Apply zoom (scale)
|
|
485
|
+
transform = transform.concatenating(CGAffineTransform(scaleX: zoomLevel, y: zoomLevel))
|
|
486
|
+
|
|
487
|
+
// Step 3: Translate back to original position
|
|
488
|
+
transform = transform.concatenating(CGAffineTransform(translationX: centerX, y: centerY))
|
|
489
|
+
|
|
490
|
+
// Step 4: Apply pan translation
|
|
491
|
+
// Pan range is normalized to how much we can pan while keeping content visible
|
|
492
|
+
// When zoomed in 2x, we can pan up to half the width/height in each direction
|
|
493
|
+
let maxPanX = (size.width * zoomLevel - size.width) / 2
|
|
494
|
+
let maxPanY = (size.height * zoomLevel - size.height) / 2
|
|
495
|
+
|
|
496
|
+
// Negative because moving the content right means translating left
|
|
497
|
+
let translateX = -panX * maxPanX
|
|
498
|
+
let translateY = -panY * maxPanY
|
|
499
|
+
|
|
500
|
+
transform = transform.concatenating(CGAffineTransform(translationX: translateX, y: translateY))
|
|
501
|
+
|
|
502
|
+
return transform
|
|
503
|
+
}
|
|
504
|
+
|
|
338
505
|
/// Create the transform matrix for mirroring
|
|
339
506
|
private func createMirrorTransform(
|
|
340
507
|
axis: MirrorAxis,
|