MDL-38158 media_videojs: Add Video.JS player
[moodle.git] / media / player / videojs / amd / src / Youtube.js
blobc4c8c4d66e08c9f89ef50488a9571bf9b18d8c3f
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
21 THE SOFTWARE. */
22 /*global define, YT*/
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));
29     });
30   } else {
31     root.Youtube = factory(root.videojs);
32   }
33 }(this, function(videojs) {
34   'use strict';
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';
51         if (_isOnMobile) {
52           this.el_.parentNode.className += ' vjs-youtube-mobile';
53         }
55         if (Youtube.isApiReady) {
56           this.initYTPlayer();
57         } else {
58           Youtube.apiReadyQueue.push(this);
59         }
60       }.bind(this));
61     },
63     dispose: function() {
64       if (this.ytPlayer) {
65         //Dispose of the YouTube Player
66         this.ytPlayer.stopVideo();
67         this.ytPlayer.destroy();
68       } else {
69         //YouTube API hasn't finished loading or the player is already disposed
70         var index = Youtube.apiReadyQueue.indexOf(this);
71         if (index !== -1) {
72           Youtube.apiReadyQueue.splice(index, 1);
73         }
74       }
75       this.ytPlayer = null;
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);
84     },
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() {
102           this.pause();
103         }.bind(this);
105         divWrapper.appendChild(divBlocker);
106       }
108       return divWrapper;
109     },
111     initYTPlayer: function() {
112       var playerVars = {
113         controls: 0,
114         modestbranding: 1,
115         rel: 0,
116         showinfo: 0,
117         loop: this.options_.loop ? 1 : 0
118       };
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;
127       }
129       if (typeof this.options_['cc_load_policy'] !== 'undefined') {
130         playerVars['cc_load_policy'] = this.options_['cc_load_policy'];
131       }
133       if (typeof this.options_.ytControls !== 'undefined') {
134         playerVars.controls = this.options_.ytControls;
135       }
137       if (typeof this.options_.disablekb !== 'undefined') {
138         playerVars.disablekb = this.options_.disablekb;
139       }
141       if (typeof this.options_.end !== 'undefined') {
142         playerVars.end = this.options_.end;
143       }
145       if (typeof this.options_.color !== 'undefined') {
146         playerVars.color = this.options_.color;
147       }
149       if (!playerVars.controls) {
150         // Let video.js handle the fullscreen unless it is the YouTube native controls
151         playerVars.fs = 0;
152       } else if (typeof this.options_.fs !== 'undefined') {
153         playerVars.fs = this.options_.fs;
154       }
156       if (typeof this.options_.end !== 'undefined') {
157         playerVars.end = this.options_.end;
158       }
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);
165       }
167       if (typeof this.options_['iv_load_policy'] !== 'undefined') {
168         playerVars['iv_load_policy'] = this.options_['iv_load_policy'];
169       }
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;
175       }
177       if (typeof this.options_.listType !== 'undefined') {
178         playerVars.listType = this.options_.listType;
179       }
181       if (typeof this.options_.modestbranding !== 'undefined') {
182         playerVars.modestbranding = this.options_.modestbranding;
183       }
185       if (typeof this.options_.playlist !== 'undefined') {
186         playerVars.playlist = this.options_.playlist;
187       }
189       if (typeof this.options_.playsinline !== 'undefined') {
190         playerVars.playsinline = this.options_.playsinline;
191       }
193       if (typeof this.options_.rel !== 'undefined') {
194         playerVars.rel = this.options_.rel;
195       }
197       if (typeof this.options_.showinfo !== 'undefined') {
198         playerVars.showinfo = this.options_.showinfo;
199       }
201       if (typeof this.options_.start !== 'undefined') {
202         playerVars.start = this.options_.start;
203       }
205       if (typeof this.options_.theme !== 'undefined') {
206         playerVars.theme = this.options_.theme;
207       }
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,
215         events: {
216           onReady: this.onPlayerReady.bind(this),
217           onPlaybackQualityChange: this.onPlayerPlaybackQualityChange.bind(this),
218           onStateChange: this.onPlayerStateChange.bind(this),
219           onError: this.onPlayerError.bind(this)
220         }
221       });
222     },
224     onPlayerReady: function() {
225       this.playerReady_ = true;
226       this.triggerReady();
228       if (this.playOnReady) {
229         this.play();
230       } else if (this.cueOnReady) {
231         this.ytPlayer.cueVideoById(this.url.videoId);
232         this.activeVideoId = this.url.videoId;
233       }
234     },
236     onPlayerPlaybackQualityChange: function() {
238     },
240     onPlayerStateChange: function(e) {
241       var state = e.data;
243       if (state === this.lastState || this.errorNumber) {
244         return;
245       }
247       this.lastState = state;
249       switch (state) {
250         case -1:
251           this.trigger('loadstart');
252           this.trigger('loadedmetadata');
253           this.trigger('durationchange');
254           break;
256         case YT.PlayerState.ENDED:
257           this.trigger('ended');
258           break;
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) {
267             this.onSeeked();
268           }
269           break;
271         case YT.PlayerState.PAUSED:
272           this.trigger('canplay');
273           if (this.isSeeking) {
274             this.onSeeked();
275           } else {
276             this.trigger('pause');
277           }
278           break;
280         case YT.PlayerState.BUFFERING:
281           this.player_.trigger('timeupdate');
282           this.player_.trigger('waiting');
283           break;
284       }
285     },
287     onPlayerError: function(e) {
288       this.errorNumber = e.data;
289       this.trigger('error');
291       this.ytPlayer.stopVideo();
292     },
294     error: function() {
295       switch (this.errorNumber) {
296         case 5:
297           return { code: 'Error while trying to play the video' };
299         case 2:
300         case 100:
301           return { code: 'Unable to find the video' };
303         case 101:
304         case 150:
305           return { code: 'Playback on other Websites has been disabled by the video owner.' };
306       }
308       return { code: 'YouTube unknown error (' + this.errorNumber + ')' };
309     },
311     src: function(src) {
312       if (src) {
313         this.setSrc({ src: src });
314       }
316       return this.source;
317     },
319     poster: function() {
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)
322       if (_isOnMobile) {
323         return null;
324       }
326       return this.poster_;
327     },
329     setPoster: function(poster) {
330       this.poster_ = poster;
331     },
333     setSrc: function(source) {
334       if (!source || !source.src) {
335         return;
336       }
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();
350         }
351       }
353       if (this.options_.autoplay && !_isOnMobile) {
354         if (this.isReady_) {
355           this.play();
356         } else {
357           this.playOnReady = true;
358         }
359       } else if (this.activeVideoId !== this.url.videoId) {
360         if (this.isReady_) {
361           this.ytPlayer.cueVideoById(this.url.videoId);
362           this.activeVideoId = this.url.videoId;
363         } else {
364           this.cueOnReady = true;
365         }
366       }
367     },
369     autoplay: function() {
370       return this.options_.autoplay;
371     },
373     setAutoplay: function(val) {
374       this.options_.autoplay = val;
375     },
377     loop: function() {
378       return this.options_.loop;
379     },
381     setLoop: function(val) {
382       this.options_.loop = val;
383     },
385     play: function() {
386       if (!this.url || !this.url.videoId) {
387         return;
388       }
390       this.wasPausedBeforeSeek = false;
392       if (this.isReady_) {
393         if (this.url.listId) {
394           if (this.activeList === this.url.listId) {
395             this.ytPlayer.playVideo();
396           } else {
397             this.ytPlayer.loadPlaylist(this.url.listId);
398             this.activeList = this.url.listId;
399           }
400         }
402         if (this.activeVideoId === this.url.videoId) {
403           this.ytPlayer.playVideo();
404         } else {
405           this.ytPlayer.loadVideoById(this.url.videoId);
406           this.activeVideoId = this.url.videoId;
407         }
408       } else {
409         this.trigger('waiting');
410         this.playOnReady = true;
411       }
412     },
414     pause: function() {
415       if (this.ytPlayer) {
416         this.ytPlayer.pauseVideo();
417       }
418     },
420     paused: function() {
421       return (this.ytPlayer) ?
422         (this.lastState !== YT.PlayerState.PLAYING && this.lastState !== YT.PlayerState.BUFFERING)
423         : true;
424     },
426     currentTime: function() {
427       return this.ytPlayer ? this.ytPlayer.getCurrentTime() : 0;
428     },
430     setCurrentTime: function(seconds) {
431       if (this.lastState === YT.PlayerState.PAUSED) {
432         this.timeBeforeSeek = this.currentTime();
433       }
435       if (!this.isSeeking) {
436         this.wasPausedBeforeSeek = this.paused();
437       }
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');
455             this.onSeeked();
456           }
457         }.bind(this), 250);
458       }
459     },
461     seeking: function () {
462       return this.isSeeking;
463     },
465     seekable: function () {
466       if(!this.ytPlayer || !this.ytPlayer.getVideoLoadedFraction) {
467         return {
468           length: 0,
469           start: function() {
470             throw new Error('This TimeRanges object is empty');
471           },
472           end: function() {
473             throw new Error('This TimeRanges object is empty');
474           }
475         };
476       }
477       var end = this.ytPlayer.getDuration();
479       return {
480         length: this.ytPlayer.getDuration(),
481         start: function() { return 0; },
482         end: function() { return end; }
483       };
484     },
486     onSeeked: function() {
487       clearInterval(this.checkSeekedInPauseInterval);
488       this.isSeeking = false;
490       if (this.wasPausedBeforeSeek) {
491         this.pause();
492       }
494       this.trigger('seeked');
495     },
497     playbackRate: function() {
498       return this.ytPlayer ? this.ytPlayer.getPlaybackRate() : 1;
499     },
501     setPlaybackRate: function(suggestedRate) {
502       if (!this.ytPlayer) {
503         return;
504       }
506       this.ytPlayer.setPlaybackRate(suggestedRate);
507       this.trigger('ratechange');
508     },
510     duration: function() {
511       return this.ytPlayer ? this.ytPlayer.getDuration() : 0;
512     },
514     currentSrc: function() {
515       return this.source && this.source.src;
516     },
518     ended: function() {
519       return this.ytPlayer ? (this.lastState === YT.PlayerState.ENDED) : false;
520     },
522     volume: function() {
523       return this.ytPlayer ? this.ytPlayer.getVolume() / 100.0 : 1;
524     },
526     setVolume: function(percentAsDecimal) {
527       if (!this.ytPlayer) {
528         return;
529       }
531       this.ytPlayer.setVolume(percentAsDecimal * 100.0);
532       this.setTimeout( function(){
533         this.trigger('volumechange');
534       }, 50);
536     },
538     muted: function() {
539       return this.ytPlayer ? this.ytPlayer.isMuted() : false;
540     },
542     setMuted: function(mute) {
543       if (!this.ytPlayer) {
544         return;
545       }
546       else{
547         this.muted(true);
548       }
550       if (mute) {
551         this.ytPlayer.mute();
552       } else {
553         this.ytPlayer.unMute();
554       }
555       this.setTimeout( function(){
556         this.trigger('volumechange');
557       }, 50);
558     },
560     buffered: function() {
561       if(!this.ytPlayer || !this.ytPlayer.getVideoLoadedFraction) {
562         return {
563           length: 0,
564           start: function() {
565             throw new Error('This TimeRanges object is empty');
566           },
567           end: function() {
568             throw new Error('This TimeRanges object is empty');
569           }
570         };
571       }
573       var end = this.ytPlayer.getVideoLoadedFraction() * this.ytPlayer.getDuration();
575       return {
576         length: this.ytPlayer.getDuration(),
577         start: function() { return 0; },
578         end: function() { return end; }
579       };
580     },
582     // TODO: Can we really do something with this on YouTUbe?
583     preload: function() {},
584     load: function() {},
585     reset: function() {},
587     supportsFullScreen: function() {
588       return true;
589     },
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';
595       try {
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) {
601               return;
602             }
603           } else if(image.height <= 90 || image.width <= 120) {
604             return;
605           }
607           this.poster_ = uri;
608           this.trigger('posterchange');
609         }.bind(this);
610         image.onerror = function(){};
611         image.src = uri;
612       }
613       catch(e){}
614     }
615   });
617   Youtube.isSupported = function() {
618     return true;
619   };
621   Youtube.canPlaySource = function(e) {
622     return Youtube.canPlayType(e.type);
623   };
625   Youtube.canPlayType = function(e) {
626     return (e === 'video/youtube');
627   };
629   var _isOnMobile = videojs.browser.IS_IOS || useNativeControlsOnAndroid();
631   Youtube.parseUrl = function(url) {
632     var result = {
633       videoId: null
634     };
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];
641     }
643     var regPlaylist = /[?&]list=([^#\&\?]+)/;
644     match = url.match(regPlaylist);
646     if(match && match[1]) {
647       result.listId = match[1];
648     }
650     return result;
651   };
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();
659       }
660     });
661   }
663   function loadScript(src, callback) {
664     var loaded = false;
665     var tag = document.createElement('script');
666     var firstScriptTag = document.getElementsByTagName('script')[0];
667     firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
668     tag.onload = function () {
669       if (!loaded) {
670         loaded = true;
671         callback();
672       }
673     };
674     tag.onreadystatechange = function () {
675       if (!loaded && (this.readyState === 'complete' || this.readyState === 'loaded')) {
676         loaded = true;
677         callback();
678       }
679     };
680     tag.src = src;
681   }
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;
697     } else {
698       style.appendChild(document.createTextNode(css));
699     }
701     head.appendChild(style);
702   }
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
708     //user agent.
709     return videojs.browser.IS_ANDROID && videojs.browser.ANDROID_VERSION < 5 && stockRegex && stockRegex[2] > 0;
710   }
712   Youtube.apiReadyQueue = [];
714   loadScript('https://www.youtube.com/iframe_api', apiLoaded);
715   injectCss();
717   // Older versions of VJS5 doesn't have the registerTech function
718   if (typeof videojs.registerTech !== 'undefined') {
719     videojs.registerTech('Youtube', Youtube);
720   } else {
721     videojs.registerComponent('Youtube', Youtube);
722   }
723 }));