@movementinfra/expo-twostep-video 0.1.0 โ†’ 0.1.1

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.
Files changed (2) hide show
  1. package/README.md +611 -5
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -9,8 +9,13 @@ Professional video editing for React Native, powered by native AVFoundation.
9
9
 
10
10
  - ๐ŸŽฌ **Load videos** from file system or Photos library
11
11
  - โœ‚๏ธ **Trim videos** with frame-accurate precision
12
+ - ๐Ÿชž **Mirror videos** horizontally, vertically, or both
13
+ - โฑ๏ธ **Speed adjustment** - slow motion (0.25x) to fast forward (4x)
14
+ - ๐Ÿ”„ **Loop segments** - repeat video sections for perfect loops
15
+ - ๐ŸŽ›๏ธ **Combined transformations** - trim + mirror + speed in one operation
12
16
  - ๐ŸŽž๏ธ **Create multi-segment compositions** (highlight reels)
13
17
  - ๐Ÿ“ธ **Generate thumbnails** at any timestamp
18
+ - ๐ŸŽฅ **Native video player** with playback controls
14
19
  - ๐Ÿ’พ **Export** with customizable quality settings
15
20
  - ๐Ÿ“Š **Real-time progress tracking** during exports
16
21
  - ๐Ÿงน **Automatic cleanup** of partial/temp files
