@movementinfra/expo-twostep-video 0.1.11 โ†’ 0.1.13

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,433 +41,262 @@ 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
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
-
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
- | `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
-
136
65
  ### Loading Videos
137
66
 
138
- #### `loadAsset(options)`
139
- Load a video from a file URI.
140
-
141
67
  ```typescript
142
- const asset = await TwoStepVideo.loadAsset({
143
- uri: 'file:///path/to/video.mp4'
144
- });
68
+ // Load from file URI
69
+ const asset = await TwoStepVideo.loadAsset({ uri: 'file:///path/to/video.mp4' });
145
70
  // Returns: { id, duration, width, height, frameRate, hasAudio }
146
- ```
147
71
 
148
- #### `loadAssetFromPhotos(localIdentifier)`
149
- Load a video from the Photos library.
150
-
151
- ```typescript
72
+ // Load from Photos library
152
73
  const asset = await TwoStepVideo.loadAssetFromPhotos(photoAsset.id);
153
- ```
154
-
155
- #### `validateVideoUri(uri)`
156
- Quickly validate a video URI without loading.
157
74
 
158
- ```typescript
75
+ // Validate without loading
159
76
  const isValid = await TwoStepVideo.validateVideoUri(uri);
160
77
  ```
161
78
 
162
79
  ### Trimming
163
80
 
164
- #### `trimVideo(options)`
165
- Trim to a single time range.
166
-
167
81
  ```typescript
82
+ // Single segment
168
83
  const composition = await TwoStepVideo.trimVideo({
169
84
  assetId: asset.id,
170
- startTime: 5.0, // seconds
85
+ startTime: 5.0,
171
86
  endTime: 15.0
172
87
  });
173
- ```
174
-
175
- #### `trimVideoMultiple(options)`
176
- Create a composition from multiple segments.
177
88
 
178
- ```typescript
89
+ // Multiple segments (highlight reel)
179
90
  const composition = await TwoStepVideo.trimVideoMultiple({
180
91
  assetId: asset.id,
181
92
  segments: [
182
- { start: 0, end: 5 },
93
+ { start: 0, end: 3 },
183
94
  { start: 10, end: 15 },
184
95
  { start: 20, end: 25 }
185
96
  ]
186
97
  });
187
98
  ```
188
99
 
189
- ### Thumbnails
190
-
191
- #### `generateThumbnails(options)`
192
- Extract thumbnail images at specific times.
193
-
194
- ```typescript
195
- const thumbnails = await TwoStepVideo.generateThumbnails({
196
- assetId: asset.id,
197
- times: [0, 5, 10, 15],
198
- size: { width: 300, height: 300 }
199
- });
200
-
201
- // Use in Image component
202
- <Image source={{ uri: `data:image/png;base64,${thumbnails[0]}` }} />
203
- ```
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.
100
+ ### Mirroring
209
101
 
210
102
  ```typescript
211
- // Mirror horizontally (flip left-right) - common for selfie videos
103
+ // Horizontal (flip left-right) - common for selfie videos
212
104
  const mirrored = await TwoStepVideo.mirrorVideo({
213
105
  assetId: asset.id,
214
106
  axis: 'horizontal'
215
107
  });
216
108
 
217
- // Mirror vertically (flip top-bottom)
109
+ // Vertical (flip top-bottom)
218
110
  const flipped = await TwoStepVideo.mirrorVideo({
219
111
  assetId: asset.id,
220
112
  axis: 'vertical'
221
113
  });
222
114
 
223
- // Mirror both axes (180ยฐ rotation effect)
115
+ // Both axes
224
116
  const both = await TwoStepVideo.mirrorVideo({
225
117
  assetId: asset.id,
226
118
  axis: 'both'
227
119
  });
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
120
  ```
237
121
 
238
122
  ### Speed Adjustment
239
123
 
240
- #### `adjustSpeed(options)`
241
- Change the playback speed of a video. Supports slow motion (< 1.0) and fast forward (> 1.0).
242
-
243
124
  ```typescript
244
- // Slow motion (0.5x = 2x slower, video becomes twice as long)
125
+ // Slow motion (0.5x = 2x slower)
245
126
  const slowMo = await TwoStepVideo.adjustSpeed({
246
127
  assetId: asset.id,
247
128
  speed: 0.5
248
129
  });
