blob: f6970680e138805065287399c75523c061b18e5f [file] [log] [blame]
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../google-signin/google-signin.html">
<link rel="import" href="../google-apis/google-client-loader.html">
<!--
Element enabling you to upload videos to YouTube.
##### Examples
Manual upload with a `Video Upload` button once a video file is selected:
<google-youtube-upload client-id="..."></google-youtube-upload>
Automatic upload on video file select, with a custom title, and 'unlisted' privacy:
<google-youtube-upload
auto
video-title="My Awesome Video"
privacy-status="unlisted"
client-id="...">
</google-youtube-upload>
@demo
-->
<dom-module id="google-youtube-upload">
<link rel="import" type="css" href="google-youtube-upload.css">
<template>
<script src="../cors-upload-sample/upload.js"></script>
<google-client-loader on-js-api-load="_clientLoaded"></google-client-loader>
<div id="login-logout">
<img id="channel-image" src="{{_channel.thumbnail}}" style$="{{_computeChannelImageStyle(_channel.thumbnail)}}">
<span id="channel-name">{{_channel.name}}</span>
<google-signin
client-id="{{clientId}}"
scopes="https://www.googleapis.com/auth/youtube.upload https://www.googleapis.com/auth/youtube.readonly">
</google-signin>
</div>
<div style="_computeUploadDivStyle(authenticated)}}">
<input type="file" id="file" class="button" accept="video/*" on-change="_handleFileChanged">
<button on-click="_handleUploadClicked" style$="{{_computeUploadButtonStyle(auto}}" disabled="{{!_selectedFile}}">Upload Video</button>
<p id="disclaimer">By uploading a video, you certify that you own all rights to the content or that you are authorized by the owner to make the content publicly available on YouTube, and that it otherwise complies with the YouTube Terms of Service located at <a href="http://www.youtube.com/t/terms" target="_blank">http://www.youtube.com/t/terms</a></p>
</div>
</template>
</dom-module>
<script>
(function() {
// One minute.
var STATUS_POLLING_ITERVAL_MILLIS = 60 * 1000;
Polymer( {
is: 'google-youtube-upload',
/**
* Fired when the upload begins.
*
* `e.detail` is set to the
* [file](https://developer.mozilla.org/en-US/docs/Web/API/File)
* being uploaded.
*
* @event youtube-upload-start
* @param {Object} e Event parameters.
*/
/**
* Fired while the upload is in progress.
*
* `e.detail.progressEvent` is set to the corresponding
* [XMLHttpRequestProgressEvent](http://www.w3.org/TR/progress-events/).
*
* `e.detail.estimatedSecondsRemaining` is set to an estimate of the time remaining
* in the upload, based on the average upload speed so far.
*
* `e.detail.bytesPerSecond` is set to the average number of bytes sent per second
* sent so far.
*
* `e.fractionComplete` is set to the fraction of the upload that's complete, in the range [0, 1].
*
* @event youtube-upload-progress
* @param {Object} e Event parameters.
*/
/**
* Fired when YouTube upload has failed.
*
* Since the actual upload failed, it's not possible for the YouTube server to attempt to
* process the video, and no `youtube-processing-poll` events will be fired.
*
* `e.detail` is set to a string explaining what went wrong.
*
* @event youtube-upload-fail
* @param {Object} e Event parameters
*/
/**
* Fired when video upload has completed, and YouTube has begun processing the video.
*
* At this point, the video is not yet playable, and there is no guarantee that
* the server-side YouTube processing will succeed.
*
* One or more `youtube-processing-poll` events will then be fired after this event,
* followed by either a `youtube-processing-complete` or `youtube-processing-fail`.
*
* `e.detail` is set to the YouTube video id of the video.
*
* @event youtube-upload-complete
* @param {Object} e Event parameters.
*/
/**
* Fired while server-side processing is in progress.
*
* Server-side processing can take an
* [unpredictable amount of time](https://support.google.com/youtube/answer/71674?hl=en&ref_topic=2888603),
* and these events will be periodically fired each time the processing status is polled.
*
* `e.detail` is set to a
* [status](https://developers.google.com/youtube/v3/docs/videos#status)
* object.
*
* @event youtube-processing-poll
* @param {Object} e Event parameters
*/
/**
* Fired when server-side processing is successful and the video is
* available for playback on YouTube.
*
* The video can be played at `https://youtu.be/VIDEO_ID` and can be
* embedded using the
* [`google-youtube`](https://github.com/GoogleWebComponents/google-youtube) element.
*
* `e.detail` is set to the YouTube video id of the video.
*
* @event youtube-processing-complete
* @param {Object} e Event parameters
*/
/**
* Fired when the video
* [failed transcoding](https://support.google.com/youtube/topic/2888603?hl=en&ref_topic=16547)
* and can't be played on YouTube.
*
* `e.detail` is set to a
* [status](https://developers.google.com/youtube/v3/docs/videos#status)
* object which has more details about the failure.
*
* @event youtube-processing-fail
* @param {Object} e Event parameters
*/
properties: {
/**
* An OAuth 2 clientId reference, obtained from the
* [Google Developers Console](https://console.developers.google.com).
*
* Follow
* [the steps](https://developers.google.com/console/help/new/#generatingoauth2)
* for registering for OAuth 2, ensure that the
* [YouTube Data API v3](https://developers.google.com/youtube/registering_an_application)
* is enabled for your API project, and ensure that the JavaScript Origin
* is set to the domain hosting the page on which
* you'll be using this element.
*
* @attribute clientId
* @type string
* @default ''
*/
clientId: {
type: String,
value: ''
},
/**
* Whether files should be automatically uploaded.
*
* @attribute auto
* @type boolean
* @default false
*/
auto: {
type: Boolean,
value: false
},
/**
* Whether the user has authenticated or not.
*
* @attribute authenticated
* @type boolean
*/
authenticated: {
type: Boolean,
value: false,
readOnly: true
},
/**
* The title for the new YouTube video.
*
* @attribute videoTitle
* @type string
* @default 'Untitled Video'
*/
videoTitle: {
type: String,
value: 'Untitled Video'
},
/**
* The description for the new YouTube video.
*
* @attribute description
* @type string
* @default 'Uploaded via a web component! Check out https://github.com/GoogleWebComponents/google-youtube-upload'
*/
description: {
type: String,
value: 'Uploaded via a web component! Check out https://github.com/GoogleWebComponents/google-youtube-upload'
},
/**
* The array of tags for the new YouTube video.
*
* @attribute tags
* @type Array.<string>
* @default ['google-youtube-upload']
*/
tags: {
type: Array,
value: function() { return ['google-youtube-upload']}
},
/**
* The numeric YouTube
* [cateogry id](https://developers.google.com/apis-explorer/#p/youtube/v3/youtube.videoCategories.list?part=snippet&regionCode=us).
*
* @attribute categoryId
* @type number
* @default 22
*/
categoryId: {
type: Number,
value: 22
},
/**
* The [privacy setting](https://support.google.com/youtube/answer/157177?hl=en)
* for the new video.
*
* Valid values are 'public', 'private', and 'unlisted'.
*
* @attribute privacyStatus
* @type string
* @default 'public'
*/
privacyStatus: {
type: String,
value: 'public'
},
/**
* The id of the new video.
*
* This is set as soon as a `youtube-upload-complete` event is fired.
*
* @attribute videoId
* @type string
* @default ''
*/
videoId: {
type: String,
value: '',
readOnly: true,
notify: true
},
_channel: {
type: Object,
value: function() { return {
name: 'Not Logged In',
thumbnail: ''
};}
},
_selectedFile: {
type: Object,
value: null
}
},
_uploadStartTime: 0,
ready: function() {
document.addEventListener('google-signin-success', function(e) {
this.accessToken = gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse().access_token;
this._setAuthenticated(true);
this._loadChannels();
}.bind(this));
document.addEventListener('google-signed-out', function(e) {
this._setAuthenticated(false);
this._loadChannelRequested = false;
this.set('_channel.name', 'Not Logged In');
this.set('_channel.thumbnail', '');
}.bind(this));
},
_clientLoaded: function() {
this._loadChannels();
},
_loadChannels: function() {
if (gapi && gapi.client && this.authenticated && !this._loadChannelRequested) {
this._loadChannelRequested = true;
gapi.client.request({
path: '/youtube/v3/channels',
params: {
part: 'snippet',
mine: true
},
callback: function(response) {
if (response.error) {
this.fire('youtube-upload-fail', response.error.message);
} else {
this.set('_channel.name', response.items[0].snippet.title);
this.set('_channel.thumbnail', response.items[0].snippet.thumbnails.default.url);
}
}.bind(this)
});
}
},
_computeChannelImageStyle: function(thumbnail) {
return 'display:' + (thumbnail ? 'inline' : 'none');
},
_computeUploadDivStyle: function(authenticated) {
return 'display:' + (authenticated ? 'block' : 'none');
},
_computeUploadButtonStyle: function(auto) {
return 'display:' + (auto ? 'none' : 'block');
},
/**
* Uploads a video file to YouTube.
*
* `this.accessToken` must already be set to a valid OAuth 2 access token.
*
* @method uploadFile
* @param {object} file File object corresponding to the video to upload.
*/
uploadFile: function(file) {
var metadata = {
snippet: {
title: this.videoTitle,
description: this.description,
tags: this.tags,
categoryId: this.categoryId
},
status: {
privacyStatus: this.privacyStatus
}
};
var uploader = new MediaUploader({
baseUrl: 'https://www.googleapis.com/upload/youtube/v3/videos',
file: file,
token: this.accessToken,
metadata: metadata,
params: {
part: Object.keys(metadata).join(',')
},
onError: function(data) {
var message = data;
// Assuming the error is raised by the YouTube API, data will be
// a JSON string with error.message set. I am not 100% sure that's the
// only time onError will be raised, though.
try {
var errorResponse = JSON.parse(data);
message = errorResponse.error.message;
} finally {
this.fire('youtube-upload-fail', message);
}
}.bind(this),
onProgress: function(data) {
var currentTime = Date.now();
var bytesUploaded = data.loaded;
var totalBytes = data.total;
// The times are in millis, so we need to divide by 1000 to get seconds.
var bytesPerSecond = bytesUploaded / ((currentTime - this._uploadStartTime) / 1000);
var estimatedSecondsRemaining = (totalBytes - bytesUploaded) / bytesPerSecond;
var fractionComplete = bytesUploaded / totalBytes;
this.fire('youtube-upload-progress', {
progressEvent: data,
bytesPerSecond: bytesPerSecond,
estimatedSecondsRemaining: estimatedSecondsRemaining,
fractionComplete: fractionComplete
});
}.bind(this),
onComplete: function(data) {
var uploadResponse = JSON.parse(data);
this.fire('youtube-upload-complete', uploadResponse.id);
this._setVideoId(uploadResponse.id);
this._pollForVideoStatus();
}.bind(this)
});
this.fire('youtube-upload-start', file);
// This won't correspond to the *exact* start of the upload, but it should be close enough.
this._uploadStartTime = Date.now();
uploader.upload();
},
_handleFileChanged: function() {
this._selectedFile = this.$.file.files[0];
if (this.auto) {
this.uploadFile(this._selectedFile);
}
},
_handleUploadClicked: function() {
this.uploadFile(this._selectedFile);
},
_pollForVideoStatus: function() {
gapi.client.request({
path: '/youtube/v3/videos',
params: {
part: 'status',
id: this.videoId
},
callback: function(response) {
if (response.error) {
// Not exactly sure how to handle this one, since it means the status polling failed.
setTimeout(this._pollForVideoStatus.bind(this), STATUS_POLLING_ITERVAL_MILLIS);
} else {
var status = response.items[0].status;
switch (status.uploadStatus) {
// This is a non-final status, so we need to poll again.
case 'uploaded':
this.fire('youtube-processing-poll', status);
setTimeout(this._pollForVideoStatus.bind(this), STATUS_POLLING_ITERVAL_MILLIS);
break;
// The video was successfully transcoded and is available.
case 'processed':
this.fire('youtube-processing-complete', this.videoId);
break;
// All other statuses indicate a permanent transcoding failure.
default:
this.fire('youtube-processing-fail', status);
break;
}
}
}.bind(this)
});
}
});
})();
</script>