@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 +139 -816
- package/build/ExpoTwoStepVideo.types.d.ts +9 -3
- package/build/ExpoTwoStepVideo.types.d.ts.map +1 -1
- package/build/ExpoTwoStepVideo.types.js.map +1 -1
- package/build/ExpoTwoStepVideoModule.d.ts +2 -5
- package/build/ExpoTwoStepVideoModule.d.ts.map +1 -1
- package/build/ExpoTwoStepVideoModule.js.map +1 -1
- package/build/TwoStepPlayerControllerView.d.ts.map +1 -1
- package/build/TwoStepPlayerControllerView.js +0 -10
- package/build/TwoStepPlayerControllerView.js.map +1 -1
- package/build/index.d.ts +3 -18
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -12
- package/build/index.js.map +1 -1
- package/ios/ExpoTwoStepVideoView.swift +255 -144
- package/package.json +1 -1
- /package/ios/{ExpoTwostepPlayerControllerView.swift โ ExpoTwoStepPlayerControllerView.swift} +0 -0
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
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
89
|
+
// Multiple segments (highlight reel)
|
|
179
90
|
const composition = await TwoStepVideo.trimVideoMultiple({
|
|
180
91
|
assetId: asset.id,
|
|
181
92
|
segments: [
|
|
182
|
-
{ start: 0, end:
|
|
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
|
-
###
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
251
|
-
const
|
|
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
|
|
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
|
-
###
|
|
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
|
|
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
|
|
152
|
+
loopCount: 4
|
|
300
153
|
});
|
|
301
154
|
|
|
302
|
-
|
|
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
|
|
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
|
-
|
|
325
|
-
const mirrored = await TwoStepVideo.transformVideo({
|
|
326
|
-
assetId: asset.id,
|
|
327
|
-
mirrorAxis: 'both'
|
|
328
|
-
});
|
|
171
|
+
### Pan & Zoom
|
|
329
172
|
|
|
330
|
-
|
|
331
|
-
|
|
173
|
+
```typescript
|
|
174
|
+
// Apply pan/zoom for export (bakes transform into video)
|
|
175
|
+
const zoomed = await TwoStepVideo.panZoomVideo({
|
|
332
176
|
assetId: asset.id,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
340
|
-
const
|
|
185
|
+
```typescript
|
|
186
|
+
const thumbnails = await TwoStepVideo.generateThumbnails({
|
|
341
187
|
assetId: asset.id,
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
218
|
+
### Memory Management
|
|
372
219
|
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
240
|
+
<View>
|
|
388
241
|
<TwoStepVideoView
|
|
389
242
|
ref={playerRef}
|
|
390
243
|
compositionId={compositionId}
|
|
391
244
|
loop={false}
|
|
392
|
-
onPlaybackStatusChange={(e) =>
|
|
393
|
-
onProgress={(e) =>
|
|
394
|
-
|
|
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
|
-
<
|
|
400
|
-
|
|
401
|
-
<
|
|
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
|
-
|
|
259
|
+
**Props:**
|
|
413
260
|
|
|
414
261
|
| Prop | Type | Description |
|
|
415
262
|
|------|------|-------------|
|
|
416
|
-
| `compositionId` | `string` | ID from
|
|
417
|
-
| `assetId` | `string` | ID from
|
|
418
|
-
| `loop` | `boolean` | Enable continuous looping
|
|
419
|
-
| `
|
|
420
|
-
| `
|
|
421
|
-
| `
|
|
422
|
-
| `
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
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
|
-
###
|
|
288
|
+
### TwoStepPlayerControllerView
|
|
457
289
|
|
|
458
|
-
|
|
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
|
-
|
|
464
|
-
|
|
292
|
+
```tsx
|
|
293
|
+
import { TwoStepPlayerControllerView } from 'expo-twostep-video';
|
|
465
294
|
|
|
466
|
-
|
|
467
|
-
|
|
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
|
|
476
|
-
TwoStepVideo.Quality.MEDIUM
|
|
477
|
-
TwoStepVideo.Quality.HIGH
|
|
478
|
-
TwoStepVideo.Quality.HIGHEST
|
|
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
|
|
316
|
+
TwoStepVideo.Mirror.HORIZONTAL // Flip left-right
|
|
485
317
|
TwoStepVideo.Mirror.VERTICAL // Flip top-bottom
|
|
486
|
-
TwoStepVideo.Mirror.BOTH // Flip both
|
|
318
|
+
TwoStepVideo.Mirror.BOTH // Flip both
|
|
487
319
|
```
|
|
488
320
|
|
|
489
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
348
|
+
- [GitHub Repository](https://github.com/rguo123/twostep-video)
|
|
349
|
+
- [Issue Tracker](https://github.com/rguo123/twostep-video/issues)
|