Skip to content

Commit d3abd36

Browse files
authored
Merge pull request #8 from apivideo/feature/progressive_upload
Feature/progressive upload
2 parents 7578d06 + 620d297 commit d3abd36

File tree

13 files changed

+315
-39
lines changed

13 files changed

+315
-39
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Changelog
22
All changes to this project will be documented in this file.
33

4+
## [1.2.0] - 2024-04-03
5+
- Add support of progressive uploads
6+
47
## [1.1.0] - 2024-02-16
58
- Add support for RN new architecture: Turbo Native Modules
69
- Add an API to set time out

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
- [Getting started](#getting-started)
1515
- [Installation](#installation)
1616
- [Code sample](#code-sample)
17+
- [Regular upload](#regular-upload)
18+
- [Progressive upload](#progressive-upload)
1719
- [Android](#android)
1820
- [Permissions](#permissions)
1921
- [Notifications](#notifications)
@@ -54,6 +56,8 @@ yarn add @api.video/react-native-video-uploader
5456

5557
### Code sample
5658

59+
#### Regular upload
60+
5761
```js
5862
import ApiVideoUploader from '@api.video/react-native-video-uploader';
5963

@@ -66,6 +70,27 @@ ApiVideoUploader.uploadWithUploadToken('YOUR_UPLOAD_TOKEN', 'path/to/my-video.mp
6670
});
6771
```
6872

73+
#### Progressive upload
74+
75+
For more details about progressive uploads, see the [progressive upload documentation](https://docs.api.video/vod/progressive-upload).
76+
77+
```js
78+
import ApiVideoUploader from '@api.video/react-native-video-uploader';
79+
80+
(async () => {
81+
const uploadSession = ApiVideoUploader.createProgressiveUploadSession({token: 'YOUR_UPLOAD_TOKEN'});
82+
try {
83+
await session.uploadPart("path/to/video.mp4.part1");
84+
await session.uploadPart("path/to/video.mp4.part2");
85+
// ...
86+
const video = await session.uploadLastPart("path/to/video.mp4.partn");
87+
// ...
88+
} catch(e: any) {
89+
// Manages error here
90+
}
91+
})();
92+
```
93+
6994
### Android
7095

7196
#### Permissions

android/src/main/java/video/api/reactnative/uploader/UploaderModule.kt

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,60 @@ class UploaderModule(reactContext: ReactApplicationContext) :
8888
}
8989
}
9090

91+
// Progressive upload
92+
@ReactMethod
93+
override fun createProgressiveUploadSession(sessionId: String, videoId: String) {
94+
uploaderModuleImpl.createUploadProgressiveSession(sessionId, videoId)
95+
}
96+
97+
@ReactMethod
98+
override fun createProgressiveUploadWithUploadTokenSession(
99+
sessionId: String,
100+
token: String,
101+
videoId: String?
102+
) {
103+
uploaderModuleImpl.createUploadWithUploadTokenProgressiveSession(sessionId, token, videoId)
104+
}
105+
106+
@ReactMethod
107+
override fun uploadPart(sessionId: String, filePath: String, promise: Promise) {
108+
uploadPart(sessionId, filePath, false, promise)
109+
}
110+
111+
@ReactMethod
112+
override fun uploadLastPart(sessionId: String, filePath: String, promise: Promise) {
113+
uploadPart(sessionId, filePath, true, promise)
114+
}
115+
116+
@ReactMethod
117+
override fun disposeProgressiveUploadSession(sessionId: String) {
118+
uploaderModuleImpl.disposeProgressiveUploadSession(sessionId)
119+
}
120+
121+
private fun uploadPart(
122+
sessionId: String,
123+
filePath: String,
124+
isLastPart: Boolean,
125+
promise: Promise
126+
) {
127+
try {
128+
uploaderModuleImpl.uploadPart(sessionId, filePath, isLastPart, { _ ->
129+
}, { video ->
130+
promise.resolve(video)
131+
}, {
132+
promise.reject(CancellationException("Upload was cancelled"))
133+
}, { e ->
134+
promise.reject(e)
135+
})
136+
} catch (e: Exception) {
137+
promise.reject(e)
138+
}
139+
}
140+
91141
companion object {
92142
const val NAME = "ApiVideoUploader"
93143

94144
const val SDK_NAME = "reactnative-uploader"
95-
const val SDK_VERSION = "1.1.0"
145+
const val SDK_VERSION = "1.2.0"
96146
}
97147
}

android/src/main/java/video/api/reactnative/uploader/UploaderPackage.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package video.api.reactnative.uploader;
22

3+
import androidx.annotation.NonNull;
34
import androidx.annotation.Nullable;
45

56
import com.facebook.react.TurboReactPackage;
@@ -14,7 +15,7 @@
1415
public class UploaderPackage extends TurboReactPackage {
1516
@Nullable
1617
@Override
17-
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
18+
public NativeModule getModule(String name, @NonNull ReactApplicationContext reactContext) {
1819
if (name.equals(UploaderModule.NAME)) {
1920
return new UploaderModule(reactContext);
2021
} else {

android/src/oldarch/video/api/reactnative/uploader/UploaderModule.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,14 @@ abstract class UploaderModuleSpec(reactContext: ReactApplicationContext) :
2020
abstract fun uploadWithUploadToken(token: String, filePath: String, videoId: String?, promise: Promise)
2121

2222
abstract fun upload(videoId: String, filePath: String, promise: Promise)
23+
24+
abstract fun createProgressiveUploadSession(sessionId: String, videoId: String)
25+
26+
abstract fun createProgressiveUploadWithUploadTokenSession(sessionId: String, token: String, videoId: String?)
27+
28+
abstract fun uploadPart(sessionId: String, filePath: String, promise: Promise)
29+
30+
abstract fun uploadLastPart(sessionId: String, filePath: String, promise: Promise)
31+
32+
abstract fun disposeProgressiveUploadSession(sessionId: String)
2333
}

example/src/App.tsx

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SafeAreaView,
88
ScrollView,
99
StyleSheet,
10+
Switch,
1011
Text,
1112
TextInput,
1213
View,
@@ -22,6 +23,7 @@ import type { Video } from 'src/types';
2223
export default function App() {
2324
const [videoFile, setVideoFile] = React.useState<string | null>(null);
2425
const [uploading, setUploading] = React.useState<boolean>(false);
26+
const [isProgressive, setIsProgressive] = React.useState<boolean>(false);
2527
const [uploadToken, setUploadToken] = React.useState<string>('');
2628
const [chunkSize, setChunkSize] = React.useState<string>('20');
2729
const [uploadResult, setUploadResult] = React.useState<any | null>(null);
@@ -58,7 +60,12 @@ export default function App() {
5860
}, []);
5961

6062
const onUploadButtonPress = React.useCallback(
61-
(token: string, uri: string | null, chunkSize: string) => {
63+
(
64+
token: string,
65+
uri: string | null,
66+
chunkSize: string,
67+
isProgressive: boolean
68+
) => {
6269
const chunkSizeInt = parseInt(chunkSize);
6370

6471
if (!uri) {
@@ -80,19 +87,46 @@ export default function App() {
8087
const resolveUri = (u: string): Promise<string> => {
8188
return Platform.OS === 'android'
8289
? ReactNativeBlobUtil.fs.stat(u).then((stat) => stat.path)
83-
: new Promise((resolve, _) => resolve(u));
90+
: new Promise((resolve, _) => resolve(u.replace('file://', '')));
8491
};
8592

86-
resolveUri(uri).then((u) => {
87-
ApiVideoUploader.uploadWithUploadToken(token, u)
88-
.then((value: Video) => {
89-
setUploadResult(value);
90-
setUploading(false);
91-
})
92-
.catch((e: any) => {
93-
Alert.alert('Upload failed', e?.message || JSON.stringify(e));
94-
setUploading(false);
93+
resolveUri(uri).then(async (u) => {
94+
if (isProgressive) {
95+
const size = (await ReactNativeBlobUtil.fs.stat(u)).size;
96+
97+
const session = ApiVideoUploader.createProgressiveUploadSession({
98+
token,
9599
});
100+
const chunkSizeBytes = 1024 * 1024 * chunkSizeInt;
101+
let start = 0;
102+
for (
103+
let i = 0;
104+
start <= size - chunkSizeBytes;
105+
start += chunkSizeBytes, i++
106+
) {
107+
await ReactNativeBlobUtil.fs.slice(
108+
u,
109+
`${u}.part${i}`,
110+
start,
111+
start + chunkSizeBytes
112+
);
113+
await session.uploadPart(`${u}.part${i}`);
114+
}
115+
await ReactNativeBlobUtil.fs.slice(u, `${u}.lastpart`, start, size);
116+
const value = await session.uploadLastPart(u + '.lastpart');
117+
setUploadResult(value);
118+
setUploading(false);
119+
} else {
120+
ApiVideoUploader.uploadWithUploadToken(token, u)
121+
.then((value: Video) => {
122+
setUploadResult(value);
123+
setUploading(false);
124+
})
125+
.catch((e: any) => {
126+
Alert.alert('Upload failed', e?.message || JSON.stringify(e));
127+
setUploading(false);
128+
});
129+
}
96130
});
97131
},
98132
[]
@@ -165,11 +199,35 @@ export default function App() {
165199
keyboardType="numeric"
166200
/>
167201
</View>
202+
<View
203+
style={{
204+
borderLeftWidth: 1,
205+
marginLeft: 8,
206+
borderLeftColor: '#F64325',
207+
marginVertical: 5,
208+
alignItems: 'flex-start',
209+
}}
210+
>
211+
<Text style={styles.label}>Progressive upload</Text>
212+
<Switch
213+
disabled={uploading}
214+
trackColor={{ false: '#767577', true: '#767577' }}
215+
thumbColor={isProgressive ? '#F64325' : '#f4f3f4'}
216+
ios_backgroundColor="#3e3e3e"
217+
onValueChange={() => setIsProgressive(!isProgressive)}
218+
value={isProgressive}
219+
/>
220+
</View>
168221
<Text style={styles.textSectionTitle}>And finally... upload!</Text>
169222
<DemoButton
170223
disabled={uploading}
171224
onPress={() =>
172-
onUploadButtonPress(uploadToken, videoFile, chunkSize)
225+
onUploadButtonPress(
226+
uploadToken,
227+
videoFile,
228+
chunkSize,
229+
isProgressive
230+
)
173231
}
174232
>
175233
{uploading ? 'UPLOADING...' : 'UPLOAD'}

ios/RNUploader.mm

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,30 @@ @interface RCT_EXTERN_REMAP_MODULE(ApiVideoUploader, RNUploader, NSObject<RCTBri
3232
withResolver:(RCTPromiseResolveBlock)resolve
3333
withRejecter:(RCTPromiseRejectBlock)reject)
3434

35-
RCT_EXTERN_METHOD(setTimeout:(NSNumber)timeout)
35+
RCT_EXTERN_METHOD(setTimeout:(nonnull NSNumber)timeout)
3636

37-
RCT_EXTERN_METHOD(uploadWithUploadToken:(NSString)token:(NSString)filePath:(NSString)videoId
37+
// MARK: Regular upload
38+
RCT_EXTERN_METHOD(uploadWithUploadToken:(nonnull NSString)token:(nonnull NSString)filePath:(NSString)videoId
3839
withResolver:(RCTPromiseResolveBlock)resolve
3940
withRejecter:(RCTPromiseRejectBlock)reject)
4041

41-
RCT_EXTERN_METHOD(upload:(NSString)videoId:(NSString)filePath
42+
RCT_EXTERN_METHOD(upload:(nonnull NSString)videoId:(nonnull NSString)filePath
4243
withResolver:(RCTPromiseResolveBlock)resolve
4344
withRejecter:(RCTPromiseRejectBlock)reject)
4445

46+
// MARK: Progressive upload
47+
RCT_EXTERN_METHOD(createUploadProgressiveSession:(nonnull NSString)sessionId:(nonnull NSString)videoId)
48+
RCT_EXTERN_METHOD(createProgressiveUploadWithUploadTokenSession:(nonnull NSString)sessionId:(nonnull NSString)token:(NSString)videoId)
49+
50+
RCT_EXTERN_METHOD(uploadPart:(nonnull NSString)sessionId:(nonnull NSString)filePath
51+
withResolver:(RCTPromiseResolveBlock)resolve
52+
withRejecter:(RCTPromiseRejectBlock)reject)
53+
RCT_EXTERN_METHOD(uploadLastPart:(nonnull NSString)sessionId:(nonnull NSString)filePath
54+
withResolver:(RCTPromiseResolveBlock)resolve
55+
withRejecter:(RCTPromiseRejectBlock)reject)
56+
57+
RCT_EXTERN_METHOD(disposeProgressiveUploadSession:(nonnull NSString)sessionId)
58+
4559
// Don't compile this code when we build for the old architecture.
4660
#ifdef RCT_NEW_ARCH_ENABLED
4761
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {

ios/RNUploader.swift

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class RNUploader: NSObject {
66

77
override init() {
88
do {
9-
try uploadModule.setSdkName(name: "reactnative-uploader", version: "1.1.0")
9+
try uploadModule.setSdkName(name: "reactnative-uploader", version: "1.2.0")
1010
} catch {
1111
fatalError("Failed to set SDK name: \(error)")
1212
}
@@ -75,4 +75,45 @@ class RNUploader: NSObject {
7575
reject("upload_failed", error.localizedDescription, error)
7676
}
7777
}
78+
79+
@objc(createUploadProgressiveSession::)
80+
func createUploadProgressiveSession(sessionId: String, videoId: String) {
81+
do {
82+
try uploadModule.createUploadProgressiveSession(sessionId: sessionId, videoId: videoId)
83+
} catch {
84+
fatalError("Failed to create progressive upload session: \(error)")
85+
}
86+
}
87+
88+
@objc(createProgressiveUploadWithUploadTokenSession:::)
89+
func createProgressiveUploadWithUploadTokenSession(sessionId: String, token: String, videoId: String?) {
90+
do {
91+
try uploadModule.createProgressiveUploadWithUploadTokenSession(sessionId: sessionId, token: token, videoId: videoId)
92+
} catch {
93+
fatalError("Failed to create progressive upload with upload token session: \(error)")
94+
}
95+
}
96+
97+
@objc(uploadPart::withResolver:withRejecter:)
98+
func uploadPart(sessionId: String, filePath: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
99+
uploadModule.uploadPart(sessionId: sessionId, filePath: filePath, onProgress: { _ in }, onSuccess: { video in
100+
resolve(video)
101+
}, onError: { error in
102+
reject("upload_part_failed", error.localizedDescription, error)
103+
})
104+
}
105+
106+
@objc(uploadLastPart::withResolver:withRejecter:)
107+
func uploadLastPart(sessionId: String, filePath: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
108+
uploadModule.uploadLastPart(sessionId: sessionId, filePath: filePath, onProgress: { _ in }, onSuccess: { video in
109+
resolve(video)
110+
}, onError: { error in
111+
reject("upload_last_part_failed", error.localizedDescription, error)
112+
})
113+
}
114+
115+
@objc(disposeProgressiveUploadSession:)
116+
func disposeProgressiveUploadSession(sessionId: String) {
117+
uploadModule.disposeProgressiveUploadSession(sessionId)
118+
}
78119
}

ios/StringExtensions.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
extension String {
22
func deletePrefix(_ prefix: String) -> String {
3-
guard self.hasPrefix(prefix) else { return self }
4-
return String(self.dropFirst(prefix.count))
3+
guard hasPrefix(prefix) else { return self }
4+
return String(dropFirst(prefix.count))
55
}
6-
6+
77
var url: URL {
8-
return URL(fileURLWithPath: self.deletePrefix("file://"))
8+
return URL(fileURLWithPath: deletePrefix("file://"))
99
}
1010
}

ios/UploaderModule.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,3 @@ public class UploaderModule: NSObject {
135135
public enum UploaderError: Error {
136136
case invalidParameter(message: String)
137137
}
138-

0 commit comments

Comments
 (0)