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