249
130
 
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({
131
+ // Fast forward (2x speed)
132
+ const fast = await TwoStepVideo.adjustSpeed({
258
133
  assetId: asset.id,
259
134
  speed: 2.0
260
135
  });
261
136
 
262
- // Timelapse effect (4x speed)
137
+ // Timelapse (4x speed)
263
138
  const timelapse = await TwoStepVideo.adjustSpeed({
264
139
  assetId: asset.id,
265
140
  speed: 4.0
266
141
  });
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
142
  ```
276
143
 
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.
144
+ ### Looping
281
145
 
282
146
  ```typescript
283
- // 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)
284
148
  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
149
  assetId: asset.id,
297
150
  startTime: 0,
298
151
  endTime: 3,
299
- loopCount: 4 // 3s * 5 plays = 15 seconds
152
+ loopCount: 4
300
153
  });
301
154
 
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
- });
155
+ console.log(`Duration: ${looped.duration}s, plays ${looped.totalPlays} times`);
309
156
  ```
310
157
 
311
158
  ### Combined Transformations
312
159
 
313
- #### `transformVideo(options)`
314
- Apply multiple transformations in a single operation: trim, mirror, and speed adjustment combined.
315
-
316
160
  ```typescript
317
- // Mirror and slow down (selfie video correction + slow-mo effect)
161
+ // Mirror + slow motion in one operation
318
162
  const transformed = await TwoStepVideo.transformVideo({
319
163
  assetId: asset.id,
320
164
  speed: 0.5,
321
- mirrorAxis: 'horizontal'
165
+ mirrorAxis: 'horizontal',
166
+ startTime: 0,
167
+ endTime: 10
322
168
  });
169
+ ```
323
170
 
324
- // Just mirror (speed defaults to 1.0)
325
- const mirrored = await TwoStepVideo.transformVideo({
326
- assetId: asset.id,
327
- mirrorAxis: 'both'
328
- });
171
+ ### Pan & Zoom
329
172
 
330
- // Trim + mirror + speed all at once
331
- const fullTransform = await TwoStepVideo.transformVideo({
173
+ ```typescript
174
+ // Apply pan/zoom for export (bakes transform into video)
175
+ const zoomed = await TwoStepVideo.panZoomVideo({
332
176
  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
177
+ panX: 0.3, // Pan position (-1 to 1)
178
+ panY: -0.2,
179
+ zoomLevel: 1.5 // Zoom level (1 to 5)
337
180
  });
181
+ ```
182
+
183
+ ### Thumbnails
338
184
 
339
- // Speed up without mirroring (just provide speed)
340
- const fastVersion = await TwoStepVideo.transformVideo({
185
+ ```typescript
186
+ const thumbnails = await TwoStepVideo.generateThumbnails({
341
187
  assetId: asset.id,
342
- speed: 1.5,
343
- startTime: 5,
344
- endTime: 15
188
+ times: [0, 5, 10, 15],
189
+ size: { width: 300, height: 300 }
345
190
  });
191
+
192
+ // Use in Image component
193
+ <Image source={{ uri: `data:image/png;base64,${thumbnails[0]}` }} />
346
194
  ```
347
195
 
348
196
  ### Exporting
349
197
 
350
- #### `exportVideo(options)`
351
- Export a composition to a file.
352
-
353
198
  ```typescript
199
+ // Export composition
354
200
  const result = await TwoStepVideo.exportVideo({
355
201
  compositionId: composition.id,
356
- quality: TwoStepVideo.Quality.HIGH, // or LOW, MEDIUM, HIGHEST
357
- outputUri: 'file:///path/to/output.mp4' // optional
202
+ quality: TwoStepVideo.Quality.HIGH
358
203
  });
359
- ```
360
-
361
- #### `exportAsset(options)`
362
- Export an asset directly (without trimming).
363
204
 
364
- ```typescript
205
+ // Export asset directly
365
206
  const result = await TwoStepVideo.exportAsset({
366
207
  assetId: asset.id,
367
208
  quality: TwoStepVideo.Quality.MEDIUM
368
209
  });
