@movementinfra/expo-twostep-video 0.1.12 โ†’ 0.1.14

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 CHANGED
@@ -7,20 +7,23 @@ Professional video editing for React Native, powered by native AVFoundation.
7
7
 
8
8
  ## Features
9
9
 
10
- - ๐ŸŽฌ **Load videos** from file system or Photos library
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
16
- - ๐ŸŽž๏ธ **Create multi-segment compositions** (highlight reels)
17
- - ๐Ÿ“ธ **Generate thumbnails** at any timestamp
18
- - ๐ŸŽฅ **Native video player** with playback controls
19
- - ๐Ÿ’พ **Export** with customizable quality settings
20
- - ๐Ÿ“Š **Real-time progress tracking** during exports
21
- - ๐Ÿงน **Automatic cleanup** of partial/temp files
22
- - ๐Ÿ”’ **Type-safe TypeScript API** with full IntelliSense
23
- - ๐Ÿ“ฑ **iOS 15+** support
10
+ - **Trim videos** with frame-accurate precision
11
+ - **Mirror videos** horizontally, vertically, or both
12
+ - **Speed adjustment** - slow motion (0.25x) to fast forward (4x)
13
+ - **Loop segments** - repeat video sections for perfect loops
14
+ - **Pan & zoom** - pinch-to-zoom with gesture controls
15
+ - **Multi-segment compositions** - create highlight reels
16
+ - **Generate thumbnails** at any timestamp
17
+ - **Native video player** with playback controls
18
+ - **Export** with customizable quality settings
19
+ - **Real-time progress** tracking during exports
20
+ - **Type-safe TypeScript API** with full IntelliSense
21
+
22
+ ## Requirements
23
+
24
+ - iOS 15.0+
25
+ - Expo SDK 50+
26
+ - React Native 0.72+
24
27
 
25
28
  ## Installation
26
29
 
@@ -38,634 +41,328 @@ const asset = await TwoStepVideo.loadAsset({
38
41
  uri: 'file:///path/to/video.mp4'
39
42
  });
40
43
 
41
- // Trim it (5 seconds to 15 seconds)
44
+ // Trim to 10 seconds
42
45
  const composition = await TwoStepVideo.trimVideo({
43
46
  assetId: asset.id,
44
47
  startTime: 5.0,
45
48
  endTime: 15.0
46
49
  });
47
50
 
48
- // Export with progress tracking
51
+ // Export
49
52
  const result = await TwoStepVideo.exportVideo({
50
53
  compositionId: composition.id,
51
54
  quality: TwoStepVideo.Quality.HIGH
52
55
  });
53
56
 
54
57
  console.log('Exported to:', result.uri);
55
- ```
56
-
57
- ## Complete Example
58
-
59
- ```typescript
60
- import React, { useState, useEffect } from 'react';
61
- import { View, Button, Text } from 'react-native';
62
- import * as TwoStepVideo from 'expo-twostep-video';
63
58
 
64
- function VideoEditor() {
65
- const [progress, setProgress] = useState(0);
66
-
67
- useEffect(() => {
68
- const subscription = TwoStepVideo.addExportProgressListener((event) => {
69
- setProgress(event.progress);
70
- });
71
- return () => subscription.remove();
72
- }, []);
73
-
74
- const trimAndExport = async () => {
75
- try {
76
- // Load
77
- const asset = await TwoStepVideo.loadAsset({
78
- uri: 'file:///path/to/video.mp4'
79
- });
80
-
81
- // Trim
82
- const composition = await TwoStepVideo.trimVideo({
83
- assetId: asset.id,
84
- startTime: 0,
85
- endTime: 10
86
- });
87
-
88
- // Export
89
- const result = await TwoStepVideo.exportVideo({
90
- compositionId: composition.id,
91
- quality: TwoStepVideo.Quality.HIGH
92
- });
93
-
94
- alert(`Saved to: ${result.uri}`);
95
-
96
- // Cleanup
97
- TwoStepVideo.releaseAsset(asset.id);
98
- TwoStepVideo.releaseComposition(composition.id);
99
-
100
- } catch (error) {
101
- console.error('Error:', error);
102
- }
103
- };
104
-
105
- return (
106
- <View>
107
- <Text>Progress: {Math.round(progress * 100)}%</Text>
108
- <Button title="Trim & Export" onPress={trimAndExport} />
109
- </View>
110
- );
111
- }
59
+ // Cleanup
60
+ TwoStepVideo.releaseAll();
112
61
  ```
113
62
 
114
63
  ## API Reference
115
64
 
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
- | `panZoomVideo({ assetId, panX, panY, zoomLevel })` | Apply pan/zoom crop | `VideoComposition` |
130
- | `generateThumbnails({ assetId, times, size? })` | Extract frames | `string[]` (base64) |
131
- | `exportVideo({ compositionId, quality? })` | Export composition | `ExportResult` |
132
- | `exportAsset({ assetId, quality? })` | Export asset directly | `ExportResult` |
133
- | `releaseAsset(id)` | Free memory | `void` |
134
- | `releaseComposition(id)` | Free memory | `void` |
135
- | `releaseAll()` | Free all memory | `void` |
136
-
137
65
  ### Loading Videos
138
66
 
139
- #### `loadAsset(options)`
140
- Load a video from a file URI.
141
-
142
67
  ```typescript
143
- const asset = await TwoStepVideo.loadAsset({
144
- uri: 'file:///path/to/video.mp4'
145
- });
68
+ // Load from file URI
69
+ const asset = await TwoStepVideo.loadAsset({ uri: 'file:///path/to/video.mp4' });
146
70
  // Returns: { id, duration, width, height, frameRate, hasAudio }
147
- ```
148
71
 
149
- #### `loadAssetFromPhotos(localIdentifier)`
150
- Load a video from the Photos library.
151
-
152
- ```typescript
72
+ // Load from Photos library
153
73
  const asset = await TwoStepVideo.loadAssetFromPhotos(photoAsset.id);
154
- ```
155
-
156
- #### `validateVideoUri(uri)`
157
- Quickly validate a video URI without loading.
158
74
 
159
- ```typescript
75
+ // Validate without loading
160
76
  const isValid = await TwoStepVideo.validateVideoUri(uri);
