ember-youtube
Version:
A simple Ember.js component to play and control single YouTube videos using the iframe API.
445 lines (400 loc) • 10.3 kB
JavaScript
/* global YT, window */
import Component from '@ember/component';
import RSVP from 'rsvp'
import { computed, getProperties, setProperties, observer } from '@ember/object';
import { debug } from '@ember/debug';
import { run } from '@ember/runloop';
import { task } from 'ember-concurrency';
export default Component.extend({
classNames: ['EmberYoutube'],
ytid: null,
width: 560,
height: 315,
// These options are used to load a video.
startSeconds: undefined,
endSeconds: undefined,
suggestedQuality: undefined,
lazyload: false,
showControls: false,
showDebug: false,
showProgress: false,
showExtras: computed.or('showControls', 'showProgress', 'showDebug'),
player: null,
playerState: 'loading',
/* Hooks */
playerCreated() {
/* Callback to be passed. */
},
playerStateChanged() {
/* Callback to be passed. */
},
error() {
/* Callback to be passed. */
},
/* State hooks */
ready() {
/* Callback to be passed. */
},
ended() {
/* Callback to be passed. */
},
playing() {
/* Callback to be passed. */
},
paused() {
/* Callback to be passed. */
},
buffering() {
/* Callback to be passed. */
},
queued() {
/* Callback to be passed. */
},
init() {
this._super();
setProperties(this, {
// YouTube's embedded player can take a number of optional parameters.
// https://developers.google.com/youtube/player_parameters#Parameters
// https://developers.google.com/youtube/youtube_player_demo
playerVars: Object.assign({}, this.playerVars),
// from YT.PlayerState
stateNames: {
'-1': 'ready', // READY
0: 'ended', // YT.Player.ENDED
1: 'playing', // YT.PlayerState.PLAYING
2: 'paused', // YT.PlayerState.PAUSED
3: 'buffering', // YT.PlayerState.BUFFERING
5: 'queued' // YT.PlayerState.CUED
}
});
this._register();
},
// Expose the component to the outside world.
_register() {
const delegate = this.get('delegate');
const delegateAs = this.get('delegate-as');
run.schedule('afterRender', () => {
if (!delegate) {
return;
}
delegate.set(delegateAs || 'emberYouTube', this);
});
},
didInsertElement() {
this._super(...arguments);
this.addProgressBarClickHandler();
if (!this.get('lazyload') && this.get('ytid')) {
// If "lazyload" is not enabled and we have an ID, we can start immediately.
// Otherwise the `loadVideo` observer will take care of things.
this.get('loadAndCreatePlayer').perform();
}
},
willDestroyElement() {
this.get('loadAndCreatePlayer').cancelAll();
// clear the timer
this.stopTimer();
// remove progress bar click handler
this.removeProgressBarClickHandler();
// destroy video player
const player = this.get('player');
if (player) {
player.destroy();
this.set('player', null);
}
// clear up if "delegated"
const delegate = this.get('delegate');
const delegateAs = this.get('delegate-as');
if (delegate) {
delegate.set(delegateAs || 'emberYouTube', null);
}
},
loadAndCreatePlayer: task(function * () {
try {
yield this.loadYouTubeApi();
let player = yield this.createPlayer();
this.setProperties({
player,
playerState: 'ready'
});
this.playerCreated(player);
this.loadVideo();
} catch(err) {
if (this.get('showDebug')) {
debug(err);
}
throw err
}
}).drop(),
// A promise that is resolved when window.onYouTubeIframeAPIReady is called.
// The promise is resolved with a reference to window.YT object.
loadYouTubeApi() {
return new RSVP.Promise((resolve) => {
let previous;
previous = window.onYouTubeIframeAPIReady;
// The API will call this function when page has finished downloading
// the JavaScript for the player API.
window.onYouTubeIframeAPIReady = () => {
if (previous) {
previous();
}
resolve(window.YT);
};
if (window.YT && window.YT.loaded) {
// If already loaded, make sure not to load the script again.
resolve(window.YT);
} else {
let ytScript = document.createElement("script");
ytScript.src = "https://www.youtube.com/iframe_api";
document.head.appendChild(ytScript);
}
});
},
// A promise that is immediately resolved with a YouTube player object.
createPlayer() {
const playerVars = this.get('playerVars');
const width = this.get('width');
const height = this.get('height');
const container = this.element.querySelector('.EmberYoutube-player');
let player;
return new RSVP.Promise((resolve, reject) => {
if (!container) {
reject(`Couldn't find the container element to create a YouTube player`);
}
player = new YT.Player(container, {
width,
height,
playerVars,
events: {
onReady() {
resolve(player);
},
onStateChange: this.onPlayerStateChange.bind(this),
onError: this.onPlayerError.bind(this)
}
});
});
},
// Gets called by the YouTube player.
onPlayerStateChange(event) {
// Set a readable state name
let state = this.get('stateNames.' + event.data.toString());
this.set('playerState', state);
if (this.get('showDebug')) {
debug(state);
}
// send actions outside
this[state](event);
this.playerStateChanged(event);
// send actions inside
this.send(state);
},
// Gets called by the YouTube player.
onPlayerError(event) {
let errorCode = event.data;
this.set('playerState', 'error');
// Send the event to the controller
this.error(errorCode);
if (this.get('showDebug')) {
debug('error' + errorCode);
}
// switch(errorCode) {
// case 2:
// debug('Invalid parameter');
// break;
// case 100:
// debug('Not found/private');
// this.send('playNext');
// break;
// case 101:
// case 150:
// debug('Embed not allowed');
// this.send('playNext');
// break;
// default:
// break;
// }
},
// Returns a boolean that indicates playback status by looking at the player state.
isPlaying: computed('playerState', {
get() {
const player = this.get('player');
if (!player) {
return false;
}
return player.getPlayerState() === 1;
}
}),
// Load (and plays) a video every time ytid changes.
ytidDidChange: observer('ytid', function () {
const player = this.get('player');
const ytid = this.get('ytid');
if (!ytid) {
return;
}
if (!player) {
this.get('loadAndCreatePlayer').perform();
return;
}
this.loadVideo();
}),
loadVideo() {
const player = this.get('player');
const ytid = this.get('ytid');
// Set parameters for the video to be played.
let options = getProperties(this, ['startSeconds', 'endSeconds', 'suggestedQuality']);
options.videoId = ytid;
// Either load or cue depending on `autoplay`.
if (this.playerVars.autoplay) {
player.loadVideoById(options);
} else {
player.cueVideoById(options);
}
},
updateTime() {
const player = this.get('player');
if (player && player.getDuration && player.getCurrentTime) {
this.set('currentTime', player.getCurrentTime());
this.set('duration', player.getDuration());
}
},
startTimer() {
// stop any previously started timer we forgot to clear
this.stopTimer();
// set initial time by getting the computed properties
this.updateTime();
// and also once every second so the progressbar is up to date
let timer = window.setInterval(() => {
this.updateTime();
}, 1000);
// save the timer so we can stop it later
this.set('timer', timer);
},
stopTimer() {
window.clearInterval(this.get('timer'));
},
// A wrapper around the YouTube method to get current time.
currentTime: computed({
get() {
let player = this.get('player');
let value = player ? player.getCurrentTime() : 0;
return value;
},
set(key, value) {
return value;
}
}),
// A wrapper around the YouTube method to get the duration.
duration: computed({
get() {
let player = this.get('player');
let value = player ? player.getDuration() : 0;
return value;
},
set(key, value) {
return value;
}
}),
// A wrapper around the YouTube method to get and set volume.
volume: computed({
get() {
let player = this.get('player');
let value = player ? player.getVolume() : 0;
return value;
},
set(name, vol) {
let player = this.get('player');
// Clamp between 0 and 100
if (vol > 100) {
vol = 100;
} else if (vol < 0) {
vol = 0;
}
if (player) {
player.setVolume(vol);
}
return vol;
}
}),
// OK, this is stupid but couldn't access the "event" inside
// an ember action so here's a manual click handler instead.
addProgressBarClickHandler() {
this.element.addEventListener(
"click",
this.progressBarClick.bind(this),
false
);
},
progressBarClick(event) {
let self = this;
let element = event.srcElement;
if (element.tagName.toLowerCase() !== "progress") return;
// get the x position of the click inside our progress el
let x = event.pageX - element.getBoundingClientRect().x;
// convert it to a value relative to the duration (max)
let clickedValue = (x * element.max) / element.offsetWidth;
// 250 = 0.25 seconds into player
self.set("currentTime", clickedValue);
self.send("seekTo", clickedValue);
},
removeProgressBarClickHandler() {
this.element.removeEventListener(
"click",
this.progressBarClick.bind(this),
false
);
},
actions: {
play() {
if (this.get('player')) {
this.get('player').playVideo();
}
},
pause() {
if (this.get('player')) {
this.get('player').pauseVideo();
}
},
togglePlay() {
if (this.get('player') && this.get('isPlaying')) {
this.send('pause');
} else {
this.send('play');
}
},
mute() {
if (this.get('player')) {
this.get('player').mute();
this.set('isMuted', true);
}
},
unMute() {
if (this.get('player')) {
this.get('player').unMute();
this.set('isMuted', false);
}
},
toggleVolume() {
if (this.get('player').isMuted()) {
this.send('unMute');
} else {
this.send('mute');
}
},
seekTo(seconds) {
if (this.get('player')) {
this.get('player').seekTo(seconds);
}
},
// YouTube events.
ready() {},
ended() {},
playing() {
this.startTimer();
},
paused() {
this.stopTimer();
},
buffering() {},
queued() {}
}
});