210
+
211
+ // Track progress
212
+ const subscription = TwoStepVideo.addExportProgressListener((event) => {
213
+ console.log(`${Math.round(event.progress * 100)}%`);
214
+ });
215
+ // Later: subscription.remove();
369
216
  ```
370
217
 
371
- ### Video Player View
218
+ ### Memory Management
372
219
 
373
- #### `TwoStepVideoView`
374
- A native video player component for playing assets and compositions with full playback control.
220
+ ```typescript
221
+ TwoStepVideo.releaseAsset(asset.id);
222
+ TwoStepVideo.releaseComposition(composition.id);
223
+ TwoStepVideo.releaseAll(); // Release everything
224
+ TwoStepVideo.cleanupFile(result.uri); // Delete temp file
225
+ ```
226
+
227
+ ## Video Player Component
228
+
229
+ ### TwoStepVideoView
230
+
231
+ Native video player with pan/zoom gesture support:
375
232
 
376
233
  ```tsx
377
234
  import { TwoStepVideoView, TwoStepVideoViewRef } from 'expo-twostep-video';
378
- import { useRef, useState } from 'react';
379
- import { View, Button, Text } from 'react-native';
380
235
 
381
236
  function VideoPlayer({ compositionId }: { compositionId: string }) {
382
237
  const playerRef = useRef<TwoStepVideoViewRef>(null);
383
- const [status, setStatus] = useState<string>('ready');
384
- const [progress, setProgress] = useState(0);
385
238
 
386
239
  return (
387
- <View style={{ flex: 1 }}>
240
+ <View>
388
241
  <TwoStepVideoView
389
242
  ref={playerRef}
390
243
  compositionId={compositionId}
391
244
  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)}
245
+ onPlaybackStatusChange={(e) => console.log(e.nativeEvent.status)}
246
+ onProgress={(e) => console.log(e.nativeEvent.progress)}
247
+ onPanZoomChange={(e) => console.log(e.nativeEvent.zoomLevel)}
396
248
  style={{ width: '100%', height: 300 }}
397
249
  />
398
250
 
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>
251
+ <Button title="Play" onPress={() => playerRef.current?.play()} />
252
+ <Button title="Pause" onPress={() => playerRef.current?.pause()} />
253
+ <Button title="Seek 5s" onPress={() => playerRef.current?.seek(5)} />
407
254
  </View>
408
255
  );
409
256
  }
410
257
  ```
411
258
 
412
- #### Player Props
259
+ **Props:**
413
260
 
414
261
  | Prop | Type | Description |
415
262
  |------|------|-------------|
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)
263
+ | `compositionId` | `string` | ID from trimVideo, mirrorVideo, etc. |
264
+ | `assetId` | `string` | ID from loadAsset (for raw playback) |
265
+ | `loop` | `boolean` | Enable continuous looping |
266
+ | `minZoom` | `number` | Minimum zoom level (default: 1.0) |
267
+ | `maxZoom` | `number` | Maximum zoom level (default: 5.0) |
268
+ | `onPlaybackStatusChange` | `function` | Status: ready, playing, paused, ended |
269
+ | `onProgress` | `function` | Progress: currentTime, duration, progress |
270
+ | `onPanZoomChange` | `function` | Pan/zoom: panX, panY, zoomLevel |
271
+ | `onEnd` | `function` | Called when playback ends |
272
+ | `onError` | `function` | Called on error |
273
+
274
+ **Ref Methods:**
425
275
 
426
276
  ```typescript
427
- const playerRef = useRef<TwoStepVideoViewRef>(null);
428
-
429
- // Start playback
430
277
  await playerRef.current?.play();
431
-
432
- // Pause playback
433
278
  await playerRef.current?.pause();
434
-
435
- // Seek to specific time (in seconds)
436
279
  await playerRef.current?.seek(10.5);
437
-
438
- // Restart from beginning
439
280
  await playerRef.current?.replay();
440
- ```
441
-
442
- ### Events
443
-
444
- #### `addExportProgressListener(callback)`
445
- Listen for export progress updates.
446
281
 
447
- ```typescript
448
- const subscription = TwoStepVideo.addExportProgressListener((event) => {
449
- console.log(`${Math.round(event.progress * 100)}%`);
450
- });
451
-
452
- // Remove listener
453
- subscription.remove();
282
+ // Pan/zoom control
283
+ const state = await playerRef.current?.getPanZoomState();
284
+ await playerRef.current?.setPanZoomState({ zoomLevel: 2.0 });
285
+ await playerRef.current?.resetPanZoom();
454
286
  ```
455
287
 
456
- ### Memory Management
288
+ ### TwoStepPlayerControllerView
457
289
 
458
- ```typescript
459
- // Release assets when done
460
- TwoStepVideo.releaseAsset(asset.id);
461
- TwoStepVideo.releaseComposition(composition.id);
290
+ Native iOS player with system controls (AirPlay, PiP, fullscreen):
462
291
 
