| OLD | NEW |
| (Empty) | |
| 1 <link rel="import" href="../polymer/polymer.html"> |
| 2 <link rel="import" href="../google-signin/google-signin.html"> |
| 3 <link rel="import" href="../google-apis/google-client-loader.html"> |
| 4 <!-- |
| 5 Element enabling you to upload videos to YouTube. |
| 6 |
| 7 ##### Examples |
| 8 |
| 9 Manual upload with a `Video Upload` button once a video file is selected: |
| 10 |
| 11 <google-youtube-upload client-id="..."></google-youtube-upload> |
| 12 |
| 13 Automatic upload on video file select, with a custom title, and 'unlisted' priva
cy: |
| 14 |
| 15 <google-youtube-upload |
| 16 auto |
| 17 video-title="My Awesome Video" |
| 18 privacy-status="unlisted" |
| 19 client-id="..."> |
| 20 </google-youtube-upload> |
| 21 |
| 22 @demo |
| 23 --> |
| 24 <dom-module id="google-youtube-upload"> |
| 25 <link rel="import" type="css" href="google-youtube-upload.css"> |
| 26 <template> |
| 27 <script src="../cors-upload-sample/upload.js"></script> |
| 28 <google-client-loader on-js-api-load="_clientLoaded"></google-client-loader> |
| 29 <div id="login-logout"> |
| 30 <img id="channel-image" src="{{_channel.thumbnail}}" style$="{{_computeCha
nnelImageStyle(_channel.thumbnail)}}"> |
| 31 <span id="channel-name">{{_channel.name}}</span> |
| 32 |
| 33 <google-signin |
| 34 client-id="{{clientId}}" |
| 35 scopes="https://www.googleapis.com/auth/youtube.upload https://www.googl
eapis.com/auth/youtube.readonly"> |
| 36 </google-signin> |
| 37 </div> |
| 38 |
| 39 <div style="_computeUploadDivStyle(authenticated)}}"> |
| 40 <input type="file" id="file" class="button" accept="video/*" on-change="_h
andleFileChanged"> |
| 41 |
| 42 <button on-click="_handleUploadClicked" style$="{{_computeUploadButtonStyl
e(auto}}" disabled="{{!_selectedFile}}">Upload Video</button> |
| 43 |
| 44 <p id="disclaimer">By uploading a video, you certify that you own all righ
ts to the content or that you are authorized by the owner to make the content pu
blicly available on YouTube, and that it otherwise complies with the YouTube Ter
ms of Service located at <a href="http://www.youtube.com/t/terms" target="_blank
">http://www.youtube.com/t/terms</a></p> |
| 45 </div> |
| 46 </template> |
| 47 </dom-module> |
| 48 <script> |
| 49 (function() { |
| 50 // One minute. |
| 51 var STATUS_POLLING_ITERVAL_MILLIS = 60 * 1000; |
| 52 |
| 53 Polymer( { |
| 54 |
| 55 is: 'google-youtube-upload', |
| 56 /** |
| 57 * Fired when the upload begins. |
| 58 * |
| 59 * `e.detail` is set to the |
| 60 * [file](https://developer.mozilla.org/en-US/docs/Web/API/File) |
| 61 * being uploaded. |
| 62 * |
| 63 * @event youtube-upload-start |
| 64 * @param {Object} e Event parameters. |
| 65 */ |
| 66 |
| 67 /** |
| 68 * Fired while the upload is in progress. |
| 69 * |
| 70 * `e.detail.progressEvent` is set to the corresponding |
| 71 * [XMLHttpRequestProgressEvent](http://www.w3.org/TR/progress-events/). |
| 72 * |
| 73 * `e.detail.estimatedSecondsRemaining` is set to an estimate of the time
remaining |
| 74 * in the upload, based on the average upload speed so far. |
| 75 * |
| 76 * `e.detail.bytesPerSecond` is set to the average number of bytes sent pe
r second |
| 77 * sent so far. |
| 78 * |
| 79 * `e.fractionComplete` is set to the fraction of the upload that's comple
te, in the range [0, 1]. |
| 80 * |
| 81 * @event youtube-upload-progress |
| 82 * @param {Object} e Event parameters. |
| 83 */ |
| 84 |
| 85 /** |
| 86 * Fired when YouTube upload has failed. |
| 87 * |
| 88 * Since the actual upload failed, it's not possible for the YouTube serve
r to attempt to |
| 89 * process the video, and no `youtube-processing-poll` events will be fire
d. |
| 90 * |
| 91 * `e.detail` is set to a string explaining what went wrong. |
| 92 * |
| 93 * @event youtube-upload-fail |
| 94 * @param {Object} e Event parameters |
| 95 */ |
| 96 |
| 97 /** |
| 98 * Fired when video upload has completed, and YouTube has begun processing
the video. |
| 99 * |
| 100 * At this point, the video is not yet playable, and there is no guarantee
that |
| 101 * the server-side YouTube processing will succeed. |
| 102 * |
| 103 * One or more `youtube-processing-poll` events will then be fired after t
his event, |
| 104 * followed by either a `youtube-processing-complete` or `youtube-processi
ng-fail`. |
| 105 * |
| 106 * `e.detail` is set to the YouTube video id of the video. |
| 107 * |
| 108 * @event youtube-upload-complete |
| 109 * @param {Object} e Event parameters. |
| 110 */ |
| 111 |
| 112 /** |
| 113 * Fired while server-side processing is in progress. |
| 114 * |
| 115 * Server-side processing can take an |
| 116 * [unpredictable amount of time](https://support.google.com/youtube/answe
r/71674?hl=en&ref_topic=2888603), |
| 117 * and these events will be periodically fired each time the processing st
atus is polled. |
| 118 * |
| 119 * `e.detail` is set to a |
| 120 * [status](https://developers.google.com/youtube/v3/docs/videos#status) |
| 121 * object. |
| 122 * |
| 123 * @event youtube-processing-poll |
| 124 * @param {Object} e Event parameters |
| 125 */ |
| 126 |
| 127 /** |
| 128 * Fired when server-side processing is successful and the video is |
| 129 * available for playback on YouTube. |
| 130 * |
| 131 * The video can be played at `https://youtu.be/VIDEO_ID` and can be |
| 132 * embedded using the |
| 133 * [`google-youtube`](https://github.com/GoogleWebComponents/google-youtub
e) element. |
| 134 * |
| 135 * `e.detail` is set to the YouTube video id of the video. |
| 136 * |
| 137 * @event youtube-processing-complete |
| 138 * @param {Object} e Event parameters |
| 139 */ |
| 140 |
| 141 /** |
| 142 * Fired when the video |
| 143 * [failed transcoding](https://support.google.com/youtube/topic/2888603?h
l=en&ref_topic=16547) |
| 144 * and can't be played on YouTube. |
| 145 * |
| 146 * `e.detail` is set to a |
| 147 * [status](https://developers.google.com/youtube/v3/docs/videos#status) |
| 148 * object which has more details about the failure. |
| 149 * |
| 150 * @event youtube-processing-fail |
| 151 * @param {Object} e Event parameters |
| 152 */ |
| 153 properties: { |
| 154 /** |
| 155 * An OAuth 2 clientId reference, obtained from the |
| 156 * [Google Developers Console](https://console.developers.google.com). |
| 157 * |
| 158 * Follow |
| 159 * [the steps](https://developers.google.com/console/help/new/#generatin
goauth2) |
| 160 * for registering for OAuth 2, ensure that the |
| 161 * [YouTube Data API v3](https://developers.google.com/youtube/registeri
ng_an_application) |
| 162 * is enabled for your API project, and ensure that the JavaScript Origi
n |
| 163 * is set to the domain hosting the page on which |
| 164 * you'll be using this element. |
| 165 * |
| 166 * @attribute clientId |
| 167 * @type string |
| 168 * @default '' |
| 169 */ |
| 170 clientId: { |
| 171 type: String, |
| 172 value: '' |
| 173 }, |
| 174 |
| 175 /** |
| 176 * Whether files should be automatically uploaded. |
| 177 * |
| 178 * @attribute auto |
| 179 * @type boolean |
| 180 * @default false |
| 181 */ |
| 182 auto: { |
| 183 type: Boolean, |
| 184 value: false |
| 185 }, |
| 186 |
| 187 /** |
| 188 * Whether the user has authenticated or not. |
| 189 * |
| 190 * @attribute authenticated |
| 191 * @type boolean |
| 192 */ |
| 193 authenticated: { |
| 194 type: Boolean, |
| 195 value: false, |
| 196 readOnly: true |
| 197 }, |
| 198 |
| 199 /** |
| 200 * The title for the new YouTube video. |
| 201 * |
| 202 * @attribute videoTitle |
| 203 * @type string |
| 204 * @default 'Untitled Video' |
| 205 */ |
| 206 videoTitle: { |
| 207 type: String, |
| 208 value: 'Untitled Video' |
| 209 }, |
| 210 |
| 211 /** |
| 212 * The description for the new YouTube video. |
| 213 * |
| 214 * @attribute description |
| 215 * @type string |
| 216 * @default 'Uploaded via a web component! Check out https://github.com/
GoogleWebComponents/google-youtube-upload' |
| 217 */ |
| 218 description: { |
| 219 type: String, |
| 220 value: 'Uploaded via a web component! Check out https://github.com/Goo
gleWebComponents/google-youtube-upload' |
| 221 }, |
| 222 |
| 223 /** |
| 224 * The array of tags for the new YouTube video. |
| 225 * |
| 226 * @attribute tags |
| 227 * @type Array.<string> |
| 228 * @default ['google-youtube-upload'] |
| 229 */ |
| 230 tags: { |
| 231 type: Array, |
| 232 value: function() { return ['google-youtube-upload']} |
| 233 }, |
| 234 |
| 235 /** |
| 236 * The numeric YouTube |
| 237 * [cateogry id](https://developers.google.com/apis-explorer/#p/youtube/
v3/youtube.videoCategories.list?part=snippet®ionCode=us). |
| 238 * |
| 239 * @attribute categoryId |
| 240 * @type number |
| 241 * @default 22 |
| 242 */ |
| 243 categoryId: { |
| 244 type: Number, |
| 245 value: 22 |
| 246 }, |
| 247 |
| 248 /** |
| 249 * The [privacy setting](https://support.google.com/youtube/answer/15717
7?hl=en) |
| 250 * for the new video. |
| 251 * |
| 252 * Valid values are 'public', 'private', and 'unlisted'. |
| 253 * |
| 254 * @attribute privacyStatus |
| 255 * @type string |
| 256 * @default 'public' |
| 257 */ |
| 258 privacyStatus: { |
| 259 type: String, |
| 260 value: 'public' |
| 261 }, |
| 262 |
| 263 /** |
| 264 * The id of the new video. |
| 265 * |
| 266 * This is set as soon as a `youtube-upload-complete` event is fired. |
| 267 * |
| 268 * @attribute videoId |
| 269 * @type string |
| 270 * @default '' |
| 271 */ |
| 272 videoId: { |
| 273 type: String, |
| 274 value: '', |
| 275 readOnly: true, |
| 276 notify: true |
| 277 }, |
| 278 |
| 279 _channel: { |
| 280 type: Object, |
| 281 value: function() { return { |
| 282 name: 'Not Logged In', |
| 283 thumbnail: '' |
| 284 };} |
| 285 }, |
| 286 _selectedFile: { |
| 287 type: Object, |
| 288 value: null |
| 289 } |
| 290 }, |
| 291 |
| 292 _uploadStartTime: 0, |
| 293 |
| 294 ready: function() { |
| 295 document.addEventListener('google-signin-success', function(e) { |
| 296 |
| 297 this.accessToken = gapi.auth2.getAuthInstance().currentUser.get().getA
uthResponse().access_token; |
| 298 this._setAuthenticated(true); |
| 299 this._loadChannels(); |
| 300 }.bind(this)); |
| 301 |
| 302 document.addEventListener('google-signed-out', function(e) { |
| 303 this._setAuthenticated(false); |
| 304 this._loadChannelRequested = false; |
| 305 this.set('_channel.name', 'Not Logged In'); |
| 306 this.set('_channel.thumbnail', ''); |
| 307 }.bind(this)); |
| 308 }, |
| 309 |
| 310 _clientLoaded: function() { |
| 311 this._loadChannels(); |
| 312 }, |
| 313 |
| 314 _loadChannels: function() { |
| 315 if (gapi && gapi.client && this.authenticated && !this._loadChannelReque
sted) { |
| 316 this._loadChannelRequested = true; |
| 317 gapi.client.request({ |
| 318 path: '/youtube/v3/channels', |
| 319 params: { |
| 320 part: 'snippet', |
| 321 mine: true |
| 322 }, |
| 323 callback: function(response) { |
| 324 if (response.error) { |
| 325 this.fire('youtube-upload-fail', response.error.message); |
| 326 } else { |
| 327 this.set('_channel.name', response.items[0].snippet.title); |
| 328 this.set('_channel.thumbnail', response.items[0].snippet.thumbna
ils.default.url); |
| 329 } |
| 330 }.bind(this) |
| 331 }); |
| 332 } |
| 333 }, |
| 334 _computeChannelImageStyle: function(thumbnail) { |
| 335 return 'display:' + (thumbnail ? 'inline' : 'none'); |
| 336 }, |
| 337 |
| 338 _computeUploadDivStyle: function(authenticated) { |
| 339 return 'display:' + (authenticated ? 'block' : 'none'); |
| 340 }, |
| 341 _computeUploadButtonStyle: function(auto) { |
| 342 return 'display:' + (auto ? 'none' : 'block'); |
| 343 }, |
| 344 /** |
| 345 * Uploads a video file to YouTube. |
| 346 * |
| 347 * `this.accessToken` must already be set to a valid OAuth 2 access token. |
| 348 * |
| 349 * @method uploadFile |
| 350 * @param {object} file File object corresponding to the video to upload. |
| 351 */ |
| 352 uploadFile: function(file) { |
| 353 var metadata = { |
| 354 snippet: { |
| 355 title: this.videoTitle, |
| 356 description: this.description, |
| 357 tags: this.tags, |
| 358 categoryId: this.categoryId |
| 359 }, |
| 360 status: { |
| 361 privacyStatus: this.privacyStatus |
| 362 } |
| 363 }; |
| 364 |
| 365 var uploader = new MediaUploader({ |
| 366 baseUrl: 'https://www.googleapis.com/upload/youtube/v3/videos', |
| 367 file: file, |
| 368 token: this.accessToken, |
| 369 metadata: metadata, |
| 370 params: { |
| 371 part: Object.keys(metadata).join(',') |
| 372 }, |
| 373 onError: function(data) { |
| 374 var message = data; |
| 375 |
| 376 // Assuming the error is raised by the YouTube API, data will be |
| 377 // a JSON string with error.message set. I am not 100% sure that's t
he |
| 378 // only time onError will be raised, though. |
| 379 try { |
| 380 var errorResponse = JSON.parse(data); |
| 381 message = errorResponse.error.message; |
| 382 } finally { |
| 383 this.fire('youtube-upload-fail', message); |
| 384 } |
| 385 }.bind(this), |
| 386 onProgress: function(data) { |
| 387 var currentTime = Date.now(); |
| 388 var bytesUploaded = data.loaded; |
| 389 var totalBytes = data.total; |
| 390 |
| 391 // The times are in millis, so we need to divide by 1000 to get seco
nds. |
| 392 var bytesPerSecond = bytesUploaded / ((currentTime - this._uploadSta
rtTime) / 1000); |
| 393 var estimatedSecondsRemaining = (totalBytes - bytesUploaded) / bytes
PerSecond; |
| 394 var fractionComplete = bytesUploaded / totalBytes; |
| 395 |
| 396 this.fire('youtube-upload-progress', { |
| 397 progressEvent: data, |
| 398 bytesPerSecond: bytesPerSecond, |
| 399 estimatedSecondsRemaining: estimatedSecondsRemaining, |
| 400 fractionComplete: fractionComplete |
| 401 }); |
| 402 }.bind(this), |
| 403 onComplete: function(data) { |
| 404 var uploadResponse = JSON.parse(data); |
| 405 this.fire('youtube-upload-complete', uploadResponse.id); |
| 406 this._setVideoId(uploadResponse.id); |
| 407 |
| 408 this._pollForVideoStatus(); |
| 409 }.bind(this) |
| 410 }); |
| 411 |
| 412 this.fire('youtube-upload-start', file); |
| 413 // This won't correspond to the *exact* start of the upload, but it shou
ld be close enough. |
| 414 this._uploadStartTime = Date.now(); |
| 415 uploader.upload(); |
| 416 }, |
| 417 |
| 418 _handleFileChanged: function() { |
| 419 this._selectedFile = this.$.file.files[0]; |
| 420 |
| 421 if (this.auto) { |
| 422 this.uploadFile(this._selectedFile); |
| 423 } |
| 424 }, |
| 425 |
| 426 _handleUploadClicked: function() { |
| 427 this.uploadFile(this._selectedFile); |
| 428 }, |
| 429 |
| 430 _pollForVideoStatus: function() { |
| 431 gapi.client.request({ |
| 432 path: '/youtube/v3/videos', |
| 433 params: { |
| 434 part: 'status', |
| 435 id: this.videoId |
| 436 }, |
| 437 callback: function(response) { |
| 438 if (response.error) { |
| 439 // Not exactly sure how to handle this one, since it means the sta
tus polling failed. |
| 440 setTimeout(this._pollForVideoStatus.bind(this), STATUS_POLLING_ITE
RVAL_MILLIS); |
| 441 } else { |
| 442 var status = response.items[0].status; |
| 443 |
| 444 switch (status.uploadStatus) { |
| 445 // This is a non-final status, so we need to poll again. |
| 446 case 'uploaded': |
| 447 this.fire('youtube-processing-poll', status); |
| 448 setTimeout(this._pollForVideoStatus.bind(this), STATUS_POLLING
_ITERVAL_MILLIS); |
| 449 break; |
| 450 |
| 451 // The video was successfully transcoded and is available. |
| 452 case 'processed': |
| 453 this.fire('youtube-processing-complete', this.videoId); |
| 454 break; |
| 455 |
| 456 // All other statuses indicate a permanent transcoding failure. |
| 457 default: |
| 458 this.fire('youtube-processing-fail', status); |
| 459 break; |
| 460 } |
| 461 } |
| 462 }.bind(this) |
| 463 }); |
| 464 } |
| 465 }); |
| 466 })(); |
| 467 </script> |
| OLD | NEW |