PageRenderTime 128ms CodeModel.GetById 105ms app.highlight 18ms RepoModel.GetById 1ms app.codeStats 0ms

/amd/src/vimeo-upload.js

https://bitbucket.org/briancaballero/assignsubmission_vimeo
JavaScript | 449 lines | 205 code | 43 blank | 201 comment | 38 complexity | 36bdd0ac5d62464e8706a441dac08197 MD5 | raw file
  1/*
  2 | Vimeo-Upload: Upload videos to your Vimeo account directly from a
  3 |               browser or a Node.js app
  4 |
  5 |  _    ___
  6 | | |  / (_)___ ___  ___  ____
  7 | | | / / / __ `__ \/ _ \/ __ \   ┌───────────────────────────┐
  8 | | |/ / / / / / / /  __/ /_/ /   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  %75    |
  9 | |___/_/_/ /_/ /_/\___/\____/    └───────────────────────────┘
 10 |                      Upload
 11 |
 12 |
 13 | This project was released under Apache 2.0" license.
 14 |
 15 | @link      http://websemantics.ca
 16 | @author    Web Semantics, Inc. Dev Team <team@websemantics.ca>
 17 | @author    Adnan M.Sagar, PhD. <adnan@websemantics.ca>
 18 | @credits   Built on cors-upload-sample, https://github.com/googledrive/cors-upload-sample
 19 */
 20
 21;
 22(function(root, factory) {
 23    if (typeof define === 'function' && define.amd) {
 24        define([], function() {
 25            return (root.VimeoUpload = factory())
 26        })
 27    } else if (typeof module === 'object' && module.exports) {
 28        module.exports = factory()
 29    } else {
 30        root.VimeoUpload = factory()
 31    }
 32}(this, function() {
 33
 34    // -------------------------------------------------------------------------
 35    // RetryHandler Class
 36
 37    /**
 38     * Helper for implementing retries with backoff. Initial retry
 39     * delay is 1 second, increasing by 2x (+jitter) for subsequent retries
 40     *
 41     * @constructor
 42     */
 43    var RetryHandler = function() {
 44        this.interval = 1000 // Start at one second
 45        this.maxInterval = 60 * 1000; // Don't wait longer than a minute
 46    }
 47
 48    /**
 49     * Invoke the function after waiting
 50     *
 51     * @param {function} fn Function to invoke
 52     */
 53    RetryHandler.prototype.retry = function(fn) {
 54        setTimeout(fn, this.interval)
 55        this.interval = this.nextInterval_()
 56    }
 57
 58    /**
 59     * Reset the counter (e.g. after successful request)
 60     */
 61    RetryHandler.prototype.reset = function() {
 62        this.interval = 1000
 63    }
 64
 65    /**
 66     * Calculate the next wait time.
 67     * @return {number} Next wait interval, in milliseconds
 68     *
 69     * @private
 70     */
 71    RetryHandler.prototype.nextInterval_ = function() {
 72        var interval = this.interval * 2 + this.getRandomInt_(0, 1000)
 73        return Math.min(interval, this.maxInterval)
 74    }
 75
 76    /**
 77     * Get a random int in the range of min to max. Used to add jitter to wait times.
 78     *
 79     * @param {number} min Lower bounds
 80     * @param {number} max Upper bounds
 81     * @private
 82     */
 83    RetryHandler.prototype.getRandomInt_ = function(min, max) {
 84        return Math.floor(Math.random() * (max - min + 1) + min)
 85    }
 86
 87    // -------------------------------------------------------------------------
 88    // Private data
 89
 90    /* Library defaults, can be changed using the 'defaults' member method,
 91
 92    - api_url (string), vimeo api url
 93    - name (string), default video name
 94    - description (string), default video description
 95    - contentType (string), video content type
 96    - token (string), vimeo api token
 97    - file (object), video file
 98    - metadata (array), data to associate with the video
 99    - upgrade_to_1080 (boolean), set video resolution to high definition
100    - offset (int),
101    - chunkSize (int),
102    - retryHandler (RetryHandler), hanlder class
103    - onComplete (function), handler for onComplete event
104    - onProgress (function), handler for onProgress event
105    - onError (function), handler for onError event
106
107    */
108
109    var defaults = {
110        api_url: 'https://api.vimeo.com',
111        name: 'Default name',
112        description: 'Default description',
113        contentType: 'application/octet-stream',
114        token: null,
115        file: {},
116        metadata: [],
117        upgrade_to_1080: false,
118        offset: 0,
119        chunkSize: 0,
120        retryHandler: new RetryHandler(),
121        onComplete: function() {},
122        onProgress: function() {},
123        onError: function() {}
124    }
125
126    /**
127     * Helper class for resumable uploads using XHR/CORS. Can upload any Blob-like item, whether
128     * files or in-memory constructs.
129     *
130     * @example
131     * var content = new Blob(["Hello world"], {"type": "text/plain"})
132     * var uploader = new VimeoUpload({
133     *   file: content,
134     *   token: accessToken,
135     *   onComplete: function(data) { ... }
136     *   onError: function(data) { ... }
137     * })
138     * uploader.upload()
139     *
140     * @constructor
141     * @param {object} options Hash of options
142     * @param {string} options.token Access token
143     * @param {blob} options.file Blob-like item to upload
144     * @param {string} [options.fileId] ID of file if replacing
145     * @param {object} [options.params] Additional query parameters
146     * @param {string} [options.contentType] Content-type, if overriding the type of the blob.
147     * @param {object} [options.metadata] File metadata
148     * @param {function} [options.onComplete] Callback for when upload is complete
149     * @param {function} [options.onProgress] Callback for status for the in-progress upload
150     * @param {function} [options.onError] Callback if upload fails
151     */
152    var me = function(opts) {
153
154        /* copy user options or use default values */
155        for (var i in defaults) {
156            this[i] = (opts[i] !== undefined) ? opts[i] : defaults[i]
157        }
158
159        this.contentType = opts.contentType || this.file.type || defaults.contentType
160        this.httpMethod = opts.fileId ? 'PUT' : 'POST'
161
162        this.videoData = {
163            name: (opts.name > '') ? opts.name : defaults.name,
164            description: (opts.description > '') ? opts.description : defaults.description,
165            'privacy.view': opts.private ? 'nobody' : 'anybody'
166        }
167
168        if (!(this.url = opts.url)) {
169            var params = opts.params || {} /*  TODO params.uploadType = 'resumable' */
170            this.url = this.buildUrl_(opts.fileId, params, opts.baseUrl)
171        }
172    }
173
174    // -------------------------------------------------------------------------
175    // Public methods
176
177    /*
178      Override class defaults
179
180        Parameters:
181        - opts (object): name value pairs
182
183    */
184
185    me.prototype.defaults = function(opts) {
186        return defaults /* TODO $.extend(true, defaults, opts) */
187    }
188
189    /**
190     * Initiate the upload (Get vimeo ticket number and upload url)
191     */
192    me.prototype.upload = function() {
193        var xhr = new XMLHttpRequest()
194        xhr.open(this.httpMethod, this.url, true)
195        xhr.setRequestHeader('Authorization', 'Bearer ' + this.token)
196        xhr.setRequestHeader('Content-Type', 'application/json')
197
198        xhr.onload = function(e) {
199            // get vimeo upload  url, user (for available quote), ticket id and complete url
200            if (e.target.status < 400) {
201                var response = JSON.parse(e.target.responseText)
202                this.url = response.upload_link_secure
203                this.user = response.user
204                this.ticket_id = response.ticket_id
205                this.complete_url = defaults.api_url + response.complete_uri
206                this.sendFile_()
207            } else {
208                this.onUploadError_(e)
209            }
210        }.bind(this)
211
212        xhr.onerror = this.onUploadError_.bind(this)
213        xhr.send(JSON.stringify({
214            type: 'streaming',
215            upgrade_to_1080: this.upgrade_to_1080
216        }))
217    }
218
219    // -------------------------------------------------------------------------
220    // Private methods
221
222    /**
223     * Send the actual file content.
224     *
225     * @private
226     */
227    me.prototype.sendFile_ = function() {
228        var content = this.file
229        var end = this.file.size
230
231        if (this.offset || this.chunkSize) {
232            // Only bother to slice the file if we're either resuming or uploading in chunks
233            if (this.chunkSize) {
234                end = Math.min(this.offset + this.chunkSize, this.file.size)
235            }
236            content = content.slice(this.offset, end)
237        }
238
239        var xhr = new XMLHttpRequest()
240        xhr.open('PUT', this.url, true)
241        xhr.setRequestHeader('Content-Type', this.contentType)
242            // xhr.setRequestHeader('Content-Length', this.file.size)
243        xhr.setRequestHeader('Content-Range', 'bytes ' + this.offset + '-' + (end - 1) + '/' + this.file.size)
244
245        if (xhr.upload) {
246            xhr.upload.addEventListener('progress', this.onProgress)
247        }
248        xhr.onload = this.onContentUploadSuccess_.bind(this)
249        xhr.onerror = this.onContentUploadError_.bind(this)
250        xhr.send(content)
251    }
252
253    /**
254     * Query for the state of the file for resumption.
255     *
256     * @private
257     */
258    me.prototype.resume_ = function() {
259        var xhr = new XMLHttpRequest()
260        xhr.open('PUT', this.url, true)
261        xhr.setRequestHeader('Content-Range', 'bytes */' + this.file.size)
262        xhr.setRequestHeader('X-Upload-Content-Type', this.file.type)
263        if (xhr.upload) {
264            xhr.upload.addEventListener('progress', this.onProgress)
265        }
266        xhr.onload = this.onContentUploadSuccess_.bind(this)
267        xhr.onerror = this.onContentUploadError_.bind(this)
268        xhr.send()
269    }
270
271    /**
272     * Extract the last saved range if available in the request.
273     *
274     * @param {XMLHttpRequest} xhr Request object
275     */
276    me.prototype.extractRange_ = function(xhr) {
277        var range = xhr.getResponseHeader('Range')
278        if (range) {
279            this.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1
280        }
281    }
282
283    /**
284     * The final step is to call vimeo.videos.upload.complete to queue up
285     * the video for transcoding.
286     *
287     * If successful call 'onUpdateVideoData_'
288     *
289     * @private
290     */
291    me.prototype.complete_ = function(xhr) {
292        var xhr = new XMLHttpRequest()
293        xhr.open('DELETE', this.complete_url, true)
294        xhr.setRequestHeader('Authorization', 'Bearer ' + this.token)
295
296        xhr.onload = function(e) {
297
298            // Get the video location (videoId)
299            if (e.target.status < 400) {
300                var location = e.target.getResponseHeader('Location')
301
302                // Example of location: ' /videos/115365719', extract the video id only
303                var video_id = location.split('/').pop()
304                    // Update the video metadata
305                this.onUpdateVideoData_(video_id)
306            } else {
307                this.onCompleteError_(e)
308            }
309        }.bind(this)
310
311        xhr.onerror = this.onCompleteError_.bind(this)
312        xhr.send()
313    }
314
315    /**
316     * Update the Video Data and add the metadata to the upload object
317     *
318     * @private
319     * @param {string} [id] Video Id
320     */
321    me.prototype.onUpdateVideoData_ = function(video_id) {
322        var url = this.buildUrl_(video_id, [], defaults.api_url + '/videos/')
323        var httpMethod = 'PATCH'
324        var xhr = new XMLHttpRequest()
325
326        xhr.open(httpMethod, url, true)
327        xhr.setRequestHeader('Authorization', 'Bearer ' + this.token)
328        xhr.onload = function(e) {
329            // add the metadata
330            this.onGetMetadata_(e, video_id)
331        }.bind(this)
332        xhr.send(this.buildQuery_(this.videoData))
333    }
334
335    /**
336     * Retrieve the metadata from a successful onUpdateVideoData_ response
337     * This is is useful when uploading unlisted videos as the URI has changed.
338     *
339     * If successful call 'onUpdateVideoData_'
340     *
341     * @private
342     * @param {object} e XHR event
343     * @param {string} [id] Video Id
344     */
345    me.prototype.onGetMetadata_ = function(e, video_id) {
346        // Get the video location (videoId)
347        if (e.target.status < 400) {
348            if (e.target.response) {
349                // add the returned metadata to the metadata array
350                var meta = JSON.parse(e.target.response)
351                    // get the new index of the item
352                var index = this.metadata.push(meta) - 1
353                    // call the complete method
354                this.onComplete(video_id, index)
355            } else {
356                this.onCompleteError_(e)
357            }
358        }
359    }
360
361    /**
362     * Handle successful responses for uploads. Depending on the context,
363     * may continue with uploading the next chunk of the file or, if complete,
364     * invokes vimeo complete service.
365     *
366     * @private
367     * @param {object} e XHR event
368     */
369    me.prototype.onContentUploadSuccess_ = function(e) {
370        if (e.target.status == 200 || e.target.status == 201) {
371            this.complete_()
372        } else if (e.target.status == 308) {
373            this.extractRange_(e.target)
374            this.retryHandler.reset()
375            this.sendFile_()
376        }
377    }
378
379    /**
380     * Handles errors for uploads. Either retries or aborts depending
381     * on the error.
382     *
383     * @private
384     * @param {object} e XHR event
385     */
386    me.prototype.onContentUploadError_ = function(e) {
387        if (e.target.status && e.target.status < 500) {
388            this.onError(e.target.response)
389        } else {
390            this.retryHandler.retry(this.resume_())
391        }
392    }
393
394    /**
395     * Handles errors for the complete request.
396     *
397     * @private
398     * @param {object} e XHR event
399     */
400    me.prototype.onCompleteError_ = function(e) {
401        this.onError(e.target.response); // TODO - Retries for initial upload
402    }
403
404    /**
405     * Handles errors for the initial request.
406     *
407     * @private
408     * @param {object} e XHR event
409     */
410    me.prototype.onUploadError_ = function(e) {
411        this.onError(e.target.response); // TODO - Retries for initial upload
412    }
413
414    /**
415     * Construct a query string from a hash/object
416     *
417     * @private
418     * @param {object} [params] Key/value pairs for query string
419     * @return {string} query string
420     */
421    me.prototype.buildQuery_ = function(params) {
422        params = params || {}
423        return Object.keys(params).map(function(key) {
424            return encodeURIComponent(key) + '=' + encodeURIComponent(params[key])
425        }).join('&')
426    }
427
428    /**
429     * Build the drive upload URL
430     *
431     * @private
432     * @param {string} [id] File ID if replacing
433     * @param {object} [params] Query parameters
434     * @return {string} URL
435     */
436    me.prototype.buildUrl_ = function(id, params, baseUrl) {
437        var url = baseUrl || defaults.api_url + '/me/videos'
438        if (id) {
439            url += id
440        }
441        var query = this.buildQuery_(params)
442        if (query) {
443            url += '?' + query
444        }
445        return url
446    }
447
448    return me
449}))