1 /* The MIT License (MIT)
3 Copyright (c) 2014-2015 Benoit Tremblay <trembl.ben@gmail.com>
5 Permission is hereby granted, free of charge, to any person obtaining a copy
6 of this software and associated documentation files (the "Software"), to deal
7 in the Software without restriction, including without limitation the rights
8 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 copies of the Software, and to permit persons to whom the Software is
10 furnished to do so, subject to the following conditions:
12 The above copyright notice and this permission notice shall be included in
13 all copies or substantial portions of the Software.
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 (function (root, factory) {
24 if(typeof exports==='object' && typeof module!=='undefined') {
25 module.exports = factory(require('video.js'));
26 } else if(typeof define === 'function' && define.amd) {
27 define(['media_videojs/video'], function(videojs){
28 return (root.Youtube = factory(videojs));
31 root.Youtube = factory(root.videojs);
33 }(this, function(videojs) {
36 var Tech = videojs.getComponent('Tech');
38 var Youtube = videojs.extend(Tech, {
40 constructor: function(options, ready) {
41 Tech.call(this, options, ready);
43 this.setPoster(options.poster);
44 this.setSrc(this.options_.source, true);
46 // Set the vjs-youtube class to the player
47 // Parent is not set yet so we have to wait a tick
48 setTimeout(function() {
49 this.el_.parentNode.className += ' vjs-youtube';
52 this.el_.parentNode.className += ' vjs-youtube-mobile';
55 if (Youtube.isApiReady) {
58 Youtube.apiReadyQueue.push(this);
65 //Dispose of the YouTube Player
66 this.ytPlayer.stopVideo();
67 this.ytPlayer.destroy();
69 //YouTube API hasn't finished loading or the player is already disposed
70 var index = Youtube.apiReadyQueue.indexOf(this);
72 Youtube.apiReadyQueue.splice(index, 1);
77 this.el_.parentNode.className = this.el_.parentNode.className
78 .replace(' vjs-youtube', '')
79 .replace(' vjs-youtube-mobile', '');
80 this.el_.parentNode.removeChild(this.el_);
82 //Needs to be called after the YouTube player is destroyed, otherwise there will be a null reference exception
83 Tech.prototype.dispose.call(this);
86 createEl: function() {
87 var div = document.createElement('div');
88 div.setAttribute('id', this.options_.techId);
89 div.setAttribute('style', 'width:100%;height:100%;top:0;left:0;position:absolute');
90 div.setAttribute('class', 'vjs-tech');
92 var divWrapper = document.createElement('div');
93 divWrapper.appendChild(div);
95 if (!_isOnMobile && !this.options_.ytControls) {
96 var divBlocker = document.createElement('div');
97 divBlocker.setAttribute('class', 'vjs-iframe-blocker');
98 divBlocker.setAttribute('style', 'position:absolute;top:0;left:0;width:100%;height:100%');
100 // In case the blocker is still there and we want to pause
101 divBlocker.onclick = function() {
105 divWrapper.appendChild(divBlocker);
111 initYTPlayer: function() {
117 loop: this.options_.loop ? 1 : 0
120 // Let the user set any YouTube parameter
121 // https://developers.google.com/youtube/player_parameters?playerVersion=HTML5#Parameters
122 // To use YouTube controls, you must use ytControls instead
123 // To use the loop or autoplay, use the video.js settings
125 if (typeof this.options_.autohide !== 'undefined') {
126 playerVars.autohide = this.options_.autohide;
129 if (typeof this.options_['cc_load_policy'] !== 'undefined') {
130 playerVars['cc_load_policy'] = this.options_['cc_load_policy'];
133 if (typeof this.options_.ytControls !== 'undefined') {
134 playerVars.controls = this.options_.ytControls;
137 if (typeof this.options_.disablekb !== 'undefined') {
138 playerVars.disablekb = this.options_.disablekb;
141 if (typeof this.options_.end !== 'undefined') {
142 playerVars.end = this.options_.end;
145 if (typeof this.options_.color !== 'undefined') {
146 playerVars.color = this.options_.color;
149 if (!playerVars.controls) {
150 // Let video.js handle the fullscreen unless it is the YouTube native controls
152 } else if (typeof this.options_.fs !== 'undefined') {
153 playerVars.fs = this.options_.fs;
156 if (typeof this.options_.end !== 'undefined') {
157 playerVars.end = this.options_.end;
160 if (typeof this.options_.hl !== 'undefined') {
161 playerVars.hl = this.options_.hl;
162 } else if (typeof this.options_.language !== 'undefined') {
163 // Set the YouTube player on the same language than video.js
164 playerVars.hl = this.options_.language.substr(0, 2);
167 if (typeof this.options_['iv_load_policy'] !== 'undefined') {
168 playerVars['iv_load_policy'] = this.options_['iv_load_policy'];
171 if (typeof this.options_.list !== 'undefined') {
172 playerVars.list = this.options_.list;
173 } else if (this.url && typeof this.url.listId !== 'undefined') {
174 playerVars.list = this.url.listId;
177 if (typeof this.options_.listType !== 'undefined') {
178 playerVars.listType = this.options_.listType;
181 if (typeof this.options_.modestbranding !== 'undefined') {
182 playerVars.modestbranding = this.options_.modestbranding;
185 if (typeof this.options_.playlist !== 'undefined') {
186 playerVars.playlist = this.options_.playlist;
189 if (typeof this.options_.playsinline !== 'undefined') {
190 playerVars.playsinline = this.options_.playsinline;
193 if (typeof this.options_.rel !== 'undefined') {
194 playerVars.rel = this.options_.rel;
197 if (typeof this.options_.showinfo !== 'undefined') {
198 playerVars.showinfo = this.options_.showinfo;
201 if (typeof this.options_.start !== 'undefined') {
202 playerVars.start = this.options_.start;
205 if (typeof this.options_.theme !== 'undefined') {
206 playerVars.theme = this.options_.theme;
209 this.activeVideoId = this.url ? this.url.videoId : null;
210 this.activeList = playerVars.list;
212 this.ytPlayer = new YT.Player(this.options_.techId, {
213 videoId: this.activeVideoId,
214 playerVars: playerVars,
216 onReady: this.onPlayerReady.bind(this),
217 onPlaybackQualityChange: this.onPlayerPlaybackQualityChange.bind(this),
218 onStateChange: this.onPlayerStateChange.bind(this),
219 onError: this.onPlayerError.bind(this)
224 onPlayerReady: function() {
225 this.playerReady_ = true;
228 if (this.playOnReady) {
230 } else if (this.cueOnReady) {
231 this.ytPlayer.cueVideoById(this.url.videoId);
232 this.activeVideoId = this.url.videoId;
236 onPlayerPlaybackQualityChange: function() {
240 onPlayerStateChange: function(e) {
243 if (state === this.lastState || this.errorNumber) {
247 this.lastState = state;
251 this.trigger('loadstart');
252 this.trigger('loadedmetadata');
253 this.trigger('durationchange');
256 case YT.PlayerState.ENDED:
257 this.trigger('ended');
260 case YT.PlayerState.PLAYING:
261 this.trigger('timeupdate');
262 this.trigger('durationchange');
263 this.trigger('playing');
264 this.trigger('play');
266 if (this.isSeeking) {
271 case YT.PlayerState.PAUSED:
272 this.trigger('canplay');
273 if (this.isSeeking) {
276 this.trigger('pause');
280 case YT.PlayerState.BUFFERING:
281 this.player_.trigger('timeupdate');
282 this.player_.trigger('waiting');
287 onPlayerError: function(e) {
288 this.errorNumber = e.data;
289 this.trigger('error');
291 this.ytPlayer.stopVideo();
295 switch (this.errorNumber) {
297 return { code: 'Error while trying to play the video' };
301 return { code: 'Unable to find the video' };
305 return { code: 'Playback on other Websites has been disabled by the video owner.' };
308 return { code: 'YouTube unknown error (' + this.errorNumber + ')' };
313 this.setSrc({ src: src });
320 // You can't start programmaticlly a video with a mobile
321 // through the iframe so we hide the poster and the play button (with CSS)
329 setPoster: function(poster) {
330 this.poster_ = poster;
333 setSrc: function(source) {
334 if (!source || !source.src) {
338 delete this.errorNumber;
339 this.source = source;
340 this.url = Youtube.parseUrl(source.src);
342 if (!this.options_.poster) {
343 if (this.url.videoId) {
344 // Set the low resolution first
345 this.poster_ = 'https://img.youtube.com/vi/' + this.url.videoId + '/0.jpg';
346 this.trigger('posterchange');
348 // Check if their is a high res
349 this.checkHighResPoster();
353 if (this.options_.autoplay && !_isOnMobile) {
357 this.playOnReady = true;
359 } else if (this.activeVideoId !== this.url.videoId) {
361 this.ytPlayer.cueVideoById(this.url.videoId);
362 this.activeVideoId = this.url.videoId;
364 this.cueOnReady = true;
369 autoplay: function() {
370 return this.options_.autoplay;
373 setAutoplay: function(val) {
374 this.options_.autoplay = val;
378 return this.options_.loop;
381 setLoop: function(val) {
382 this.options_.loop = val;
386 if (!this.url || !this.url.videoId) {
390 this.wasPausedBeforeSeek = false;
393 if (this.url.listId) {
394 if (this.activeList === this.url.listId) {
395 this.ytPlayer.playVideo();
397 this.ytPlayer.loadPlaylist(this.url.listId);
398 this.activeList = this.url.listId;
402 if (this.activeVideoId === this.url.videoId) {
403 this.ytPlayer.playVideo();
405 this.ytPlayer.loadVideoById(this.url.videoId);
406 this.activeVideoId = this.url.videoId;
409 this.trigger('waiting');
410 this.playOnReady = true;
416 this.ytPlayer.pauseVideo();
421 return (this.ytPlayer) ?
422 (this.lastState !== YT.PlayerState.PLAYING && this.lastState !== YT.PlayerState.BUFFERING)
426 currentTime: function() {
427 return this.ytPlayer ? this.ytPlayer.getCurrentTime() : 0;
430 setCurrentTime: function(seconds) {
431 if (this.lastState === YT.PlayerState.PAUSED) {
432 this.timeBeforeSeek = this.currentTime();
435 if (!this.isSeeking) {
436 this.wasPausedBeforeSeek = this.paused();
439 this.ytPlayer.seekTo(seconds, true);
440 this.trigger('timeupdate');
441 this.trigger('seeking');
442 this.isSeeking = true;
444 // A seek event during pause does not return an event to trigger a seeked event,
445 // so run an interval timer to look for the currentTime to change
446 if (this.lastState === YT.PlayerState.PAUSED && this.timeBeforeSeek !== seconds) {
447 clearInterval(this.checkSeekedInPauseInterval);
448 this.checkSeekedInPauseInterval = setInterval(function() {
449 if (this.lastState !== YT.PlayerState.PAUSED || !this.isSeeking) {
450 // If something changed while we were waiting for the currentTime to change,
451 // clear the interval timer
452 clearInterval(this.checkSeekedInPauseInterval);
453 } else if (this.currentTime() !== this.timeBeforeSeek) {
454 this.trigger('timeupdate');
461 seeking: function () {
462 return this.isSeeking;
465 seekable: function () {
466 if(!this.ytPlayer || !this.ytPlayer.getVideoLoadedFraction) {
470 throw new Error('This TimeRanges object is empty');
473 throw new Error('This TimeRanges object is empty');
477 var end = this.ytPlayer.getDuration();
480 length: this.ytPlayer.getDuration(),
481 start: function() { return 0; },
482 end: function() { return end; }
486 onSeeked: function() {
487 clearInterval(this.checkSeekedInPauseInterval);
488 this.isSeeking = false;
490 if (this.wasPausedBeforeSeek) {
494 this.trigger('seeked');
497 playbackRate: function() {
498 return this.ytPlayer ? this.ytPlayer.getPlaybackRate() : 1;
501 setPlaybackRate: function(suggestedRate) {
502 if (!this.ytPlayer) {
506 this.ytPlayer.setPlaybackRate(suggestedRate);
507 this.trigger('ratechange');
510 duration: function() {
511 return this.ytPlayer ? this.ytPlayer.getDuration() : 0;
514 currentSrc: function() {
515 return this.source && this.source.src;
519 return this.ytPlayer ? (this.lastState === YT.PlayerState.ENDED) : false;
523 return this.ytPlayer ? this.ytPlayer.getVolume() / 100.0 : 1;
526 setVolume: function(percentAsDecimal) {
527 if (!this.ytPlayer) {
531 this.ytPlayer.setVolume(percentAsDecimal * 100.0);
532 this.setTimeout( function(){
533 this.trigger('volumechange');
539 return this.ytPlayer ? this.ytPlayer.isMuted() : false;
542 setMuted: function(mute) {
543 if (!this.ytPlayer) {
551 this.ytPlayer.mute();
553 this.ytPlayer.unMute();
555 this.setTimeout( function(){
556 this.trigger('volumechange');
560 buffered: function() {
561 if(!this.ytPlayer || !this.ytPlayer.getVideoLoadedFraction) {
565 throw new Error('This TimeRanges object is empty');
568 throw new Error('This TimeRanges object is empty');
573 var end = this.ytPlayer.getVideoLoadedFraction() * this.ytPlayer.getDuration();
576 length: this.ytPlayer.getDuration(),
577 start: function() { return 0; },
578 end: function() { return end; }
582 // TODO: Can we really do something with this on YouTUbe?
583 preload: function() {},
585 reset: function() {},
587 supportsFullScreen: function() {
591 // Tries to get the highest resolution thumbnail available for the video
592 checkHighResPoster: function(){
593 var uri = 'https://img.youtube.com/vi/' + this.url.videoId + '/maxresdefault.jpg';
596 var image = new Image();
597 image.onload = function(){
598 // Onload may still be called if YouTube returns the 120x90 error thumbnail
599 if('naturalHeight' in image){
600 if (image.naturalHeight <= 90 || image.naturalWidth <= 120) {
603 } else if(image.height <= 90 || image.width <= 120) {
608 this.trigger('posterchange');
610 image.onerror = function(){};
617 Youtube.isSupported = function() {
621 Youtube.canPlaySource = function(e) {
622 return Youtube.canPlayType(e.type);
625 Youtube.canPlayType = function(e) {
626 return (e === 'video/youtube');
629 var _isOnMobile = videojs.browser.IS_IOS || useNativeControlsOnAndroid();
631 Youtube.parseUrl = function(url) {
636 var regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
637 var match = url.match(regex);
639 if (match && match[2].length === 11) {
640 result.videoId = match[2];
643 var regPlaylist = /[?&]list=([^#\&\?]+)/;
644 match = url.match(regPlaylist);
646 if(match && match[1]) {
647 result.listId = match[1];
653 function apiLoaded() {
654 YT.ready(function() {
655 Youtube.isApiReady = true;
657 for (var i = 0; i < Youtube.apiReadyQueue.length; ++i) {
658 Youtube.apiReadyQueue[i].initYTPlayer();
663 function loadScript(src, callback) {
665 var tag = document.createElement('script');
666 var firstScriptTag = document.getElementsByTagName('script')[0];
667 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
668 tag.onload = function () {
674 tag.onreadystatechange = function () {
675 if (!loaded && (this.readyState === 'complete' || this.readyState === 'loaded')) {
683 function injectCss() {
684 var css = // iframe blocker to catch mouse events
685 '.vjs-youtube .vjs-iframe-blocker { display: none; }' +
686 '.vjs-youtube.vjs-user-inactive .vjs-iframe-blocker { display: block; }' +
687 '.vjs-youtube .vjs-poster { background-size: cover; }' +
688 '.vjs-youtube-mobile .vjs-big-play-button { display: none; }';
690 var head = document.head || document.getElementsByTagName('head')[0];
692 var style = document.createElement('style');
693 style.type = 'text/css';
695 if (style.styleSheet){
696 style.styleSheet.cssText = css;
698 style.appendChild(document.createTextNode(css));
701 head.appendChild(style);
704 function useNativeControlsOnAndroid() {
705 var stockRegex = window.navigator.userAgent.match(/applewebkit\/(\d*).*Version\/(\d*.\d*)/i);
706 //True only Android Stock Browser on OS versions 4.X and below
707 //where a Webkit version and a "Version/X.X" String can be found in
709 return videojs.browser.IS_ANDROID && videojs.browser.ANDROID_VERSION < 5 && stockRegex && stockRegex[2] > 0;
712 Youtube.apiReadyQueue = [];
714 loadScript('https://www.youtube.com/iframe_api', apiLoaded);
717 // Older versions of VJS5 doesn't have the registerTech function
718 if (typeof videojs.registerTech !== 'undefined') {
719 videojs.registerTech('Youtube', Youtube);
721 videojs.registerComponent('Youtube', Youtube);