vimeo-upload
Version:
Upload videos to your Vimeo account and update their metadata directly from a browser or a Node.js app.
450 lines (402 loc) • 14.7 kB
JavaScript
/*
| Vimeo-Upload: Upload videos to your Vimeo account directly from a
| browser or a Node.js app
|
| _ ___
| | | / (_)___ ___ ___ ____
| | | / / / __ `__ \/ _ \/ __ \ ┌───────────────────────────┐
| | |/ / / / / / / / __/ /_/ / | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ %75 |
| |___/_/_/ /_/ /_/\___/\____/ └───────────────────────────┘
| Upload
|
|
| This project was released under Apache 2.0" license.
|
| @link http://websemantics.ca
| @author Web Semantics, Inc. Dev Team <team@websemantics.ca>
| @author Adnan M.Sagar, PhD. <adnan@websemantics.ca>
| @credits Built on cors-upload-sample, https://github.com/googledrive/cors-upload-sample
*/
;
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define([], function() {
return (root.VimeoUpload = factory())
})
} else if (typeof module === 'object' && module.exports) {
module.exports = factory()
} else {
root.VimeoUpload = factory()
}
}(this, function() {
// -------------------------------------------------------------------------
// RetryHandler Class
/**
* Helper for implementing retries with backoff. Initial retry
* delay is 1 second, increasing by 2x (+jitter) for subsequent retries
*
* @constructor
*/
var RetryHandler = function() {
this.interval = 1000 // Start at one second
this.maxInterval = 60 * 1000; // Don't wait longer than a minute
}
/**
* Invoke the function after waiting
*
* @param {function} fn Function to invoke
*/
RetryHandler.prototype.retry = function(fn) {
setTimeout(fn, this.interval)
this.interval = this.nextInterval_()
}
/**
* Reset the counter (e.g. after successful request)
*/
RetryHandler.prototype.reset = function() {
this.interval = 1000
}
/**
* Calculate the next wait time.
* @return {number} Next wait interval, in milliseconds
*
* @private
*/
RetryHandler.prototype.nextInterval_ = function() {
var interval = this.interval * 2 + this.getRandomInt_(0, 1000)
return Math.min(interval, this.maxInterval)
}
/**
* Get a random int in the range of min to max. Used to add jitter to wait times.
*
* @param {number} min Lower bounds
* @param {number} max Upper bounds
* @private
*/
RetryHandler.prototype.getRandomInt_ = function(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
// -------------------------------------------------------------------------
// Private data
/* Library defaults, can be changed using the 'defaults' member method,
- api_url (string), vimeo api url
- name (string), default video name
- description (string), default video description
- contentType (string), video content type
- token (string), vimeo api token
- file (object), video file
- metadata (array), data to associate with the video
- upgrade_to_1080 (boolean), set video resolution to high definition
- offset (int),
- chunkSize (int),
- retryHandler (RetryHandler), hanlder class
- onComplete (function), handler for onComplete event
- onProgress (function), handler for onProgress event
- onError (function), handler for onError event
*/
var defaults = {
api_url: 'https://api.vimeo.com',
name: 'Default name',
description: 'Default description',
contentType: 'application/octet-stream',
token: null,
file: {},
metadata: [],
upgrade_to_1080: false,
offset: 0,
chunkSize: 0,
retryHandler: new RetryHandler(),
onComplete: function() {},
onProgress: function() {},
onError: function() {}
}
/**
* Helper class for resumable uploads using XHR/CORS. Can upload any Blob-like item, whether
* files or in-memory constructs.
*
* @example
* var content = new Blob(["Hello world"], {"type": "text/plain"})
* var uploader = new VimeoUpload({
* file: content,
* token: accessToken,
* onComplete: function(data) { ... }
* onError: function(data) { ... }
* })
* uploader.upload()
*
* @constructor
* @param {object} options Hash of options
* @param {string} options.token Access token
* @param {blob} options.file Blob-like item to upload
* @param {string} [options.fileId] ID of file if replacing
* @param {object} [options.params] Additional query parameters
* @param {string} [options.contentType] Content-type, if overriding the type of the blob.
* @param {object} [options.metadata] File metadata
* @param {function} [options.onComplete] Callback for when upload is complete
* @param {function} [options.onProgress] Callback for status for the in-progress upload
* @param {function} [options.onError] Callback if upload fails
*/
var me = function(opts) {
/* copy user options or use default values */
for (var i in defaults) {
this[i] = (opts[i] !== undefined) ? opts[i] : defaults[i]
}
this.contentType = opts.contentType || this.file.type || defaults.contentType
this.httpMethod = opts.fileId ? 'PUT' : 'POST'
this.videoData = {
name: (opts.name > '') ? opts.name : defaults.name,
description: (opts.description > '') ? opts.description : defaults.description,
'privacy.view': opts.private ? 'nobody' : 'anybody'
}
if (!(this.url = opts.url)) {
var params = opts.params || {} /* TODO params.uploadType = 'resumable' */
this.url = this.buildUrl_(opts.fileId, params, opts.baseUrl)
}
}
// -------------------------------------------------------------------------
// Public methods
/*
Override class defaults
Parameters:
- opts (object): name value pairs
*/
me.prototype.defaults = function(opts) {
return defaults /* TODO $.extend(true, defaults, opts) */
}
/**
* Initiate the upload (Get vimeo ticket number and upload url)
*/
me.prototype.upload = function() {
var xhr = new XMLHttpRequest()
xhr.open(this.httpMethod, this.url, true)
xhr.setRequestHeader('Authorization', 'Bearer ' + this.token)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.onload = function(e) {
// get vimeo upload url, user (for available quote), ticket id and complete url
if (e.target.status < 400) {
var response = JSON.parse(e.target.responseText)
this.url = response.upload_link_secure
this.user = response.user
this.ticket_id = response.ticket_id
this.complete_url = defaults.api_url + response.complete_uri
this.sendFile_()
} else {
this.onUploadError_(e)
}
}.bind(this)
xhr.onerror = this.onUploadError_.bind(this)
xhr.send(JSON.stringify({
type: 'streaming',
upgrade_to_1080: this.upgrade_to_1080
}))
}
// -------------------------------------------------------------------------
// Private methods
/**
* Send the actual file content.
*
* @private
*/
me.prototype.sendFile_ = function() {
var content = this.file
var end = this.file.size
if (this.offset || this.chunkSize) {
// Only bother to slice the file if we're either resuming or uploading in chunks
if (this.chunkSize) {
end = Math.min(this.offset + this.chunkSize, this.file.size)
}
content = content.slice(this.offset, end)
}
var xhr = new XMLHttpRequest()
xhr.open('PUT', this.url, true)
xhr.setRequestHeader('Content-Type', this.contentType)
// xhr.setRequestHeader('Content-Length', this.file.size)
xhr.setRequestHeader('Content-Range', 'bytes ' + this.offset + '-' + (end - 1) + '/' + this.file.size)
if (xhr.upload) {
xhr.upload.addEventListener('progress', this.onProgress)
}
xhr.onload = this.onContentUploadSuccess_.bind(this)
xhr.onerror = this.onContentUploadError_.bind(this)
xhr.send(content)
}
/**
* Query for the state of the file for resumption.
*
* @private
*/
me.prototype.resume_ = function() {
var xhr = new XMLHttpRequest()
xhr.open('PUT', this.url, true)
xhr.setRequestHeader('Content-Range', 'bytes */' + this.file.size)
xhr.setRequestHeader('X-Upload-Content-Type', this.file.type)
if (xhr.upload) {
xhr.upload.addEventListener('progress', this.onProgress)
}
xhr.onload = this.onContentUploadSuccess_.bind(this)
xhr.onerror = this.onContentUploadError_.bind(this)
xhr.send()
}
/**
* Extract the last saved range if available in the request.
*
* @param {XMLHttpRequest} xhr Request object
*/
me.prototype.extractRange_ = function(xhr) {
var range = xhr.getResponseHeader('Range')
if (range) {
this.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1
}
}
/**
* The final step is to call vimeo.videos.upload.complete to queue up
* the video for transcoding.
*
* If successful call 'onUpdateVideoData_'
*
* @private
*/
me.prototype.complete_ = function(xhr) {
var xhr = new XMLHttpRequest()
xhr.open('DELETE', this.complete_url, true)
xhr.setRequestHeader('Authorization', 'Bearer ' + this.token)
xhr.onload = function(e) {
// Get the video location (videoId)
if (e.target.status < 400) {
var location = e.target.getResponseHeader('Location')
// Example of location: ' /videos/115365719', extract the video id only
var video_id = location.split('/').pop()
// Update the video metadata
this.onUpdateVideoData_(video_id)
} else {
this.onCompleteError_(e)
}
}.bind(this)
xhr.onerror = this.onCompleteError_.bind(this)
xhr.send()
}
/**
* Update the Video Data and add the metadata to the upload object
*
* @private
* @param {string} [id] Video Id
*/
me.prototype.onUpdateVideoData_ = function(video_id) {
var url = this.buildUrl_(video_id, [], defaults.api_url + '/videos/')
var httpMethod = 'PATCH'
var xhr = new XMLHttpRequest()
xhr.open(httpMethod, url, true)
xhr.setRequestHeader('Authorization', 'Bearer ' + this.token)
xhr.onload = function(e) {
// add the metadata
this.onGetMetadata_(e, video_id)
}.bind(this)
xhr.send(this.buildQuery_(this.videoData))
}
/**
* Retrieve the metadata from a successful onUpdateVideoData_ response
* This is is useful when uploading unlisted videos as the URI has changed.
*
* If successful call 'onUpdateVideoData_'
*
* @private
* @param {object} e XHR event
* @param {string} [id] Video Id
*/
me.prototype.onGetMetadata_ = function(e, video_id) {
// Get the video location (videoId)
if (e.target.status < 400) {
if (e.target.response) {
// add the returned metadata to the metadata array
var meta = JSON.parse(e.target.response)
// get the new index of the item
var index = this.metadata.push(meta) - 1
// call the complete method
this.onComplete(video_id, index)
} else {
this.onCompleteError_(e)
}
}
}
/**
* Handle successful responses for uploads. Depending on the context,
* may continue with uploading the next chunk of the file or, if complete,
* invokes vimeo complete service.
*
* @private
* @param {object} e XHR event
*/
me.prototype.onContentUploadSuccess_ = function(e) {
if (e.target.status == 200 || e.target.status == 201) {
this.complete_()
} else if (e.target.status == 308) {
this.extractRange_(e.target)
this.retryHandler.reset()
this.sendFile_()
}
}
/**
* Handles errors for uploads. Either retries or aborts depending
* on the error.
*
* @private
* @param {object} e XHR event
*/
me.prototype.onContentUploadError_ = function(e) {
if (e.target.status && e.target.status < 500) {
this.onError(e.target.response)
} else {
this.retryHandler.retry(this.resume_())
}
}
/**
* Handles errors for the complete request.
*
* @private
* @param {object} e XHR event
*/
me.prototype.onCompleteError_ = function(e) {
this.onError(e.target.response); // TODO - Retries for initial upload
}
/**
* Handles errors for the initial request.
*
* @private
* @param {object} e XHR event
*/
me.prototype.onUploadError_ = function(e) {
this.onError(e.target.response); // TODO - Retries for initial upload
}
/**
* Construct a query string from a hash/object
*
* @private
* @param {object} [params] Key/value pairs for query string
* @return {string} query string
*/
me.prototype.buildQuery_ = function(params) {
params = params || {}
return Object.keys(params).map(function(key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key])
}).join('&')
}
/**
* Build the drive upload URL
*
* @private
* @param {string} [id] File ID if replacing
* @param {object} [params] Query parameters
* @return {string} URL
*/
me.prototype.buildUrl_ = function(id, params, baseUrl) {
var url = baseUrl || defaults.api_url + '/me/videos'
if (id) {
url += id
}
var query = this.buildQuery_(params)
if (query) {
url += '?' + query
}
return url
}
return me
}))