161
77
  ```
162
78
 
163
79
  ### Trimming
164
80
 
165
- #### `trimVideo(options)`
166
- Trim to a single time range.
167
-
168
81
  ```typescript
82
+ // Single segment
169
83
  const composition = await TwoStepVideo.trimVideo({
170
84
  assetId: asset.id,
171
- startTime: 5.0, // seconds
85
+ startTime: 5.0,
172
86
  endTime: 15.0
173
87
  });
174
- ```
175
-
176
- #### `trimVideoMultiple(options)`
177
- Create a composition from multiple segments.
178
88
 
179
- ```typescript
89
+ // Multiple segments (highlight reel)
180
90
  const composition = await TwoStepVideo.trimVideoMultiple({
181
91
  assetId: asset.id,
182
92
  segments: [
183
- { start: 0, end: 5 },
93
+ { start: 0, end: 3 },
184
94
  { start: 10, end: 15 },
185
95
  { start: 20, end: 25 }
186
96
  ]
187
97
  });
188
98
  ```
189
99
 
190
- ### Thumbnails
191
-
192
- #### `generateThumbnails(options)`
193
- Extract thumbnail images at specific times.
194
-
195
- ```typescript
196
- const thumbnails = await TwoStepVideo.generateThumbnails({
197
- assetId: asset.id,
198
- times: [0, 5, 10, 15],
199
- size: { width: 300, height: 300 }
200
- });
201
-
202
- // Use in Image component
203
- <Image source={{ uri: `data:image/png;base64,${thumbnails[0]}` }} />
204
- ```
205
-
206
- ### Video Mirroring
207
-
208
- #### `mirrorVideo(options)`
209
- Mirror (flip) a video horizontally, vertically, or both. Perfect for selfie videos that need to be flipped.
100
+ ### Mirroring
210
101
 
211
102
  ```typescript
212
- // Mirror horizontally (flip left-right) - common for selfie videos
103
+ // Horizontal (flip left-right) - common for selfie videos
213
104
  const mirrored = await TwoStepVideo.mirrorVideo({
214
105
  assetId: asset.id,
215
106
  axis: 'horizontal'
216
107
  });
217
108
 
218
- // Mirror vertically (flip top-bottom)
109
+ // Vertical (flip top-bottom)
219
110
  const flipped = await TwoStepVideo.mirrorVideo({
220
111
  assetId: asset.id,
221
112
  axis: 'vertical'
222
113
  });
223
114
 
224
- // Mirror both axes (180ยฐ rotation effect)
115
+ // Both axes
225
116
  const both = await TwoStepVideo.mirrorVideo({
226
117
  assetId: asset.id,
227
118
  axis: 'both'
228
119
  });
229
-
230
- // Mirror only a specific segment (5s to 10s)
231
- const partialMirror = await TwoStepVideo.mirrorVideo({
232
- assetId: asset.id,
233
- axis: 'horizontal',
234
- startTime: 5,
235
- endTime: 10
236
- });
237
120
  ```
238
121
 
239
122
  ### Speed Adjustment
240
123
 
241
- #### `adjustSpeed(options)`
242
- Change the playback speed of a video. Supports slow motion (< 1.0) and fast forward (> 1.0).
243
-
244
124
  ```typescript
245
- // Slow motion (0.5x = 2x slower, video becomes twice as long)
125
+ // Slow motion (0.5x = 2x slower)
246
126
  const slowMo = await TwoStepVideo.adjustSpeed({
247
127
  assetId: asset.id,
248
128
  speed: 0.5
249
129
  });
250
130
 
