1
+ interface OptionsWithUploadToken extends Options {
2
+ uploadToken : string ;
3
+ }
4
+ interface OptionsWithAccessToken extends Options {
5
+ accessToken : string ;
6
+ videoId : string ;
7
+ }
1
8
interface Options {
2
9
file : File ;
3
- uploadToken : string ;
4
10
chunkSize ?: number ;
5
- uploadEndpoint ?: string ;
11
+ apiHost ?: string ;
12
+ retries ?: number ;
6
13
}
7
14
8
15
interface UploadProgressEvent {
@@ -11,25 +18,46 @@ interface UploadProgressEvent {
11
18
}
12
19
13
20
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" ;
15
23
16
24
export class VideoUploader {
17
25
private file : File ;
18
26
private chunkSize : number ;
19
27
private uploadEndpoint : string ;
20
- private uploadToken : string ;
21
28
private currentChunk : number = 0 ;
22
29
private chunksCount : number ;
23
30
private fileSize : number ;
24
31
private fileName : string ;
25
32
private videoId ?: string ;
33
+ private retries : number ;
26
34
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
+ }
27
58
28
- constructor ( options : Options ) {
29
- this . validateOptions ( options ) ;
30
59
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 ;
33
61
this . file = options . file ;
34
62
this . fileSize = this . file . size ;
35
63
this . fileName = this . file . name ;
@@ -44,20 +72,30 @@ export class VideoUploader {
44
72
public upload ( ) : Promise < any > {
45
73
return new Promise ( async ( resolve , reject ) => {
46
74
let response ;
75
+ let retriesCount = 0 ;
47
76
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
+ }
51
90
}
52
91
resolve ( response ) ;
53
92
} ) ;
54
93
}
55
94
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
+ } )
61
99
}
62
100
63
101
private createFormData ( startByte : number , endByte : number ) : FormData {
@@ -78,10 +116,22 @@ export class VideoUploader {
78
116
79
117
const contentRange = `bytes ${ firstByte } -${ lastByte - 1 } /${ this . fileSize } ` ;
80
118
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 ) ;
83
121
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
+ } ;
85
135
xhr . onload = ( e ) => resolve ( JSON . parse ( xhr . response ) ) ;
86
136
xhr . onprogress = ( e ) => this . onProgressCallbacks . forEach ( cb => cb ( {
87
137
loaded : e . loaded + firstByte ,
0 commit comments