463
- // Release all at once
464
- TwoStepVideo.releaseAll();
292
+ ```tsx
293
+ import { TwoStepPlayerControllerView } from 'expo-twostep-video';
465
294
 
466
- // Clean up temp files
467
- TwoStepVideo.cleanupFile(tempFileUri);
295
+ <TwoStepPlayerControllerView
296
+ assetId={asset.id}
297
+ showsPlaybackControls={true}
298
+ style={{ width: '100%', height: 300 }}
299
+ />
468
300
  ```
469
301
 
470
302
  ## Constants
@@ -472,555 +304,46 @@ TwoStepVideo.cleanupFile(tempFileUri);
472
304
  ### Quality Presets
473
305
 
474
306
  ```typescript
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
307
+ TwoStepVideo.Quality.LOW // Fast, small files
308
+ TwoStepVideo.Quality.MEDIUM // Good for web/social
309
+ TwoStepVideo.Quality.HIGH // Recommended default
310
+ TwoStepVideo.Quality.HIGHEST // Archival quality
479
311
  ```
480
312
 
481
313
  ### Mirror Axis
482
314
 
483
315
  ```typescript
484
- TwoStepVideo.Mirror.HORIZONTAL // Flip left-right (selfie correction)
316
+ TwoStepVideo.Mirror.HORIZONTAL // Flip left-right
485
317
  TwoStepVideo.Mirror.VERTICAL // Flip top-bottom
486
- TwoStepVideo.Mirror.BOTH // Flip both axes (180ยฐ rotation effect)
318
+ TwoStepVideo.Mirror.BOTH // Flip both
487
319
  ```
488
320
 
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
-
499
- ## TypeScript Support
500
-
501
- Full TypeScript definitions included:
321
+ ## TypeScript Types
502
322
 
503
323
  ```typescript
504
- // Core types
505
- interface VideoAsset {
506
- id: string;
507
- duration: number;
508
- width: number;
509
- height: number;
510
- frameRate: number;
511
- hasAudio: boolean;
512
- }
513
-
514
- interface VideoComposition {
515
- id: string;
516
- duration: number;
517
- }
518
-
519
- interface ExportResult {
520
- uri: string;
521
- path: string;
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
- }
324
+ import type {
325
+ VideoAsset,
326
+ VideoComposition,
327
+ ExportResult,
328
+ LoopResult,
329
+ TwoStepVideoViewRef,
330
+ TwoStepVideoViewProps,
331
+ PanZoomState,
332
+ MirrorAxis,
333
+ VideoQuality,
334
+ } from 'expo-twostep-video';
582
335
  ```
583
336
 
