Skip to content

Commit 0c6e919

Browse files
Merge pull request #1 from apivideo/retries
Retries
2 parents 7a86f69 + 96e23f1 commit 0c6e919

File tree

8 files changed

+344
-30
lines changed

8 files changed

+344
-30
lines changed

.github/workflows/release.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: Release package to npmjs
2+
on:
3+
release:
4+
types: [created]
5+
jobs:
6+
deploy:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v2
10+
- uses: actions/setup-node@v2
11+
with:
12+
registry-url: 'https://registry.npmjs.org'
13+
- run: npm install --no-save
14+
- run: npm publish --access=public
15+
env:
16+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

README.md

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# api.video video uploader
44

5-
Typescript library to upload videos to api.video using delegated token
5+
Typescript library to upload videos to api.video using delegated token (or usual access token) from front-end.
66

77
# Usage
88

@@ -73,3 +73,83 @@ Then, once the `window.onload` event has been trigered, create your player using
7373
}
7474
</script>
7575
```
76+
77+
# Instanciation
78+
79+
## Options
80+
81+
The upload is instanciated using an `options` object. Options to provide depend on the way you want to authenticate to the API: either using a delegated upload token (recommanded), or using a usual access token.
82+
83+
### Using a delegated token:
84+
85+
86+
| Option name | Mandatory | Type | Description |
87+
| ----------------------------: | --------- | ------ | ----------------- |
88+
| uploadToken | **yes** | string | your upload token |
89+
| _common options (see bellow)_ | | | |
90+
91+
92+
### Using an access token:
93+
94+
95+
| Option name | Mandatory | Type | Description |
96+
| ----------------------------: | --------- | ------ | ----------------------- |
97+
| accessToken | **yes** | string | your access token |
98+
| videoId | **yes** | string | id of an existing video |
99+
| _common options (see bellow)_ | | | |
100+
101+
102+
### Common options
103+
104+
105+
| Option name | Mandatory | Type | Description |
106+
| ----------: | --------- | ------ | ----------------------------------------------------- |
107+
| file | yes | File | the file you want to upload |
108+
| chunkSize | no | number | number of bytes of each upload chunk (default: 1MB) |
109+
| apiHost | no | string | api.video host (default: ws.api.video) |
110+
| retries | no | number | number of retries when an API call fails (default: 5) |
111+
112+
113+
## Example
114+
115+
```javascript
116+
const uploader = new VideoUploader({
117+
file: files[0],
118+
uploadToken: "YOUR_DELEGATED_TOKEN",
119+
chunkSize: 1024*1024*10, // 10MB
120+
retries: 10,
121+
});
122+
```
123+
124+
# Methods
125+
126+
## `upload()`
127+
128+
The upload() method starts the upload. It takes no parameter. It returns a Promise that resolves once the file is uploaded. If an API call fails more than the specified number of retries, then the promise is rejected.
129+
On success, the promise embeds the `video` object returned by the API.
130+
On fail, the promise embeds the status code & error message returned by the API.
131+
132+
### Example
133+
134+
```javascript
135+
// ... uploader instanciation
136+
137+
uploader.upload()
138+
.then((video) => console.log(video))
139+
.catch((error) => console.log(error.status, error.message));
140+
```
141+
142+
## `onProgress()`
143+
144+
The onProgress() method let you defined an upload progress listener. It takes a callback function with one parameter: the onProgress events.
145+
An onProgress event contains 2 attributes:
146+
- loaded: the number of uploaded bytes
147+
- total: the total number of bytes
148+
149+
### Example
150+
151+
```javascript
152+
// ... uploader instanciation
153+
154+
uploader.onProgress((event) => console.log(event.loaded, event.total));
155+
```

dist/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/src/index.d.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
interface OptionsWithUploadToken extends Options {
2+
uploadToken: string;
3+
}
4+
interface OptionsWithAccessToken extends Options {
5+
accessToken: string;
6+
videoId: string;
7+
}
18
interface Options {
29
file: File;
3-
uploadToken: string;
410
chunkSize?: number;
5-
uploadEndpoint?: string;
11+
apiHost?: string;
12+
retries?: number;
613
}
714
interface UploadProgressEvent {
815
loaded: number;
@@ -12,17 +19,18 @@ export declare class VideoUploader {
1219
private file;
1320
private chunkSize;
1421
private uploadEndpoint;
15-
private uploadToken;
1622
private currentChunk;
1723
private chunksCount;
1824
private fileSize;
1925
private fileName;
2026
private videoId?;
27+
private retries;
2128
private onProgressCallbacks;
22-
constructor(options: Options);
29+
private headers;
30+
constructor(options: OptionsWithAccessToken | OptionsWithUploadToken);
2331
onProgress(cb: () => UploadProgressEvent): void;
2432
upload(): Promise<any>;
25-
private validateOptions;
33+
private sleep;
2634
private createFormData;
2735
private uploadCurrentChunk;
2836
}

package-lock.json

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"tslint": "^6.1.3",
3535
"typescript": "^4.0.2",
3636
"webpack": "^4.44.1",
37-
"webpack-cli": "^3.3.12"
37+
"webpack-cli": "^3.3.12",
38+
"xhr-mock": "^2.5.1"
3839
},
3940
"dependencies": {
4041
"core-js": "^3.8.3"

src/index.ts

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
interface OptionsWithUploadToken extends Options {
2+
uploadToken: string;
3+
}
4+
interface OptionsWithAccessToken extends Options {
5+
accessToken: string;
6+
videoId: string;
7+
}
18
interface Options {
29
file: File;
3-
uploadToken: string;
410
chunkSize?: number;
5-
uploadEndpoint?: string;
11+
apiHost?: string;
12+
retries?: number;
613
}
714

815
interface UploadProgressEvent {
@@ -11,25 +18,46 @@ interface UploadProgressEvent {
1118
}
1219

1320
const DEFAULT_CHUNK_SIZE = 1024 * 1024; // 1mb
14-
const DEFAULT_UPLOAD_ENDPOINT = "https://ws.api.video/upload";
21+
const DEFAULT_RETRIES = 5;
22+
const DEFAULT_API_HOST = "ws.api.video";
1523

1624
export class VideoUploader {
1725
private file: File;
1826
private chunkSize: number;
1927
private uploadEndpoint: string;
20-
private uploadToken: string;
2128
private currentChunk: number = 0;
2229
private chunksCount: number;
2330
private fileSize: number;
2431
private fileName: string;
2532
private videoId?: string;
33+
private retries: number;
2634
private onProgressCallbacks: ((e: UploadProgressEvent) => void)[] = [];
35+
private headers: { [name: string]: string } = {};
36+
37+
constructor(options: OptionsWithAccessToken | OptionsWithUploadToken) {
38+
const apiHost = options.apiHost || DEFAULT_API_HOST;
39+
40+
if (!options.file) {
41+
throw new Error("'file' is missing");
42+
}
43+
44+
if (options.hasOwnProperty("uploadToken")) {
45+
const optionsWithUploadToken = options as OptionsWithUploadToken;
46+
this.uploadEndpoint = `https://${apiHost}/upload?token=${optionsWithUploadToken.uploadToken}`;
47+
48+
} else if (options.hasOwnProperty("accessToken")) {
49+
const optionsWithAccessToken = options as OptionsWithAccessToken;
50+
if (!optionsWithAccessToken.videoId) {
51+
throw new Error("'videoId' is missing");
52+
}
53+
this.uploadEndpoint = `https://${apiHost}/videos/${optionsWithAccessToken.videoId}/source`;
54+
this.headers.Authorization = `Bearer ${optionsWithAccessToken.accessToken}`;
55+
} else {
56+
throw new Error(`You must provide either an accessToken or an uploadToken`);
57+
}
2758