@@ -108,6 +113,26 @@ function VideoEditor() {
108
113
 
109
114
  ## API Reference
110
115
 
116
+ ### Quick Reference
117
+
118
+ | Function | Description | Returns |
119
+ |----------|-------------|---------|
120
+ | `loadAsset({ uri })` | Load video from file URI | `VideoAsset` |
121
+ | `loadAssetFromPhotos(id)` | Load from Photos library | `VideoAsset` |
122
+ | `validateVideoUri(uri)` | Validate without loading | `boolean` |
123
+ | `trimVideo({ assetId, startTime, endTime })` | Trim to time range | `VideoComposition` |
124
+ | `trimVideoMultiple({ assetId, segments })` | Multi-segment trim | `VideoComposition` |
125
+ | `mirrorVideo({ assetId, axis, startTime?, endTime? })` | Mirror/flip video | `VideoComposition` |
126
+ | `adjustSpeed({ assetId, speed, startTime?, endTime? })` | Change speed | `VideoComposition` |
127
+ | `loopSegment({ assetId, startTime, endTime, loopCount })` | Loop a segment | `LoopResult` |
128
+ | `transformVideo({ assetId, speed?, mirrorAxis?, startTime?, endTime? })` | Combined transform | `VideoComposition` |
129
+ | `generateThumbnails({ assetId, times, size? })` | Extract frames | `string[]` (base64) |
130
+ | `exportVideo({ compositionId, quality? })` | Export composition | `ExportResult` |
131
+ | `exportAsset({ assetId, quality? })` | Export asset directly | `ExportResult` |
132
+ | `releaseAsset(id)` | Free memory | `void` |
133
+ | `releaseComposition(id)` | Free memory | `void` |
134
+ | `releaseAll()` | Free all memory | `void` |
135
+
111
136
  ### Loading Videos
112
137
 
113
138
  #### `loadAsset(options)`
@@ -177,6 +202,149 @@ const thumbnails = await TwoStepVideo.generateThumbnails({
177
202
  <Image source={{ uri: `data:image/png;base64,${thumbnails[0]}` }} />
178
203
  ```
179
204
 
205
+ ### Video Mirroring
206
+
207
+ #### `mirrorVideo(options)`
208
+ Mirror (flip) a video horizontally, vertically, or both. Perfect for selfie videos that need to be flipped.
209
+
210
+ ```typescript
211
+ // Mirror horizontally (flip left-right) - common for selfie videos
212
+ const mirrored = await TwoStepVideo.mirrorVideo({
213
+ assetId: asset.id,
214
+ axis: 'horizontal'
215
+ });
216
+
217
+ // Mirror vertically (flip top-bottom)
218
+ const flipped = await TwoStepVideo.mirrorVideo({
219
+ assetId: asset.id,
220
+ axis: 'vertical'
221
+ });
222
+
223
+ // Mirror both axes (180ยฐ rotation effect)
224
+ const both = await TwoStepVideo.mirrorVideo({
225
+ assetId: asset.id,
226
+ axis: 'both'
227
+ });
228
+
229
+ // Mirror only a specific segment (5s to 10s)
230
+ const partialMirror = await TwoStepVideo.mirrorVideo({
231
+ assetId: asset.id,
232
+ axis: 'horizontal',
233
+ startTime: 5,
234
+ endTime: 10
235
+ });
236
+ ```
237
+
238
+ ### Speed Adjustment
239
+
240
+ #### `adjustSpeed(options)`
241
+ Change the playback speed of a video. Supports slow motion (< 1.0) and fast forward (> 1.0).
242
+
243
+ ```typescript
244
+ // Slow motion (0.5x = 2x slower, video becomes twice as long)
245
+ const slowMo = await TwoStepVideo.adjustSpeed({
246
+ assetId: asset.id,
247
+ speed: 0.5
248
+ });
249
+
250
+ // Very slow motion (0.25x = 4x slower)
251
+ const verySlow = await TwoStepVideo.adjustSpeed({
252
+ assetId: asset.id,
253
+ speed: 0.25
254
+ });
255
+
256
+ // Fast forward (2x speed, video becomes half as long)
257
+ const fastForward = await TwoStepVideo.adjustSpeed({
258
+ assetId: asset.id,
259
+ speed: 2.0
260
+ });
261
+
262
+ // Timelapse effect (4x speed)
263
+ const timelapse = await TwoStepVideo.adjustSpeed({
264
+ assetId: asset.id,
265
+ speed: 4.0
266
+ });
267
+
268
+ // Speed up only a specific segment (10s to 30s becomes a quick timelapse)
269
+ const partialSpeed = await TwoStepVideo.adjustSpeed({
270
+ assetId: asset.id,
271
+ speed: 4.0,
272
+ startTime: 10,
273
+ endTime: 30
274
+ });
275
+ ```
276
+
277
+ ### Video Looping
278
+
279
+ #### `loopSegment(options)`
280
+ Loop a segment of video multiple times. Great for creating perfect loops for social media or extending short clips.
281
+
282
+ ```typescript
283
+ // Loop a 2-second segment 3 times (plays 4 times total = 8 seconds)
284
+ const looped = await TwoStepVideo.loopSegment({
285
+ assetId: asset.id,
286
+ startTime: 5,
287
+ endTime: 7,
288
+ loopCount: 3 // Repeats 3 times after first play
289
+ });
290
+
291
+ console.log(`Duration: ${looped.duration}s`); // 8 seconds
292
+ console.log(`Total plays: ${looped.totalPlays}`); // 4 times
293
+
294
+ // Create a 15-second loop from a 3-second clip for Instagram/TikTok
295
+ const socialLoop = await TwoStepVideo.loopSegment({
296
+ assetId: asset.id,
297
+ startTime: 0,
298
+ endTime: 3,
299
+ loopCount: 4 // 3s * 5 plays = 15 seconds
300
+ });
301
+
302
+ // Loop the best moment of a video
303
+ const bestMoment = await TwoStepVideo.loopSegment({
304
+ assetId: asset.id,
305
+ startTime: 12.5, // Start at 12.5 seconds
306
+ endTime: 14.0, // 1.5 second clip
307
+ loopCount: 9 // 1.5s * 10 plays = 15 seconds
308
+ });
309
+ ```
310
+
311
+ ### Combined Transformations
312
+
313
+ #### `transformVideo(options)`
314
+ Apply multiple transformations in a single operation: trim, mirror, and speed adjustment combined.
315
+
316
+ ```typescript
317
+ // Mirror and slow down (selfie video correction + slow-mo effect)
318
+ const transformed = await TwoStepVideo.transformVideo({
319
+ assetId: asset.id,
320
+ speed: 0.5,
321
+ mirrorAxis: 'horizontal'
322
+ });
323
+
324
+ // Just mirror (speed defaults to 1.0)
325
+ const mirrored = await TwoStepVideo.transformVideo({
326
+ assetId: asset.id,
327
+ mirrorAxis: 'both'
328
+ });
329
+
330
+ // Trim + mirror + speed all at once
331
+ const fullTransform = await TwoStepVideo.transformVideo({
332
+ assetId: asset.id,
333
+ speed: 2.0,
334
+ mirrorAxis: 'horizontal',
335
+ startTime: 0,
336
+ endTime: 10 // Take first 10 seconds, mirror it, speed it up
337
+ });
338
+
339
+ // Speed up without mirroring (just provide speed)
340
+ const fastVersion = await TwoStepVideo.transformVideo({
341
+ assetId: asset.id,
342
+ speed: 1.5,
343
+ startTime: 5,
344
+ endTime: 15
345
+ });
346
+ ```
347
+
180
348
  ### Exporting
181
349
 
182
350
  #### `exportVideo(options)`
@@ -200,6 +368,77 @@ const result = await TwoStepVideo.exportAsset({
200
368
  });
201
369
  ```
202
370
 
371
+ ### Video Player View
372
+
373
+ #### `TwoStepVideoView`
374
+ A native video player component for playing assets and compositions with full playback control.
375
+
376
+ ```tsx
377
+ import { TwoStepVideoView, TwoStepVideoViewRef } from 'expo-twostep-video';
378
+ import { useRef, useState } from 'react';
379
+ import { View, Button, Text } from 'react-native';
380
+
381
+ function VideoPlayer({ compositionId }: { compositionId: string }) {
382
+ const playerRef = useRef<TwoStepVideoViewRef>(null);
383
+ const [status, setStatus] = useState<string>('ready');
384
+ const [progress, setProgress] = useState(0);
385
+
386
+ return (
387
+ <View style={{ flex: 1 }}>
388
+ <TwoStepVideoView
389
+ ref={playerRef}
390
+ compositionId={compositionId}
391
+ loop={false}
392
+ onPlaybackStatusChange={(e) => setStatus(e.nativeEvent.status)}
393
+ onProgress={(e) => setProgress(e.nativeEvent.progress)}
394
+ onEnd={() => console.log('Video ended')}
395
+ onError={(e) => console.error(e.nativeEvent.error)}
396
+ style={{ width: '100%', height: 300 }}
397
+ />
398
+
399
+ <Text>Status: {status} | Progress: {Math.round(progress * 100)}%</Text>
400
+
401
+ <View style={{ flexDirection: 'row', gap: 10 }}>
402
+ <Button title="Play" onPress={() => playerRef.current?.play()} />
403
+ <Button title="Pause" onPress={() => playerRef.current?.pause()} />
404
+ <Button title="Seek 5s" onPress={() => playerRef.current?.seek(5)} />
405
+ <Button title="Replay" onPress={() => playerRef.current?.replay()} />
406
+ </View>
407
+ </View>
408
+ );
409
+ }
410
+ ```
411
+
412
+ #### Player Props
413
+
414
+ | Prop | Type | Description |
415
+ |------|------|-------------|
416
+ | `compositionId` | `string` | ID from `mirrorVideo`, `trimVideo`, `loopSegment`, etc. |
417
+ | `assetId` | `string` | ID from `loadAsset` (for playing without transformations) |
418
+ | `loop` | `boolean` | Enable continuous looping (default: `false`) |
419
+ | `onPlaybackStatusChange` | `function` | Called when status changes (`ready`, `playing`, `paused`, `ended`, `seeked`) |
420
+ | `onProgress` | `function` | Called periodically with `{ currentTime, duration, progress }` |
421
+ | `onEnd` | `function` | Called when video ends (not called if `loop` is true) |
422
+ | `onError` | `function` | Called on playback error |
423
+
424
+ #### Player Methods (via ref)
425
+
426
+ ```typescript
427
+ const playerRef = useRef<TwoStepVideoViewRef>(null);
428
+
429
+ // Start playback
430
+ await playerRef.current?.play();
431
+
432
+ // Pause playback
433
+ await playerRef.current?.pause();
434
+
435
+ // Seek to specific time (in seconds)
436
+ await playerRef.current?.seek(10.5);
437
+
438
+ // Restart from beginning
439
+ await playerRef.current?.replay();
440
+ ```
441
+
203
442
  ### Events
204
443
 
205
444
  #### `addExportProgressListener(callback)`
@@ -228,20 +467,41 @@ TwoStepVideo.releaseAll();
228
467
  TwoStepVideo.cleanupFile(tempFileUri);
229
468
  ```
230
469
 
231
- ## Quality Presets
470
+ ## Constants
471
+
472
+ ### Quality Presets
232
473
 
233
474
  ```typescript
234
- TwoStepVideo.Quality.LOW // ~0.1 bits per pixel
235
- TwoStepVideo.Quality.MEDIUM // ~0.2 bits per pixel
236
- TwoStepVideo.Quality.HIGH // ~0.4 bits per pixel (recommended)
237
- TwoStepVideo.Quality.HIGHEST // ~0.8 bits per pixel
475
+ TwoStepVideo.Quality.LOW // ~0.1 bits per pixel - fast, small files
476
+ TwoStepVideo.Quality.MEDIUM // ~0.2 bits per pixel - good for web/social
477
+ TwoStepVideo.Quality.HIGH // ~0.4 bits per pixel (recommended default)
478
+ TwoStepVideo.Quality.HIGHEST // ~0.8 bits per pixel - archival quality
238
479
  ```
239
480
 
481
+ ### Mirror Axis
482
+
483
+ ```typescript
484
+ TwoStepVideo.Mirror.HORIZONTAL // Flip left-right (selfie correction)
485
+ TwoStepVideo.Mirror.VERTICAL // Flip top-bottom
486
+ TwoStepVideo.Mirror.BOTH // Flip both axes (180ยฐ rotation effect)
487
+ ```
488
+
489
+ ### Speed Ranges
490
+
491
+ | Speed | Effect | Duration Change |
492
+ |-------|--------|-----------------|
493
+ | 0.25 | Very slow motion | 4x longer |
494
+ | 0.5 | Slow motion | 2x longer |
495
+ | 1.0 | Normal speed | No change |
496
+ | 2.0 | Fast forward | 2x shorter |
497
+ | 4.0 | Timelapse | 4x shorter |
498
+
240
499
  ## TypeScript Support
241
500
 
242
501
  Full TypeScript definitions included:
243
502
 
244
503
  ```typescript
504
+ // Core types
245
505
  interface VideoAsset {
246
506
  id: string;
247
507
  duration: number;
@@ -260,6 +520,65 @@ interface ExportResult {
260
520
  uri: string;
261
521
  path: string;
262
522
  }
523
+
524
+ // Transformation types
525
+ type MirrorAxis = 'horizontal' | 'vertical' | 'both';
526
+ type VideoQuality = 'low' | 'medium' | 'high' | 'highest';
527
+
528
+ interface MirrorVideoOptions {
529
+ assetId: string;
530
+ axis: MirrorAxis;
531
+ startTime?: number; // Optional: mirror only a segment
532
+ endTime?: number;
533
+ }
534
+
535
+ interface AdjustSpeedOptions {
536
+ assetId: string;
537
+ speed: number; // 0.25 to 4.0
538
+ startTime?: number;
539
+ endTime?: number;
540
+ }
541
+
542
+ interface LoopSegmentOptions {
543
+ assetId: string;
544
+ startTime: number;
545
+ endTime: number;
546
+ loopCount: number; // Repeats after first play
547
+ }
548
+
549
+ interface LoopResult {
550
+ id: string;
551
+ duration: number;
552
+ loopCount: number;
553
+ totalPlays: number; // loopCount + 1
554
+ }
555
+
556
+ interface TransformVideoOptions {
557
+ assetId: string;
558
+ speed?: number;
559
+ mirrorAxis?: MirrorAxis;
560
+ startTime?: number;
561
+ endTime?: number;
562
+ }
563
+
564
+ // Player types
565
+ interface TwoStepVideoViewProps {
566
+ compositionId?: string;
567
+ assetId?: string;
568
+ loop?: boolean;
569
+ onPlaybackStatusChange?: (event: { nativeEvent: PlaybackStatusEvent }) => void;
570
+ onProgress?: (event: { nativeEvent: ProgressEvent }) => void;
571
+ onEnd?: (event: { nativeEvent: {} }) => void;
572
+ onError?: (event: { nativeEvent: ErrorEvent }) => void;
573
+ style?: ViewStyle;
574
+ }
575
+
576
+ interface TwoStepVideoViewRef {
577
+ play: () => Promise<void>;
578
+ pause: () => Promise<void>;
579
+ seek: (time: number) => Promise<void>;
580
+ replay: () => Promise<void>;
581
+ }
263
582
  ```
264
583
 
265
584
  ## Advanced Usage
@@ -291,6 +610,293 @@ async function createHighlightReel(videoUri: string) {
291
610
  }
292
611
  ```
293
612
 
613
+ ### Selfie Video Mirror & Loop
614
+
615
+ A complete example for processing selfie videos with mirror correction and perfect loops:
616
+
617
+ ```tsx
618
+ import React, { useState, useRef, useEffect } from 'react';
619
+ import { View, Button, Text, StyleSheet, ActivityIndicator } from 'react-native';
620
+ import * as TwoStepVideo from 'expo-twostep-video';
621
+ import { TwoStepVideoView, TwoStepVideoViewRef } from 'expo-twostep-video';
622
+
623
+ interface Props {
624
+ videoUri: string;
625
+ }
626
+
627
+ function SelfieVideoEditor({ videoUri }: Props) {
628
+ const playerRef = useRef<TwoStepVideoViewRef>(null);
629
+ const [asset, setAsset] = useState<TwoStepVideo.VideoAsset | null>(null);
630
+ const [composition, setComposition] = useState<TwoStepVideo.VideoComposition | null>(null);
631
+ const [isProcessing, setIsProcessing] = useState(false);
632
+ const [exportProgress, setExportProgress] = useState(0);
633
+
634
+ // Load the video asset
635
+ useEffect(() => {
636
+ async function loadVideo() {
637
+ const loaded = await TwoStepVideo.loadAsset({ uri: videoUri });
638
+ setAsset(loaded);
639
+ }
640
+ loadVideo();
641
+
642
+ return () => {
643
+ // Cleanup on unmount
644
+ TwoStepVideo.releaseAll();
645
+ };
646
+ }, [videoUri]);
647
+
648
+ // Mirror the selfie video (fix the mirror effect from front camera)
649
+ const handleMirror = async () => {
650
+ if (!asset) return;
651
+ setIsProcessing(true);
652
+
653
+ const mirrored = await TwoStepVideo.mirrorVideo({
654
+ assetId: asset.id,
655
+ axis: 'horizontal'
656
+ });
657
+
658
+ setComposition(mirrored);
659
+ setIsProcessing(false);
660
+ };
661
+
662
+ // Create a slow-motion effect
663
+ const handleSlowMo = async () => {
664
+ if (!asset) return;
665
+ setIsProcessing(true);
666
+
667
+ const slowMo = await TwoStepVideo.adjustSpeed({
668
+ assetId: asset.id,
669
+ speed: 0.5 // Half speed
670
+ });
671
+
672
+ setComposition(slowMo);
673
+ setIsProcessing(false);
674
+ };
675
+
676
+ // Mirror + slow motion combined
677
+ const handleMirrorAndSlowMo = async () => {
678
+ if (!asset) return;
679
+ setIsProcessing(true);
680
+
681
+ const transformed = await TwoStepVideo.transformVideo({
682
+ assetId: asset.id,
683
+ speed: 0.5,
684
+ mirrorAxis: 'horizontal'
685
+ });
686
+
687
+ setComposition(transformed);
688
+ setIsProcessing(false);
689
+ };
690
+
691
+ // Create a perfect loop for social media
692
+ const handleCreateLoop = async () => {
693
+ if (!asset) return;
694
+ setIsProcessing(true);
695
+
696
+ // Loop the first 3 seconds to create 15 seconds of content
697
+ const looped = await TwoStepVideo.loopSegment({
698
+ assetId: asset.id,
699
+ startTime: 0,
700
+ endTime: 3,
701
+ loopCount: 4 // 3s * 5 plays = 15 seconds
702
+ });
703
+
704
+ setComposition(looped);
705
+ setIsProcessing(false);
706
+ console.log(`Created ${looped.totalPlays}-play loop, ${looped.duration}s duration`);
707
+ };
708
+
709
+ // Export the result
710
+ const handleExport = async () => {
711
+ if (!composition) return;
712
+
713
+ const subscription = TwoStepVideo.addExportProgressListener((event) => {
714
+ setExportProgress(event.progress);
715
+ });
716
+
717
+ try {
718
+ const result = await TwoStepVideo.exportVideo({
719
+ compositionId: composition.id,
720
+ quality: 'high'
721
+ });
722
+
723
+ alert(`Exported to: ${result.uri}`);
724
+ } finally {
725
+ subscription.remove();
726
+ setExportProgress(0);
727
+ }
728
+ };
729
+
730
+ if (!asset) {
731
+ return <ActivityIndicator size="large" />;
732
+ }
733
+
734
+ return (
735
+ <View style={styles.container}>
736
+ {/* Video Player */}
737
+ <TwoStepVideoView
738
+ ref={playerRef}
739
+ compositionId={composition?.id}
740
+ assetId={!composition ? asset.id : undefined}
741
+ loop={true}
742
+ style={styles.video}
743
+ />
744
+
745
+ {/* Video Info */}
746
+ <Text style={styles.info}>
747
+ Duration: {composition?.duration.toFixed(1) || asset.duration.toFixed(1)}s |
748
+ Size: {asset.width}x{asset.height}
749
+ </Text>
750
+
751
+ {/* Processing Indicator */}
752
+ {isProcessing && <ActivityIndicator size="small" />}
753
+
754
+ {/* Transformation Buttons */}
755
+ <View style={styles.buttons}>
756
+ <Button title="Mirror" onPress={handleMirror} disabled={isProcessing} />
757
+ <Button title="Slow-Mo" onPress={handleSlowMo} disabled={isProcessing} />
758
+ <Button title="Mirror + Slow" onPress={handleMirrorAndSlowMo} disabled={isProcessing} />
759
+ <Button title="Create Loop" onPress={handleCreateLoop} disabled={isProcessing} />
760
+ </View>
761
+
762
+ {/* Playback Controls */}
763
+ <View style={styles.controls}>
764
+ <Button title="Play" onPress={() => playerRef.current?.play()} />
765
+ <Button title="Pause" onPress={() => playerRef.current?.pause()} />
766
+ <Button title="Replay" onPress={() => playerRef.current?.replay()} />
767
+ </View>
768
+
769
+ {/* Export */}
770
+ {composition && (
771
+ <View style={styles.export}>
772
+ <Button title="Export Video" onPress={handleExport} />
773
+ {exportProgress > 0 && (
774
+ <Text>Exporting: {Math.round(exportProgress * 100)}%</Text>
775
+ )}
776
+ </View>
777
+ )}
778
+ </View>
779
+ );
780
+ }
781
+
782
+ const styles = StyleSheet.create({
783
+ container: { flex: 1, padding: 16 },
784
+ video: { width: '100%', height: 300, backgroundColor: '#000' },
785
+ info: { textAlign: 'center', marginVertical: 8 },
786
+ buttons: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginVertical: 8 },
787
+ controls: { flexDirection: 'row', justifyContent: 'center', gap: 16 },
788
+ export: { marginTop: 16, alignItems: 'center' },
789
+ });
790
+
791
+ export default SelfieVideoEditor;
792
+ ```
793
+
794
+ ### Timestamp-Based Effects
795
+
796
+ Apply different effects to specific timestamps/segments of a video:
797
+
798
+ ```typescript
799
+ import * as TwoStepVideo from 'expo-twostep-video';
800
+
801
+ async function applyTimestampEffects(videoUri: string) {
802
+ const asset = await TwoStepVideo.loadAsset({ uri: videoUri });
803
+
804
+ // Example: Mirror only the segment from 5s to 10s
805
+ const mirroredSegment = await TwoStepVideo.mirrorVideo({
806
+ assetId: asset.id,
807
+ axis: 'horizontal',
808
+ startTime: 5,
809
+ endTime: 10
810
+ });
811
+
812
+ // Example: Speed up only a boring middle section (10s to 30s at 4x speed)
813
+ const timelapseMiddle = await TwoStepVideo.adjustSpeed({
814
+ assetId: asset.id,
815
+ speed: 4.0,
816
+ startTime: 10,
817
+ endTime: 30
818
+ });
819
+
820
+ // Example: Transform a specific segment (trim + mirror + speed)
821
+ const transformedClip = await TwoStepVideo.transformVideo({
822
+ assetId: asset.id,
823
+ speed: 0.5, // Slow motion
824
+ mirrorAxis: 'horizontal',
825
+ startTime: 45, // Best moment starts at 45s
826
+ endTime: 50 // 5 seconds of slow-mo mirrored footage
827
+ });
828
+
829
+ return transformedClip;
830
+ }
831
+ ```
832
+
833
+ ### Create Social Media Loop with Mirror
834
+
835
+ Perfect for TikTok/Instagram Reels - select a short segment, mirror it, and loop it:
836
+
837
+ ```typescript
838
+ async function createSocialMediaLoop(
839
+ videoUri: string,
840
+ startTime: number,
841
+ endTime: number,
842
+ targetDuration: number = 15
843
+ ) {
844
+ const asset = await TwoStepVideo.loadAsset({ uri: videoUri });
845
+
846
+ // First, create the mirrored and trimmed base clip
847
+ const mirroredClip = await TwoStepVideo.transformVideo({
848
+ assetId: asset.id,
849
+ mirrorAxis: 'horizontal',
850
+ startTime,
851
+ endTime
852
+ });
853
+
854
+ // Export the mirrored clip first (loopSegment needs the original asset)
855
+ const exportedMirror = await TwoStepVideo.exportVideo({
856
+ compositionId: mirroredClip.id,
857
+ quality: 'high'
858
+ });
859
+
860
+ // Load the exported mirrored video
861
+ const mirroredAsset = await TwoStepVideo.loadAsset({
862
+ uri: exportedMirror.uri
863
+ });
864
+
865
+ // Calculate how many loops needed to reach target duration
866
+ const clipDuration = endTime - startTime;
867
+ const loopCount = Math.ceil(targetDuration / clipDuration) - 1;
868
+
869
+ // Create the loop
870
+ const looped = await TwoStepVideo.loopSegment({
871
+ assetId: mirroredAsset.id,
872
+ startTime: 0,
873
+ endTime: mirroredAsset.duration,
874
+ loopCount
875
+ });
876
+
877
+ console.log(`Created ${looped.duration}s loop from ${clipDuration}s clip`);
878
+
879
+ // Export final result
880
+ const result = await TwoStepVideo.exportVideo({
881
+ compositionId: looped.id,
882
+ quality: 'high'
883
+ });
884
+
885
+ // Cleanup intermediate files
886
+ TwoStepVideo.cleanupFile(exportedMirror.uri);
887
+
888
+ return result;
889
+ }
890
+
891
+ // Usage
892
+ const socialLoop = await createSocialMediaLoop(
893
+ 'file:///path/to/selfie.mp4',
894
+ 5.0, // Start at 5 seconds
895
+ 8.0, // End at 8 seconds (3 second clip)
896
+ 15 // Target 15 seconds for Instagram
897
+ );
898
+ ```
899
+
294
900
  ### With Progress Bar
295
901
 
296
902
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@movementinfra/expo-twostep-video",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Minimal video editing for React Native using AVFoundation",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",