584
- ## Advanced Usage
585
-
586
- ### Create a Highlight Reel
587
-
588
- ```typescript
589
- async function createHighlightReel(videoUri: string) {
590
- const asset = await TwoStepVideo.loadAsset({ uri: videoUri });
591
-
592
- // Extract 3-second clips evenly throughout video
593
- const numClips = 5;
594
- const interval = asset.duration / (numClips + 1);
595
-
596
- const segments = Array.from({ length: numClips }, (_, i) => ({
597
- start: interval * (i + 1),
598
- end: Math.min(interval * (i + 1) + 3, asset.duration)
599
- }));
600
-
601
- const composition = await TwoStepVideo.trimVideoMultiple({
602
- assetId: asset.id,
603
- segments
604
- });
605
-
606
- return await TwoStepVideo.exportVideo({
607
- compositionId: composition.id,
608
- quality: TwoStepVideo.Quality.HIGH
609
- });
610
- }
611
- ```
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
-
900
- ### With Progress Bar
901
-
902
- ```typescript
903
- function VideoExporter({ videoUri }) {
904
- const progress = TwoStepVideo.useExportProgress();
905
-
906
- return (
907
- <View>
908
- <ProgressBar progress={progress} />
909
- <Text>{Math.round(progress * 100)}%</Text>
910
- </View>
911
- );
912
- }
913
- ```
914
-
915
- ## Development
916
-
917
- This module contains both the Swift library and Expo bridge in one package.
918
-
919
- ### Running Tests
920
-
921
- ```bash
922
- # Run Swift unit tests
923
- npm run test:swift
924
-
925
- # All tests should pass:
926
- # โœ… 69 tests executed, 20 skipped (integration tests)
927
- ```
928
-
929
- ### Building
930
-
931
- ```bash
932
- # Build TypeScript
933
- npm run build
934
-
935
- # Clean build artifacts
936
- npm run clean
937
- ```
938
-
939
- ### Running Example App
940
-
941
- ```bash
942
- cd example
943
- npm install
944
- npm run ios
945
- ```
946
-
947
- ## Architecture
948
-
949
- ```
950
- expo-twostep-video/
951
- โ”œโ”€โ”€ ios/
952
- โ”‚ โ”œโ”€โ”€ TwoStepVideo/ # Swift library
953
- โ”‚ โ”‚ โ”œโ”€โ”€ Models/ # Data models
954
- โ”‚ โ”‚ โ”œโ”€โ”€ Core/ # Core functionality
955
- โ”‚ โ”‚ โ””โ”€โ”€ TwoStepVideo.swift # Main facade
956
- โ”‚ โ”œโ”€โ”€ Tests/ # Swift unit tests
957
- โ”‚ โ”œโ”€โ”€ ExpoTwoStepVideoModule.swift # Expo bridge
958
- โ”‚ โ””โ”€โ”€ Package.swift # Swift package for testing
959
- โ”œโ”€โ”€ src/
960
- โ”‚ โ””โ”€โ”€ index.ts # TypeScript API
961
- โ”œโ”€โ”€ example/ # Example React Native app
962
- โ””โ”€โ”€ docs/ # Documentation
963
- ```
964
-
965
- ## Documentation
966
-
967
- - [Architecture](./docs/ARCHITECTURE.md) - Design and implementation details
968
- - [Developer Guide](./docs/CLAUDE.md) - Best practices and patterns
969
- - [Temp File Management](./docs/TEMP_FILE_MANAGEMENT.md) - File cleanup strategies
970
-
971
- ## Requirements
972
-
973
- - iOS 15.0+
974
- - Expo SDK 50+
975
- - React Native 0.72+
976
-
977
337
  ## Platform Support
978
338
 
979
339
  - โœ… iOS (fully supported)
980
340
  - โณ Android (coming soon)
981
341
 
982
- ## Error Handling
983
-
984
- All errors include descriptive codes and messages:
985
-
986
- ```typescript
987
- try {
988
- await TwoStepVideo.exportVideo(options);
989
- } catch (error: any) {
990
- switch (error.code) {
991
- case 'EXPORT_FAILED':
992
- // Handle export failure
993
- break;
994
- case 'ASSET_NOT_FOUND':
995
- // Asset was released too early
996
- break;
997
- default:
998
- console.error('Unknown error:', error);
999
- }
1000
- }
1001
- ```
1002
-
1003
- ## Performance Tips
1004
-
1005
- 1. **Release resources promptly** after use
1006
- 2. **Use `releaseAll()`** when unmounting components
1007
- 3. **Validate URIs** before loading with `validateVideoUri()`
1008
- 4. **Clean up temp files** after moving/uploading
1009
-
1010
- ## Contributing
1011
-
1012
- Contributions welcome! Please see [CONTRIBUTING](./docs/CONTRIBUTING.md).
1013
-
1014
342
  ## License
1015
343
 
1016
344
  MIT ยฉ Richard Guo
1017
345
 
1018
- ## Support
1019
-
1020
- - ๐Ÿ“ง Email: richardg7890@gmail.com
1021
- - ๐Ÿ› Issues: [GitHub Issues](https://github.com/rguo123/twostep-video/issues)
1022
- - ๐Ÿ“š Docs: [Full Documentation](./docs/)
1023
-
1024
- ## Acknowledgments
346
+ ## Links
1025
347
 
1026
- Built with AVFoundation and Expo Modules.
348
+ - [GitHub Repository](https://github.com/rguo123/twostep-video)
349
+ - [Issue Tracker](https://github.com/rguo123/twostep-video/issues)