28-
constructor(options: Options) {
29-
this.validateOptions(options);
3059
this.chunkSize = options.chunkSize || DEFAULT_CHUNK_SIZE;
31-
this.uploadEndpoint = options.uploadEndpoint || DEFAULT_UPLOAD_ENDPOINT;
32-
this.uploadToken = options.uploadToken;
60+
this.retries = options.retries || DEFAULT_RETRIES;
3361
this.file = options.file;
3462
this.fileSize = this.file.size;
3563
this.fileName = this.file.name;
@@ -44,20 +72,30 @@ export class VideoUploader {
4472
public upload(): Promise<any> {
4573
return new Promise(async (resolve, reject) => {
4674
let response;
75+
let retriesCount = 0;
4776
while (this.currentChunk < this.chunksCount) {
48-
response = await this.uploadCurrentChunk();
49-
this.videoId = response.videoId;
50-
this.currentChunk++;
77+
try {
78+
response = await this.uploadCurrentChunk();
79+
this.videoId = response.videoId;
80+
this.currentChunk++;
81+
82+
} catch (e) {
83+
if(retriesCount >= this.retries) {
84+
reject(e);
85+
break;
86+
}
87+
await this.sleep(200 + retriesCount * 500);
88+
retriesCount++;
89+
}
5190
}
5291
resolve(response);
5392
});
5493
}
5594

56-
private validateOptions(options: Options) {
57-
const required = ['file', 'uploadToken']
58-
required.forEach(r => {
59-
if (!(options as any)[r]) throw new Error(`"${r}" is missing`);
60-
});
95+
private sleep(duration: number): Promise<void> {
96+
return new Promise((resolve, reject) => {
97+
setTimeout(() => resolve(), duration);
98+
})
6199
}
62100

63101
private createFormData(startByte: number, endByte: number): FormData {
@@ -78,10 +116,22 @@ export class VideoUploader {
78116

79117
const contentRange = `bytes ${firstByte}-${lastByte - 1}/${this.fileSize}`;
80118

81-
const xhr = new XMLHttpRequest();
82-
xhr.open("POST", `${this.uploadEndpoint}?token=${this.uploadToken}`, true);
119+
const xhr = new window.XMLHttpRequest();
120+
xhr.open("POST", `${this.uploadEndpoint}`, true);
83121
xhr.setRequestHeader("Content-Range", contentRange);
84-
122+
for (const headerName of Object.keys(this.headers)) {
123+
xhr.setRequestHeader(headerName, this.headers[headerName]);
124+
}
125+
xhr.onreadystatechange = (e) => {
126+
if (xhr.readyState === 4) { // DONE
127+
if (xhr.status >= 400) {
128+
reject({
129+
status: xhr.status,
130+
message: xhr.response
131+
});
132+
}
133+
}
134+
};
85135
xhr.onload = (e) => resolve(JSON.parse(xhr.response));
86136
xhr.onprogress = (e) => this.onProgressCallbacks.forEach(cb => cb({
87137
loaded: e.loaded + firstByte,

0 commit comments

Comments
 (0)