251
- // Very slow motion (0.25x = 4x slower)
252
- const verySlow = await TwoStepVideo.adjustSpeed({
253
- assetId: asset.id,
254
- speed: 0.25
255
- });
256
-
257
- // Fast forward (2x speed, video becomes half as long)
258
- const fastForward = await TwoStepVideo.adjustSpeed({
131
+ // Fast forward (2x speed)
132
+ const fast = await TwoStepVideo.adjustSpeed({
259
133
  assetId: asset.id,
260
134
  speed: 2.0
261
135
  });
262
136
 
263
- // Timelapse effect (4x speed)
137
+ // Timelapse (4x speed)
264
138
  const timelapse = await TwoStepVideo.adjustSpeed({
265
139
  assetId: asset.id,
266
140
  speed: 4.0
267
141
  });
268
-
269
- // Speed up only a specific segment (10s to 30s becomes a quick timelapse)
270
- const partialSpeed = await TwoStepVideo.adjustSpeed({
271
- assetId: asset.id,
272
- speed: 4.0,
273
- startTime: 10,
274
- endTime: 30
275
- });
276
142
  ```
277
143
 
278
- ### Video Looping
279
-
280
- #### `loopSegment(options)`
281
- Loop a segment of video multiple times. Great for creating perfect loops for social media or extending short clips.
144
+ ### Looping
282
145
 
283
146
  ```typescript
284
- // Loop a 2-second segment 3 times (plays 4 times total = 8 seconds)
147
+ // Loop a 3-second segment 4 times (plays 5 times total = 15 seconds)
285
148
  const looped = await TwoStepVideo.loopSegment({
286
- assetId: asset.id,
287
- startTime: 5,
288
- endTime: 7,
289
- loopCount: 3 // Repeats 3 times after first play
290
- });
291
-
292
- console.log(`Duration: ${looped.duration}s`); // 8 seconds
293
- console.log(`Total plays: ${looped.totalPlays}`); // 4 times
294
-
295
- // Create a 15-second loop from a 3-second clip for Instagram/TikTok
296
- const socialLoop = await TwoStepVideo.loopSegment({
297
149
  assetId: asset.id,
298
150
  startTime: 0,
299
151
  endTime: 3,
300
- loopCount: 4 // 3s * 5 plays = 15 seconds
152
+ loopCount: 4
301
153
  });
302
154
 
303
- // Loop the best moment of a video
304
- const bestMoment = await TwoStepVideo.loopSegment({
305
- assetId: asset.id,
306
- startTime: 12.5, // Start at 12.5 seconds
307
- endTime: 14.0, // 1.5 second clip
308
- loopCount: 9 // 1.5s * 10 plays = 15 seconds
309
- });
155
+ console.log(`Duration: ${looped.duration}s, plays ${looped.totalPlays} times`);
310
156
  ```
311
157
 
312
158
  ### Combined Transformations
313
159
 
314
- #### `transformVideo(options)`
315
- Apply multiple transformations in a single operation: trim, mirror, and speed adjustment combined.
316
-
317
160
  ```typescript
318
- // Mirror and slow down (selfie video correction + slow-mo effect)
161
+ // Mirror + slow motion in one operation
319
162
  const transformed = await TwoStepVideo.transformVideo({
320
163
  assetId: asset.id,
321
164
  speed: 0.5,
322
- mirrorAxis: 'horizontal'
165
+ mirrorAxis: 'horizontal',
166
+ startTime: 0,
167
+ endTime: 10
323
168
  });
169
+ ```
324
170
 
325
- // Just mirror (speed defaults to 1.0)
326
- const mirrored = await TwoStepVideo.transformVideo({
327
- assetId: asset.id,
328
- mirrorAxis: 'both'
329
- });
171
+ ### Pan & Zoom
330
172
 
331
- // Trim + mirror + speed all at once
332
- const fullTransform = await TwoStepVideo.transformVideo({
173
+ ```typescript
174
+ // Apply pan/zoom for export (bakes transform into video)
175
+ const zoomed = await TwoStepVideo.panZoomVideo({
333
176
  assetId: asset.id,
334
- speed: 2.0,
335
- mirrorAxis: 'horizontal',
336
- startTime: 0,
337
- endTime: 10 // Take first 10 seconds, mirror it, speed it up
177
+ panX: 0.3, // Pan position (-1 to 1)
178
+ panY: -0.2,
179
+ zoomLevel: 1.5 // Zoom level (1 to 5)
338
180
  });
181
+ ```
182
+
183
+ ### Thumbnails
339
184
 
340
- // Speed up without mirroring (just provide speed)
341
- const fastVersion = await TwoStepVideo.transformVideo({
185
+ ```typescript
186
+ const thumbnails = await TwoStepVideo.generateThumbnails({
342
187
  assetId: asset.id,
343
- speed: 1.5,
344
- startTime: 5,
345
- endTime: 15
188
+ times: [0, 5, 10, 15],
189
+ size: { width: 300, height: 300 }
346
190
  });
191
+
192
+ // Use in Image component
193
+ <Image source={{ uri: `data:image/png;base64,${thumbnails[0]}` }} />
347
194
  ```
348
195
 
349
196
  ### Exporting
350
197
 
351
- #### `exportVideo(options)`
352
- Export a composition to a file.
353
-
354
198
  ```typescript
199
+ // Export composition
355
200
  const result = await TwoStepVideo.exportVideo({
356
201
  compositionId: composition.id,
357
- quality: TwoStepVideo.Quality.HIGH, // or LOW, MEDIUM, HIGHEST
358
- outputUri: 'file:///path/to/output.mp4' // optional
202
+ quality: TwoStepVideo.Quality.HIGH
359
203
  });
360
- ```
361
204
 
362
- #### `exportAsset(options)`
363
- Export an asset directly (without trimming).
364
-
365
- ```typescript
205
+ // Export asset directly
366
206
  const result = await TwoStepVideo.exportAsset({
367
207
  assetId: asset.id,
368
208
  quality: TwoStepVideo.Quality.MEDIUM
369
209
  });
370
- ```
371
-
372
- ### Video Player View
373
-
374
- #### `TwoStepVideoView`
375
- A native video player component for playing assets and compositions with full playback control.
376
210
 
377
- ```tsx
378
- import { TwoStepVideoView, TwoStepVideoViewRef } from 'expo-twostep-video';
379
- import { useRef, useState } from 'react';
380
- import { View, Button, Text } from 'react-native';
381
-
382
- function VideoPlayer({ compositionId }: { compositionId: string }) {
383
- const playerRef = useRef<TwoStepVideoViewRef>(null);
384
- const [status, setStatus] = useState<string>('ready');
385
- const [progress, setProgress] = useState(0);
386
-
387
- return (
388
- <View style={{ flex: 1 }}>
389
- <TwoStepVideoView
390
- ref={playerRef}
391
- compositionId={compositionId}
392
- loop={false}
393
- onPlaybackStatusChange={(e) => setStatus(e.nativeEvent.status)}
394
- onProgress={(e) => setProgress(e.nativeEvent.progress)}
395
- onEnd={() => console.log('Video ended')}
396
- onError={(e) => console.error(e.nativeEvent.error)}
397
- style={{ width: '100%', height: 300 }}
398
- />
399
-
400
- <Text>Status: {status} | Progress: {Math.round(progress * 100)}%</Text>
401
-
402
- <View style={{ flexDirection: 'row', gap: 10 }}>
403
- <Button title="Play" onPress={() => playerRef.current?.play()} />
404
- <Button title="Pause" onPress={() => playerRef.current?.pause()} />
405
- <Button title="Seek 5s" onPress={() => playerRef.current?.seek(5)} />
406
- <Button title="Replay" onPress={() => playerRef.current?.replay()} />
407
- </View>
408
- </View>
409
- );
410
- }
211
+ // Track progress
212
+ const subscription = TwoStepVideo.addExportProgressListener((event) => {
213
+ console.log(`${Math.round(event.progress * 100)}%`);
214
+ });
215
+ // Later: subscription.remove();
411
216
  ```
412
217
 
413
- #### Player Props
414
-
415
- | Prop | Type | Description |
416
- |------|------|-------------|
417
- | `compositionId` | `string` | ID from `mirrorVideo`, `trimVideo`, `loopSegment`, etc. |
418
- | `assetId` | `string` | ID from `loadAsset` (for playing without transformations) |
419
- | `loop` | `boolean` | Enable continuous looping (default: `false`) |
420
- | `onPlaybackStatusChange` | `function` | Called when status changes (`ready`, `playing`, `paused`, `ended`, `seeked`) |
421
- | `onProgress` | `function` | Called periodically with `{ currentTime, duration, progress }` |
422
- | `onEnd` | `function` | Called when video ends (not called if `loop` is true) |
423
- | `onError` | `function` | Called on playback error |
424
- | `onPanZoomChange` | `function` | Called when user pinches/pans with `{ panX, panY, zoomLevel }` |
425
-
426
- #### Player Methods (via ref)
218
+ ### Memory Management
427
219
 
428
220
  ```typescript
429
- const playerRef = useRef<TwoStepVideoViewRef>(null);
430
-
431
- // Start playback
432
- await playerRef.current?.play();
433
-
434
- // Pause playback
435
- await playerRef.current?.pause();
436
-
437
- // Seek to specific time (in seconds)
438
- await playerRef.current?.seek(10.5);
439
-
440
- // Restart from beginning
441
- await playerRef.current?.replay();
221
+ TwoStepVideo.releaseAsset(asset.id);
222
+ TwoStepVideo.releaseComposition(composition.id);
223
+ TwoStepVideo.releaseAll(); // Release everything
224
+ TwoStepVideo.cleanupFile(result.uri); // Delete temp file
442
225
  ```
443
226
 
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
227
+ ## Video Scrubber Component
449
228
 
450
- | Gesture | Action |
451
- |---------|--------|
452
- | Pinch (2 fingers) | Zoom in/out (1x to 5x) |
453
- | Drag (2 fingers) | Pan around when zoomed in |
229
+ ### VideoScrubber
454
230
 
455
- #### Pan/Zoom Events
456
-
457
- Listen for pan/zoom state changes with the `onPanZoomChange` prop:
231
+ A native-style video scrubber/trimmer with thumbnail strip, draggable trim handles, and a draggable playhead for frame-accurate seeking:
458
232
 
459
233
  ```tsx
460
- import { TwoStepVideoView, PanZoomState } from 'expo-twostep-video';
234
+ import { VideoScrubber, VideoScrubberRef } from 'expo-twostep-video';
461
235
 
462
- function VideoWithZoom() {
463
- const [panZoom, setPanZoom] = useState<PanZoomState>({
464
- panX: 0,
465
- panY: 0,
466
- zoomLevel: 1,
467
- });
236
+ function VideoEditor({ asset }: { asset: VideoAsset }) {
237
+ const playerRef = useRef<TwoStepVideoViewRef>(null);
238
+ const [trimStart, setTrimStart] = useState(0);
239
+ const [trimEnd, setTrimEnd] = useState(asset.duration);
240
+ const [currentTime, setCurrentTime] = useState(0);
468
241
 
469
242
  return (
470
243
  <View>
471
244
  <TwoStepVideoView
245
+ ref={playerRef}
472
246
  assetId={asset.id}
473
- onPanZoomChange={(e) => setPanZoom(e.nativeEvent)}
474
- style={{ width: '100%', height: 300 }}
247
+ onProgress={(e) => setCurrentTime(e.nativeEvent.currentTime)}
475
248
  />
476
249
 
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
- )}
250
+ <VideoScrubber
251
+ assetId={asset.id}
252
+ duration={asset.duration}
253
+ currentTime={currentTime}
254
+ startTime={trimStart}
255
+ endTime={trimEnd}
256
+ onStartTimeChange={setTrimStart}
257
+ onEndTimeChange={setTrimEnd}
258
+ onScrub={(time) => playerRef.current?.seek(time)}
259
+ onScrubbing={(time) => playerRef.current?.seek(time)}
260
+ />
483
261
  </View>
484
262
  );
485
263
  }
486
264
  ```
487
265
 
488
- #### Pan/Zoom Methods (via ref)
489
-
490
- Control pan/zoom programmatically using the player ref:
266
+ **Props:**
491
267
 
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:
268
+ | Prop | Type | Description |
269
+ |------|------|-------------|
270
+ | `assetId` | `string` | Asset ID for thumbnail generation |
271
+ | `duration` | `number` | Total video duration in seconds |
272
+ | `currentTime` | `number` | Current playback position (for playhead) |
273
+ | `startTime` | `number` | Selected start time in seconds |
274
+ | `endTime` | `number` | Selected end time in seconds |
275
+ | `thumbnailCount` | `number` | Number of thumbnails (default: 10) |
276
+ | `thumbnailHeight` | `number` | Height of thumbnail strip (default: 50) |
277
+ | `onStartTimeChange` | `function` | Called when start handle moves |
278
+ | `onEndTimeChange` | `function` | Called when end handle moves |
279
+ | `onScrub` | `function` | Called when playhead is tapped or dragged |
280
+ | `onScrubbing` | `function` | Called continuously during any drag |
281
+ | `onScrubEnd` | `function` | Called when drag ends |
282
+ | `minDuration` | `number` | Minimum selection duration (default: 0.5) |
283
+ | `showPlayhead` | `boolean` | Show playhead indicator (default: true) |
284
+ | `handleWidth` | `number` | Handle width in pixels (default: 20) |
285
+ | `theme` | `object` | Custom colors for handles, playhead, etc. |
286
+
287
+ **Features:**
288
+ - Drag the **yellow handles** to adjust trim start/end times
289
+ - Drag the **white playhead** to scrub through the video
290
+ - Tap anywhere in the selection to seek to that position
291
+ - All drag operations call `onScrubbing` for smooth video preview
292
+
293
+ ## Video Player Component
294
+
295
+ ### TwoStepVideoView
296
+
297
+ Native video player with pan/zoom gesture support:
535
298
 
536
299
  ```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';
300
+ import { TwoStepVideoView, TwoStepVideoViewRef } from 'expo-twostep-video';
545
301
 
546
- function PanZoomEditor({ assetId }: { assetId: string }) {
302
+ function VideoPlayer({ compositionId }: { compositionId: string }) {
547
303
  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
304
 
584
305
  return (
585
- <View style={styles.container}>
586
- {/* Video Preview with Gestures */}
306
+ <View>
587
307
  <TwoStepVideoView
588
308
  ref={playerRef}
589
- assetId={assetId}
590
- onPanZoomChange={(e) => setPanZoom(e.nativeEvent)}
591
- style={styles.video}
309
+ compositionId={compositionId}
310
+ loop={false}
311
+ onPlaybackStatusChange={(e) => console.log(e.nativeEvent.status)}
312
+ onProgress={(e) => console.log(e.nativeEvent.progress)}
313
+ onPanZoomChange={(e) => console.log(e.nativeEvent.zoomLevel)}
314
+ style={{ width: '100%', height: 300 }}
592
315
  />
593
316
 
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>
317
+ <Button title="Play" onPress={() => playerRef.current?.play()} />
318
+ <Button title="Pause" onPress={() => playerRef.current?.pause()} />
319
+ <Button title="Seek 5s" onPress={() => playerRef.current?.seek(5)} />
621
320
  </View>
622
321
  );
623
322
  }
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
323
  ```
635
324
 
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
325
+ **Props:**
642
326
 
643
- ### Events
644
-
645
- #### `addExportProgressListener(callback)`
646
- Listen for export progress updates.
327
+ | Prop | Type | Description |
328
+ |------|------|-------------|
329
+ | `compositionId` | `string` | ID from trimVideo, mirrorVideo, etc. |
330
+ | `assetId` | `string` | ID from loadAsset (for raw playback) |
331
+ | `loop` | `boolean` | Enable continuous looping |
332
+ | `minZoom` | `number` | Minimum zoom level (default: 1.0) |
333
+ | `maxZoom` | `number` | Maximum zoom level (default: 5.0) |
334
+ | `onPlaybackStatusChange` | `function` | Status: ready, playing, paused, ended |
335
+ | `onProgress` | `function` | Progress: currentTime, duration, progress |
336
+ | `onPanZoomChange` | `function` | Pan/zoom: panX, panY, zoomLevel |
337
+ | `onEnd` | `function` | Called when playback ends |
338
+ | `onError` | `function` | Called on error |
339
+
340
+ **Ref Methods:**
647
341
 
648
342
  ```typescript
649
- const subscription = TwoStepVideo.addExportProgressListener((event) => {
650
- console.log(`${Math.round(event.progress * 100)}%`);
651
- });
343
+ await playerRef.current?.play();
344
+ await playerRef.current?.pause();
345
+ await playerRef.current?.seek(10.5);
346
+ await playerRef.current?.replay();
652
347
 
653
- // Remove listener
654
- subscription.remove();
348
+ // Pan/zoom control
349
+ const state = await playerRef.current?.getPanZoomState();
350
+ await playerRef.current?.setPanZoomState({ zoomLevel: 2.0 });
351
+ await playerRef.current?.resetPanZoom();
655
352
  ```
656
353
 
657
- ### Memory Management
354
+ ### TwoStepPlayerControllerView
658
355
 
659
- ```typescript
660
- // Release assets when done
661
- TwoStepVideo.releaseAsset(asset.id);
662
- TwoStepVideo.releaseComposition(composition.id);
356
+ Native iOS player with system controls (AirPlay, PiP, fullscreen):
663
357
 
664
- // Release all at once
665
- TwoStepVideo.releaseAll();
358
+ ```tsx
359
+ import { TwoStepPlayerControllerView } from 'expo-twostep-video';
666
360
 
667
- // Clean up temp files
668
- TwoStepVideo.cleanupFile(tempFileUri);
361
+ <TwoStepPlayerControllerView
362
+ assetId={asset.id}
363
+ showsPlaybackControls={true}
364
+ style={{ width: '100%', height: 300 }}
365
+ />
669
366
  ```
670
367
 
671
368
  ## Constants
@@ -673,575 +370,49 @@ TwoStepVideo.cleanupFile(tempFileUri);
673
370
  ### Quality Presets
674
371
 
675
372
  ```typescript
676
- TwoStepVideo.Quality.LOW // ~0.1 bits per pixel - fast, small files
677
- TwoStepVideo.Quality.MEDIUM // ~0.2 bits per pixel - good for web/social
678
- TwoStepVideo.Quality.HIGH // ~0.4 bits per pixel (recommended default)
679
- TwoStepVideo.Quality.HIGHEST // ~0.8 bits per pixel - archival quality
373
+ TwoStepVideo.Quality.LOW // Fast, small files
374
+ TwoStepVideo.Quality.MEDIUM // Good for web/social
375
+ TwoStepVideo.Quality.HIGH // Recommended default
376
+ TwoStepVideo.Quality.HIGHEST // Archival quality
680
377
  ```
681
378
 
682
379
  ### Mirror Axis
683
380
 
684
381
  ```typescript
685
- TwoStepVideo.Mirror.HORIZONTAL // Flip left-right (selfie correction)
382
+ TwoStepVideo.Mirror.HORIZONTAL // Flip left-right
686
383
  TwoStepVideo.Mirror.VERTICAL // Flip top-bottom
687
- TwoStepVideo.Mirror.BOTH // Flip both axes (180ยฐ rotation effect)
688
- ```
689
-
690
- ### Speed Ranges
691
-
692
- | Speed | Effect | Duration Change |
693
- |-------|--------|-----------------|
694
- | 0.25 | Very slow motion | 4x longer |
695
- | 0.5 | Slow motion | 2x longer |
696
- | 1.0 | Normal speed | No change |
697
- | 2.0 | Fast forward | 2x shorter |
698
- | 4.0 | Timelapse | 4x shorter |
699
-
700
- ## TypeScript Support
701
-
702
- Full TypeScript definitions included:
703
-
704
- ```typescript
705
- // Core types
706
- interface VideoAsset {
707
- id: string;
708
- duration: number;
709
- width: number;
710
- height: number;
711
- frameRate: number;
712
- hasAudio: boolean;
713
- }
714
-
715
- interface VideoComposition {
716
- id: string;
717
- duration: number;
718
- }
719
-
720
- interface ExportResult {
721
- uri: string;
722
- path: string;
723
- }
724
-
725
- // Transformation types
726
- type MirrorAxis = 'horizontal' | 'vertical' | 'both';
727
- type VideoQuality = 'low' | 'medium' | 'high' | 'highest';
728
-
729
- interface MirrorVideoOptions {
730
- assetId: string;
731
- axis: MirrorAxis;
732
- startTime?: number; // Optional: mirror only a segment
733
- endTime?: number;
734
- }
735
-
736
- interface AdjustSpeedOptions {
737
- assetId: string;
738
- speed: number; // 0.25 to 4.0
739
- startTime?: number;
740
- endTime?: number;
741
- }
742
-
743
- interface LoopSegmentOptions {
744
- assetId: string;
745
- startTime: number;
746
- endTime: number;
747
- loopCount: number; // Repeats after first play
748
- }
749
-
750
- interface LoopResult {
751
- id: string;
752
- duration: number;
753
- loopCount: number;
754
- totalPlays: number; // loopCount + 1
755
- }
756
-
757
- interface TransformVideoOptions {
758
- assetId: string;
759
- speed?: number;
760
- mirrorAxis?: MirrorAxis;
761
- startTime?: number;
762
- endTime?: number;
763
- }
764
-
765
- // Player types
766
- interface TwoStepVideoViewProps {
767
- compositionId?: string;
768
- assetId?: string;
769
- loop?: boolean;
770
- onPlaybackStatusChange?: (event: { nativeEvent: PlaybackStatusEvent }) => void;
771
- onProgress?: (event: { nativeEvent: ProgressEvent }) => void;
772
- onEnd?: (event: { nativeEvent: {} }) => void;
773
- onError?: (event: { nativeEvent: ErrorEvent }) => void;
774
- onPanZoomChange?: (event: { nativeEvent: PanZoomState }) => void;
775
- style?: ViewStyle;
776
- }
777
-
778
- interface TwoStepVideoViewRef {
779
- play: () => Promise<void>;
780
- pause: () => Promise<void>;
781
- seek: (time: number) => Promise<void>;
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;
802
- }
384
+ TwoStepVideo.Mirror.BOTH // Flip both
803
385
  ```
804
386
 
805
- ## Advanced Usage
806
-
807
- ### Create a Highlight Reel
387
+ ## TypeScript Types
808
388
 
809
389
  ```typescript
810
- async function createHighlightReel(videoUri: string) {
811
- const asset = await TwoStepVideo.loadAsset({ uri: videoUri });
812
-
813
- // Extract 3-second clips evenly throughout video
814
- const numClips = 5;
815
- const interval = asset.duration / (numClips + 1);
816
-
817
- const segments = Array.from({ length: numClips }, (_, i) => ({
818
- start: interval * (i + 1),
819
- end: Math.min(interval * (i + 1) + 3, asset.duration)
820
- }));
821
-
822
- const composition = await TwoStepVideo.trimVideoMultiple({
823
- assetId: asset.id,
824
- segments
825
- });
826
-
827
- return await TwoStepVideo.exportVideo({
828
- compositionId: composition.id,
829
- quality: TwoStepVideo.Quality.HIGH
830
- });
831
- }
832
- ```
833
-
834
- ### Selfie Video Mirror & Loop
835
-
836
- A complete example for processing selfie videos with mirror correction and perfect loops:
837
-
838
- ```tsx
839
- import React, { useState, useRef, useEffect } from 'react';
840
- import { View, Button, Text, StyleSheet, ActivityIndicator } from 'react-native';
841
- import * as TwoStepVideo from 'expo-twostep-video';
842
- import { TwoStepVideoView, TwoStepVideoViewRef } from 'expo-twostep-video';
843
-
844
- interface Props {
845
- videoUri: string;
846
- }
847
-
848
- function SelfieVideoEditor({ videoUri }: Props) {
849
- const playerRef = useRef<TwoStepVideoViewRef>(null);
850
- const [asset, setAsset] = useState<TwoStepVideo.VideoAsset | null>(null);
851
- const [composition, setComposition] = useState<TwoStepVideo.VideoComposition | null>(null);
852
- const [isProcessing, setIsProcessing] = useState(false);
853
- const [exportProgress, setExportProgress] = useState(0);
854
-
855
- // Load the video asset
856
- useEffect(() => {
857
- async function loadVideo() {
858
- const loaded = await TwoStepVideo.loadAsset({ uri: videoUri });
859
- setAsset(loaded);
860
- }
861
- loadVideo();
862
-
863
- return () => {
864
- // Cleanup on unmount
865
- TwoStepVideo.releaseAll();
866
- };
867
- }, [videoUri]);
868
-
869
- // Mirror the selfie video (fix the mirror effect from front camera)
870
- const handleMirror = async () => {
871
- if (!asset) return;
872
- setIsProcessing(true);
873
-
874
- const mirrored = await TwoStepVideo.mirrorVideo({
875
- assetId: asset.id,
876
- axis: 'horizontal'
877
- });
878
-
879
- setComposition(mirrored);
880
- setIsProcessing(false);
881
- };
882
-
883
- // Create a slow-motion effect
884
- const handleSlowMo = async () => {
885
- if (!asset) return;
886
- setIsProcessing(true);
887
-
888
- const slowMo = await TwoStepVideo.adjustSpeed({
889
- assetId: asset.id,
890
- speed: 0.5 // Half speed
891
- });
892
-
893
- setComposition(slowMo);
894
- setIsProcessing(false);
895
- };
896
-
897
- // Mirror + slow motion combined
898
- const handleMirrorAndSlowMo = async () => {
899
- if (!asset) return;
900
- setIsProcessing(true);
901
-
902
- const transformed = await TwoStepVideo.transformVideo({
903
- assetId: asset.id,
904
- speed: 0.5,
905
- mirrorAxis: 'horizontal'
906
- });
907
-
908
- setComposition(transformed);
909
- setIsProcessing(false);
910
- };
911
-
912
- // Create a perfect loop for social media
913
- const handleCreateLoop = async () => {
914
- if (!asset) return;
915
- setIsProcessing(true);
916
-
917
- // Loop the first 3 seconds to create 15 seconds of content
918
- const looped = await TwoStepVideo.loopSegment({
919
- assetId: asset.id,
920
- startTime: 0,
921
- endTime: 3,
922
- loopCount: 4 // 3s * 5 plays = 15 seconds
923
- });
924
-
925
- setComposition(looped);
926
- setIsProcessing(false);
927
- console.log(`Created ${looped.totalPlays}-play loop, ${looped.duration}s duration`);
928
- };
929
-
930
- // Export the result
931
- const handleExport = async () => {
932
- if (!composition) return;
933
-
934
- const subscription = TwoStepVideo.addExportProgressListener((event) => {
935
- setExportProgress(event.progress);
936
- });
937
-
938
- try {
939
- const result = await TwoStepVideo.exportVideo({
940
- compositionId: composition.id,
941
- quality: 'high'
942
- });
943
-
944
- alert(`Exported to: ${result.uri}`);
945
- } finally {
946
- subscription.remove();
947
- setExportProgress(0);
948
- }
949
- };
950
-
951
- if (!asset) {
952
- return <ActivityIndicator size="large" />;
953
- }
954
-
955
- return (
956
- <View style={styles.container}>
957
- {/* Video Player */}
958
- <TwoStepVideoView
959
- ref={playerRef}
960
- compositionId={composition?.id}
961
- assetId={!composition ? asset.id : undefined}
962
- loop={true}
963
- style={styles.video}
964
- />
965
-
966
- {/* Video Info */}
967
- <Text style={styles.info}>
968
- Duration: {composition?.duration.toFixed(1) || asset.duration.toFixed(1)}s |
969
- Size: {asset.width}x{asset.height}
970
- </Text>
971
-
972
- {/* Processing Indicator */}
973
- {isProcessing && <ActivityIndicator size="small" />}
974
-
975
- {/* Transformation Buttons */}
976
- <View style={styles.buttons}>
977
- <Button title="Mirror" onPress={handleMirror} disabled={isProcessing} />
978
- <Button title="Slow-Mo" onPress={handleSlowMo} disabled={isProcessing} />
979
- <Button title="Mirror + Slow" onPress={handleMirrorAndSlowMo} disabled={isProcessing} />
980
- <Button title="Create Loop" onPress={handleCreateLoop} disabled={isProcessing} />
981
- </View>
982
-
983
- {/* Playback Controls */}
984
- <View style={styles.controls}>
985
- <Button title="Play" onPress={() => playerRef.current?.play()} />
986
- <Button title="Pause" onPress={() => playerRef.current?.pause()} />
987
- <Button title="Replay" onPress={() => playerRef.current?.replay()} />
988
- </View>
989
-
990
- {/* Export */}
991
- {composition && (
992
- <View style={styles.export}>
993
- <Button title="Export Video" onPress={handleExport} />
994
- {exportProgress > 0 && (
995
- <Text>Exporting: {Math.round(exportProgress * 100)}%</Text>
996
- )}
997
- </View>
998
- )}
999
- </View>
1000
- );
1001
- }
1002
-
1003
- const styles = StyleSheet.create({
1004
- container: { flex: 1, padding: 16 },
1005
- video: { width: '100%', height: 300, backgroundColor: '#000' },
1006
- info: { textAlign: 'center', marginVertical: 8 },
1007
- buttons: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginVertical: 8 },
1008
- controls: { flexDirection: 'row', justifyContent: 'center', gap: 16 },
1009
- export: { marginTop: 16, alignItems: 'center' },
1010
- });
1011
-
1012
- export default SelfieVideoEditor;
1013
- ```
1014
-
1015
- ### Timestamp-Based Effects
1016
-
1017
- Apply different effects to specific timestamps/segments of a video:
1018
-
1019
- ```typescript
1020
- import * as TwoStepVideo from 'expo-twostep-video';
1021
-
1022
- async function applyTimestampEffects(videoUri: string) {
1023
- const asset = await TwoStepVideo.loadAsset({ uri: videoUri });
1024
-
1025
- // Example: Mirror only the segment from 5s to 10s
1026
- const mirroredSegment = await TwoStepVideo.mirrorVideo({
1027
- assetId: asset.id,
1028
- axis: 'horizontal',
1029
- startTime: 5,
1030
- endTime: 10
1031
- });
1032
-
1033
- // Example: Speed up only a boring middle section (10s to 30s at 4x speed)
1034
- const timelapseMiddle = await TwoStepVideo.adjustSpeed({
1035
- assetId: asset.id,
1036
- speed: 4.0,
1037
- startTime: 10,
1038
- endTime: 30
1039
- });
1040
-
1041
- // Example: Transform a specific segment (trim + mirror + speed)
1042
- const transformedClip = await TwoStepVideo.transformVideo({
1043
- assetId: asset.id,
1044
- speed: 0.5, // Slow motion
1045
- mirrorAxis: 'horizontal',
1046
- startTime: 45, // Best moment starts at 45s
1047
- endTime: 50 // 5 seconds of slow-mo mirrored footage
1048
- });
1049
-
1050
- return transformedClip;
1051
- }
1052
- ```
1053
-
1054
- ### Create Social Media Loop with Mirror
1055
-
1056
- Perfect for TikTok/Instagram Reels - select a short segment, mirror it, and loop it:
1057
-
1058
- ```typescript
1059
- async function createSocialMediaLoop(
1060
- videoUri: string,
1061
- startTime: number,
1062
- endTime: number,
1063
- targetDuration: number = 15
1064
- ) {
1065
- const asset = await TwoStepVideo.loadAsset({ uri: videoUri });
1066
-
1067
- // First, create the mirrored and trimmed base clip
1068
- const mirroredClip = await TwoStepVideo.transformVideo({
1069
- assetId: asset.id,
1070
- mirrorAxis: 'horizontal',
1071
- startTime,
1072
- endTime
1073
- });
1074
-
1075
- // Export the mirrored clip first (loopSegment needs the original asset)
1076
- const exportedMirror = await TwoStepVideo.exportVideo({
1077
- compositionId: mirroredClip.id,
1078
- quality: 'high'
1079
- });
1080
-
1081
- // Load the exported mirrored video
1082
- const mirroredAsset = await TwoStepVideo.loadAsset({
1083
- uri: exportedMirror.uri
1084
- });
1085
-
1086
- // Calculate how many loops needed to reach target duration
1087
- const clipDuration = endTime - startTime;
1088
- const loopCount = Math.ceil(targetDuration / clipDuration) - 1;
1089
-
1090
- // Create the loop
1091
- const looped = await TwoStepVideo.loopSegment({
1092
- assetId: mirroredAsset.id,
1093
- startTime: 0,
1094
- endTime: mirroredAsset.duration,
1095
- loopCount
1096
- });
1097
-
1098
- console.log(`Created ${looped.duration}s loop from ${clipDuration}s clip`);
1099
-
1100
- // Export final result
1101
- const result = await TwoStepVideo.exportVideo({
1102
- compositionId: looped.id,
1103
- quality: 'high'
1104
- });
1105
-
1106
- // Cleanup intermediate files
1107
- TwoStepVideo.cleanupFile(exportedMirror.uri);
1108
-
1109
- return result;
1110
- }
1111
-
1112
- // Usage
1113
- const socialLoop = await createSocialMediaLoop(
1114
- 'file:///path/to/selfie.mp4',
1115
- 5.0, // Start at 5 seconds
1116
- 8.0, // End at 8 seconds (3 second clip)
1117
- 15 // Target 15 seconds for Instagram
1118
- );
1119
- ```
1120
-
1121
- ### With Progress Bar
1122
-
1123
- ```typescript
1124
- function VideoExporter({ videoUri }) {
1125
- const progress = TwoStepVideo.useExportProgress();
1126
-
1127
- return (
1128
- <View>
1129
- <ProgressBar progress={progress} />
1130
- <Text>{Math.round(progress * 100)}%</Text>
1131
- </View>
1132
- );
1133
- }
1134
- ```
1135
-
1136
- ## Development
1137
-
1138
- This module contains both the Swift library and Expo bridge in one package.
1139
-
1140
- ### Running Tests
1141
-
1142
- ```bash
1143
- # Run Swift unit tests
1144
- npm run test:swift
1145
-
1146
- # All tests should pass:
1147
- # โœ… 69 tests executed, 20 skipped (integration tests)
1148
- ```
1149
-
1150
- ### Building
1151
-
1152
- ```bash
1153
- # Build TypeScript
1154
- npm run build
1155
-
1156
- # Clean build artifacts
1157
- npm run clean
1158
- ```
1159
-
1160
- ### Running Example App
1161
-
1162
- ```bash
1163
- cd example
1164
- npm install
1165
- npm run ios
1166
- ```
1167
-
1168
- ## Architecture
1169
-
1170
- ```
1171
- expo-twostep-video/
1172
- โ”œโ”€โ”€ ios/
1173
- โ”‚ โ”œโ”€โ”€ TwoStepVideo/ # Swift library
1174
- โ”‚ โ”‚ โ”œโ”€โ”€ Models/ # Data models
1175
- โ”‚ โ”‚ โ”œโ”€โ”€ Core/ # Core functionality
1176
- โ”‚ โ”‚ โ””โ”€โ”€ TwoStepVideo.swift # Main facade
1177
- โ”‚ โ”œโ”€โ”€ Tests/ # Swift unit tests
1178
- โ”‚ โ”œโ”€โ”€ ExpoTwoStepVideoModule.swift # Expo bridge
1179
- โ”‚ โ””โ”€โ”€ Package.swift # Swift package for testing
1180
- โ”œโ”€โ”€ src/
1181
- โ”‚ โ””โ”€โ”€ index.ts # TypeScript API
1182
- โ”œโ”€โ”€ example/ # Example React Native app
1183
- โ””โ”€โ”€ docs/ # Documentation
390
+ import type {
391
+ VideoAsset,
392
+ VideoComposition,
393
+ ExportResult,
394
+ LoopResult,
395
+ TwoStepVideoViewRef,
396
+ TwoStepVideoViewProps,
397
+ PanZoomState,
398
+ MirrorAxis,
399
+ VideoQuality,
400
+ VideoScrubberProps,
401
+ VideoScrubberRef,
402
+ VideoScrubberTheme,
403
+ } from 'expo-twostep-video';
1184
404
  ```
1185
405
 
1186
- ## Documentation
1187
-
1188
- - [Architecture](./docs/ARCHITECTURE.md) - Design and implementation details
1189
- - [Developer Guide](./docs/CLAUDE.md) - Best practices and patterns
1190
- - [Temp File Management](./docs/TEMP_FILE_MANAGEMENT.md) - File cleanup strategies
1191
-
1192
- ## Requirements
1193
-
1194
- - iOS 15.0+
1195
- - Expo SDK 50+
1196
- - React Native 0.72+
1197
-
1198
406
  ## Platform Support
1199
407
 
1200
408
  - โœ… iOS (fully supported)
1201
409
  - โณ Android (coming soon)
1202
410
 
1203
- ## Error Handling
1204
-
1205
- All errors include descriptive codes and messages:
1206
-
1207
- ```typescript
1208
- try {
1209
- await TwoStepVideo.exportVideo(options);
1210
- } catch (error: any) {
1211
- switch (error.code) {
1212
- case 'EXPORT_FAILED':
1213
- // Handle export failure
1214
- break;
1215
- case 'ASSET_NOT_FOUND':
1216
- // Asset was released too early
1217
- break;
1218
- default:
1219
- console.error('Unknown error:', error);
1220
- }
1221
- }
1222
- ```
1223
-
1224
- ## Performance Tips
1225
-
1226
- 1. **Release resources promptly** after use
1227
- 2. **Use `releaseAll()`** when unmounting components
1228
- 3. **Validate URIs** before loading with `validateVideoUri()`
1229
- 4. **Clean up temp files** after moving/uploading
1230
-
1231
- ## Contributing
1232
-
1233
- Contributions welcome! Please see [CONTRIBUTING](./docs/CONTRIBUTING.md).
1234
-
1235
411
  ## License
1236
412
 
1237
413
  MIT ยฉ Richard Guo
1238
414
 
1239
- ## Support
1240
-
1241
- - ๐Ÿ“ง Email: richardg7890@gmail.com
1242
- - ๐Ÿ› Issues: [GitHub Issues](https://github.com/rguo123/twostep-video/issues)
1243
- - ๐Ÿ“š Docs: [Full Documentation](./docs/)
1244
-
1245
- ## Acknowledgments
415
+ ## Links
1246
416
 
1247
- Built with AVFoundation and Expo Modules.
417
+ - [GitHub Repository](https://github.com/rguo123/twostep-video)
418
+ - [Issue Tracker](https://github.com/rguo123/twostep-video/issues)