UNPKG

22.2 kBJavaScriptView Raw
1/* The MIT License (MIT)
2
3Copyright (c) 2014-2015 Benoit Tremblay <trembl.ben@gmail.com>
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in
13all copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21THE 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(['videojs'], 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';
35
36 var _isOnMobile = videojs.browser.IS_IOS || videojs.browser.IS_NATIVE_ANDROID;
37 var Tech = videojs.getTech('Tech');
38
39 var Youtube = videojs.extend(Tech, {
40
41 constructor: function(options, ready) {
42 Tech.call(this, options, ready);
43
44 this.setPoster(options.poster);
45 this.setSrc(this.options_.source, true);
46
47 // Set the vjs-youtube class to the player
48 // Parent is not set yet so we have to wait a tick
49 this.setTimeout(function() {
50 if (this.el_) {
51 this.el_.parentNode.className += ' vjs-youtube';
52
53 if (_isOnMobile) {
54 this.el_.parentNode.className += ' vjs-youtube-mobile';
55 }
56
57 if (Youtube.isApiReady) {
58 this.initYTPlayer();
59 } else {
60 Youtube.apiReadyQueue.push(this);
61 }
62 }
63 }.bind(this));
64 },
65
66 dispose: function() {
67 if (this.ytPlayer) {
68 //Dispose of the YouTube Player
69 if (this.ytPlayer.stopVideo) {
70 this.ytPlayer.stopVideo();
71 }
72 if (this.ytPlayer.destroy) {
73 this.ytPlayer.destroy();
74 }
75 } else {
76 //YouTube API hasn't finished loading or the player is already disposed
77 var index = Youtube.apiReadyQueue.indexOf(this);
78 if (index !== -1) {
79 Youtube.apiReadyQueue.splice(index, 1);
80 }
81 }
82 this.ytPlayer = null;
83
84 this.el_.parentNode.className = this.el_.parentNode.className
85 .replace(' vjs-youtube', '')
86 .replace(' vjs-youtube-mobile', '');
87 this.el_.parentNode.removeChild(this.el_);
88
89 //Needs to be called after the YouTube player is destroyed, otherwise there will be a null reference exception
90 Tech.prototype.dispose.call(this);
91 },
92
93 createEl: function() {
94 var div = document.createElement('div');
95 div.setAttribute('id', this.options_.techId);
96 div.setAttribute('style', 'width:100%;height:100%;top:0;left:0;position:absolute');
97 div.setAttribute('class', 'vjs-tech');
98
99 var divWrapper = document.createElement('div');
100 divWrapper.appendChild(div);
101
102 if (!_isOnMobile && !this.options_.ytControls) {
103 var divBlocker = document.createElement('div');
104 divBlocker.setAttribute('class', 'vjs-iframe-blocker');
105 divBlocker.setAttribute('style', 'position:absolute;top:0;left:0;width:100%;height:100%');
106
107 // In case the blocker is still there and we want to pause
108 divBlocker.onclick = function() {
109 this.pause();
110 }.bind(this);
111
112 divWrapper.appendChild(divBlocker);
113 }
114
115 return divWrapper;
116 },
117
118 initYTPlayer: function() {
119 var playerVars = {
120 controls: 0,
121 modestbranding: 1,
122 rel: 0,
123 showinfo: 0,
124 loop: this.options_.loop ? 1 : 0
125 };
126
127 // Let the user set any YouTube parameter
128 // https://developers.google.com/youtube/player_parameters?playerVersion=HTML5#Parameters
129 // To use YouTube controls, you must use ytControls instead
130 // To use the loop or autoplay, use the video.js settings
131
132 if (typeof this.options_.autohide !== 'undefined') {
133 playerVars.autohide = this.options_.autohide;
134 }
135
136 if (typeof this.options_['cc_load_policy'] !== 'undefined') {
137 playerVars['cc_load_policy'] = this.options_['cc_load_policy'];
138 }
139
140 if (typeof this.options_.ytControls !== 'undefined') {
141 playerVars.controls = this.options_.ytControls;
142 }
143
144 if (typeof this.options_.disablekb !== 'undefined') {
145 playerVars.disablekb = this.options_.disablekb;
146 }
147
148 if (typeof this.options_.end !== 'undefined') {
149 playerVars.end = this.options_.end;
150 }
151
152 if (typeof this.options_.color !== 'undefined') {
153 playerVars.color = this.options_.color;
154 }
155
156 if (!playerVars.controls) {
157 // Let video.js handle the fullscreen unless it is the YouTube native controls
158 playerVars.fs = 0;
159 } else if (typeof this.options_.fs !== 'undefined') {
160 playerVars.fs = this.options_.fs;
161 }
162
163 if (typeof this.options_.end !== 'undefined') {
164 playerVars.end = this.options_.end;
165 }
166
167 if (typeof this.options_.hl !== 'undefined') {
168 playerVars.hl = this.options_.hl;
169 } else if (typeof this.options_.language !== 'undefined') {
170 // Set the YouTube player on the same language than video.js
171 playerVars.hl = this.options_.language.substr(0, 2);
172 }
173
174 if (typeof this.options_['iv_load_policy'] !== 'undefined') {
175 playerVars['iv_load_policy'] = this.options_['iv_load_policy'];
176 }
177
178 if (typeof this.options_.list !== 'undefined') {
179 playerVars.list = this.options_.list;
180 } else if (this.url && typeof this.url.listId !== 'undefined') {
181 playerVars.list = this.url.listId;
182 }
183
184 if (typeof this.options_.listType !== 'undefined') {
185 playerVars.listType = this.options_.listType;
186 }
187
188 if (typeof this.options_.modestbranding !== 'undefined') {
189 playerVars.modestbranding = this.options_.modestbranding;
190 }
191
192 if (typeof this.options_.playlist !== 'undefined') {
193 playerVars.playlist = this.options_.playlist;
194 }
195
196 if (typeof this.options_.playsinline !== 'undefined') {
197 playerVars.playsinline = this.options_.playsinline;
198 }
199
200 if (typeof this.options_.rel !== 'undefined') {
201 playerVars.rel = this.options_.rel;
202 }
203
204 if (typeof this.options_.showinfo !== 'undefined') {
205 playerVars.showinfo = this.options_.showinfo;
206 }
207
208 if (typeof this.options_.start !== 'undefined') {
209 playerVars.start = this.options_.start;
210 }
211
212 if (typeof this.options_.theme !== 'undefined') {
213 playerVars.theme = this.options_.theme;
214 }
215
216 // Allow undocumented options to be passed along via customVars
217 if (typeof this.options_.customVars !== 'undefined') {
218 var customVars = this.options_.customVars;
219 Object.keys(customVars).forEach(function(key) {
220 playerVars[key] = customVars[key];
221 });
222 }
223
224 this.activeVideoId = this.url ? this.url.videoId : null;
225 this.activeList = playerVars.list;
226
227 var playerConfig = {
228 videoId: this.activeVideoId,
229 playerVars: playerVars,
230 events: {
231 onReady: this.onPlayerReady.bind(this),
232 onPlaybackQualityChange: this.onPlayerPlaybackQualityChange.bind(this),
233 onPlaybackRateChange: this.onPlayerPlaybackRateChange.bind(this),
234 onStateChange: this.onPlayerStateChange.bind(this),
235 onVolumeChange: this.onPlayerVolumeChange.bind(this),
236 onError: this.onPlayerError.bind(this)
237 }
238 };
239
240 if (typeof this.options_.enablePrivacyEnhancedMode !== 'undefined' && this.options_.enablePrivacyEnhancedMode) {
241 playerConfig.host = 'https://www.youtube-nocookie.com';
242 }
243
244 this.ytPlayer = new YT.Player(this.options_.techId, playerConfig);
245 },
246
247 onPlayerReady: function() {
248 if (this.options_.muted) {
249 this.ytPlayer.mute();
250 }
251
252 var playbackRates = this.ytPlayer.getAvailablePlaybackRates();
253 if (playbackRates.length > 1) {
254 this.featuresPlaybackRate = true;
255 }
256
257 this.playerReady_ = true;
258 this.triggerReady();
259
260 if (this.playOnReady) {
261 this.play();
262 } else if (this.cueOnReady) {
263 this.cueVideoById_(this.url.videoId);
264 this.activeVideoId = this.url.videoId;
265 }
266 },
267
268 onPlayerPlaybackQualityChange: function() {
269
270 },
271
272 onPlayerPlaybackRateChange: function() {
273 this.trigger('ratechange');
274 },
275
276 onPlayerStateChange: function(e) {
277 var state = e.data;
278
279 if (state === this.lastState || this.errorNumber) {
280 return;
281 }
282
283 this.lastState = state;
284
285 switch (state) {
286 case -1:
287 this.trigger('loadstart');
288 this.trigger('loadedmetadata');
289 this.trigger('durationchange');
290 this.trigger('ratechange');
291 break;
292
293 case YT.PlayerState.ENDED:
294 this.trigger('ended');
295 break;
296
297 case YT.PlayerState.PLAYING:
298 this.trigger('timeupdate');
299 this.trigger('durationchange');
300 this.trigger('playing');
301 this.trigger('play');
302
303 if (this.isSeeking) {
304 this.onSeeked();
305 }
306 break;
307
308 case YT.PlayerState.PAUSED:
309 this.trigger('canplay');
310 if (this.isSeeking) {
311 this.onSeeked();
312 } else {
313 this.trigger('pause');
314 }
315 break;
316
317 case YT.PlayerState.BUFFERING:
318 this.player_.trigger('timeupdate');
319 this.player_.trigger('waiting');
320 break;
321 }
322 },
323
324 onPlayerVolumeChange: function() {
325 this.trigger('volumechange');
326 },
327
328 onPlayerError: function(e) {
329 this.errorNumber = e.data;
330 this.trigger('pause');
331 this.trigger('error');
332 },
333
334 error: function() {
335 var code = 1000 + this.errorNumber; // as smaller codes are reserved
336 switch (this.errorNumber) {
337 case 5:
338 return { code: code, message: 'Error while trying to play the video' };
339
340 case 2:
341 case 100:
342 return { code: code, message: 'Unable to find the video' };
343
344 case 101:
345 case 150:
346 return {
347 code: code,
348 message: 'Playback on other Websites has been disabled by the video owner.'
349 };
350 }
351
352 return { code: code, message: 'YouTube unknown error (' + this.errorNumber + ')' };
353 },
354
355 loadVideoById_: function(id) {
356 var options = {
357 videoId: id
358 };
359 if (this.options_.start) {
360 options.startSeconds = this.options_.start;
361 }
362 if (this.options_.end) {
363 options.endEnd = this.options_.end;
364 }
365 this.ytPlayer.loadVideoById(options);
366 },
367
368 cueVideoById_: function(id) {
369 var options = {
370 videoId: id
371 };
372 if (this.options_.start) {
373 options.startSeconds = this.options_.start;
374 }
375 if (this.options_.end) {
376 options.endEnd = this.options_.end;
377 }
378 this.ytPlayer.cueVideoById(options);
379 },
380
381 src: function(src) {
382 if (src) {
383 this.setSrc({ src: src });
384 }
385
386 return this.source;
387 },
388
389 poster: function() {
390 // You can't start programmaticlly a video with a mobile
391 // through the iframe so we hide the poster and the play button (with CSS)
392 if (_isOnMobile) {
393 return null;
394 }
395
396 return this.poster_;
397 },
398
399 setPoster: function(poster) {
400 this.poster_ = poster;
401 },
402
403 setSrc: function(source) {
404 if (!source || !source.src) {
405 return;
406 }
407
408 delete this.errorNumber;
409 this.source = source;
410 this.url = Youtube.parseUrl(source.src);
411
412 if (!this.options_.poster) {
413 if (this.url.videoId) {
414 // Set the low resolution first
415 this.poster_ = 'https://img.youtube.com/vi/' + this.url.videoId + '/0.jpg';
416 this.trigger('posterchange');
417
418 // Check if their is a high res
419 this.checkHighResPoster();
420 }
421 }
422
423 if (this.options_.autoplay && !_isOnMobile) {
424 if (this.isReady_) {
425 this.play();
426 } else {
427 this.playOnReady = true;
428 }
429 } else if (this.activeVideoId !== this.url.videoId) {
430 if (this.isReady_) {
431 this.cueVideoById_(this.url.videoId);
432 this.activeVideoId = this.url.videoId;
433 } else {
434 this.cueOnReady = true;
435 }
436 }
437 },
438
439 autoplay: function() {
440 return this.options_.autoplay;
441 },
442
443 setAutoplay: function(val) {
444 this.options_.autoplay = val;
445 },
446
447 loop: function() {
448 return this.options_.loop;
449 },
450
451 setLoop: function(val) {
452 this.options_.loop = val;
453 },
454
455 play: function() {
456 if (!this.url || !this.url.videoId) {
457 return;
458 }
459
460 this.wasPausedBeforeSeek = false;
461
462 if (this.isReady_) {
463 if (this.url.listId) {
464 if (this.activeList === this.url.listId) {
465 this.ytPlayer.playVideo();
466 } else {
467 this.ytPlayer.loadPlaylist(this.url.listId);
468 this.activeList = this.url.listId;
469 }
470 }
471
472 if (this.activeVideoId === this.url.videoId) {
473 this.ytPlayer.playVideo();
474 } else {
475 this.loadVideoById_(this.url.videoId);
476 this.activeVideoId = this.url.videoId;
477 }
478 } else {
479 this.trigger('waiting');
480 this.playOnReady = true;
481 }
482 },
483
484 pause: function() {
485 if (this.ytPlayer) {
486 this.ytPlayer.pauseVideo();
487 }
488 },
489
490 paused: function() {
491 return (this.ytPlayer) ?
492 (this.lastState !== YT.PlayerState.PLAYING && this.lastState !== YT.PlayerState.BUFFERING)
493 : true;
494 },
495
496 currentTime: function() {
497 return this.ytPlayer ? this.ytPlayer.getCurrentTime() : 0;
498 },
499
500 setCurrentTime: function(seconds) {
501 if (this.lastState === YT.PlayerState.PAUSED) {
502 this.timeBeforeSeek = this.currentTime();
503 }
504
505 if (!this.isSeeking) {
506 this.wasPausedBeforeSeek = this.paused();
507 }
508
509 this.ytPlayer.seekTo(seconds, true);
510 this.trigger('timeupdate');
511 this.trigger('seeking');
512 this.isSeeking = true;
513
514 // A seek event during pause does not return an event to trigger a seeked event,
515 // so run an interval timer to look for the currentTime to change
516 if (this.lastState === YT.PlayerState.PAUSED && this.timeBeforeSeek !== seconds) {
517 clearInterval(this.checkSeekedInPauseInterval);
518 this.checkSeekedInPauseInterval = setInterval(function() {
519 if (this.lastState !== YT.PlayerState.PAUSED || !this.isSeeking) {
520 // If something changed while we were waiting for the currentTime to change,
521 // clear the interval timer
522 clearInterval(this.checkSeekedInPauseInterval);
523 } else if (this.currentTime() !== this.timeBeforeSeek) {
524 this.trigger('timeupdate');
525 this.onSeeked();
526 }
527 }.bind(this), 250);
528 }
529 },
530
531 seeking: function () {
532 return this.isSeeking;
533 },
534
535 seekable: function () {
536 if(!this.ytPlayer) {
537 return videojs.createTimeRange();
538 }
539
540 return videojs.createTimeRange(0, this.ytPlayer.getDuration());
541 },
542
543 onSeeked: function() {
544 clearInterval(this.checkSeekedInPauseInterval);
545 this.isSeeking = false;
546
547 if (this.wasPausedBeforeSeek) {
548 this.pause();
549 }
550
551 this.trigger('seeked');
552 },
553
554 playbackRate: function() {
555 return this.ytPlayer ? this.ytPlayer.getPlaybackRate() : 1;
556 },
557
558 setPlaybackRate: function(suggestedRate) {
559 if (!this.ytPlayer) {
560 return;
561 }
562
563 this.ytPlayer.setPlaybackRate(suggestedRate);
564 },
565
566 duration: function() {
567 return this.ytPlayer ? this.ytPlayer.getDuration() : 0;
568 },
569
570 currentSrc: function() {
571 return this.source && this.source.src;
572 },
573
574 ended: function() {
575 return this.ytPlayer ? (this.lastState === YT.PlayerState.ENDED) : false;
576 },
577
578 volume: function() {
579 return this.ytPlayer ? this.ytPlayer.getVolume() / 100.0 : 1;
580 },
581
582 setVolume: function(percentAsDecimal) {
583 if (!this.ytPlayer) {
584 return;
585 }
586
587 this.ytPlayer.setVolume(percentAsDecimal * 100.0);
588 },
589
590 muted: function() {
591 return this.ytPlayer ? this.ytPlayer.isMuted() : false;
592 },
593
594 setMuted: function(mute) {
595 if (!this.ytPlayer) {
596 return;
597 }
598 else{
599 this.muted(true);
600 }
601
602 if (mute) {
603 this.ytPlayer.mute();
604 } else {
605 this.ytPlayer.unMute();
606 }
607 this.setTimeout( function(){
608 this.trigger('volumechange');
609 }, 50);
610 },
611
612 buffered: function() {
613 if(!this.ytPlayer || !this.ytPlayer.getVideoLoadedFraction) {
614 return videojs.createTimeRange();
615 }
616
617 var bufferedEnd = this.ytPlayer.getVideoLoadedFraction() * this.ytPlayer.getDuration();
618
619 return videojs.createTimeRange(0, bufferedEnd);
620 },
621
622 // TODO: Can we really do something with this on YouTUbe?
623 preload: function() {},
624 load: function() {},
625 reset: function() {},
626 networkState: function () {
627 if (!this.ytPlayer) {
628 return 0; //NETWORK_EMPTY
629 }
630 switch (this.ytPlayer.getPlayerState()) {
631 case -1: //unstarted
632 return 0; //NETWORK_EMPTY
633 case 3: //buffering
634 return 2; //NETWORK_LOADING
635 default:
636 return 1; //NETWORK_IDLE
637 }
638 },
639 readyState: function () {
640 if (!this.ytPlayer) {
641 return 0; //HAVE_NOTHING
642 }
643 switch (this.ytPlayer.getPlayerState()) {
644 case -1: //unstarted
645 return 0; //HAVE_NOTHING
646 case 5: //video cued
647 return 1; //HAVE_METADATA
648 case 3: //buffering
649 return 2; //HAVE_CURRENT_DATA
650 default:
651 return 4; //HAVE_ENOUGH_DATA
652 }
653 },
654
655 supportsFullScreen: function() {
656 return true;
657 },
658
659 // Tries to get the highest resolution thumbnail available for the video
660 checkHighResPoster: function(){
661 var uri = 'https://img.youtube.com/vi/' + this.url.videoId + '/maxresdefault.jpg';
662
663 try {
664 var image = new Image();
665 image.onload = function(){
666 // Onload may still be called if YouTube returns the 120x90 error thumbnail
667 if('naturalHeight' in image){
668 if (image.naturalHeight <= 90 || image.naturalWidth <= 120) {
669 return;
670 }
671 } else if(image.height <= 90 || image.width <= 120) {
672 return;
673 }
674
675 this.poster_ = uri;
676 this.trigger('posterchange');
677 }.bind(this);
678 image.onerror = function(){};
679 image.src = uri;
680 }
681 catch(e){}
682 }
683 });
684
685 Youtube.isSupported = function() {
686 return true;
687 };
688
689 Youtube.canPlaySource = function(e) {
690 return Youtube.canPlayType(e.type);
691 };
692
693 Youtube.canPlayType = function(e) {
694 return (e === 'video/youtube');
695 };
696
697 Youtube.parseUrl = function(url) {
698 var result = {
699 videoId: null
700 };
701
702 var regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
703 var match = url.match(regex);
704
705 if (match && match[2].length === 11) {
706 result.videoId = match[2];
707 }
708
709 var regPlaylist = /[?&]list=([^#\&\?]+)/;
710 match = url.match(regPlaylist);
711
712 if(match && match[1]) {
713 result.listId = match[1];
714 }
715
716 return result;
717 };
718
719 function apiLoaded() {
720 YT.ready(function() {
721 Youtube.isApiReady = true;
722
723 for (var i = 0; i < Youtube.apiReadyQueue.length; ++i) {
724 Youtube.apiReadyQueue[i].initYTPlayer();
725 }
726 });
727 }
728
729 function loadScript(src, callback) {
730 var loaded = false;
731 var tag = document.createElement('script');
732 var firstScriptTag = document.getElementsByTagName('script')[0];
733 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
734 tag.onload = function () {
735 if (!loaded) {
736 loaded = true;
737 callback();
738 }
739 };
740 tag.onreadystatechange = function () {
741 if (!loaded && (this.readyState === 'complete' || this.readyState === 'loaded')) {
742 loaded = true;
743 callback();
744 }
745 };
746 tag.src = src;
747 }
748
749 function injectCss() {
750 var css = // iframe blocker to catch mouse events
751 '.vjs-youtube .vjs-iframe-blocker { display: none; }' +
752 '.vjs-youtube.vjs-user-inactive .vjs-iframe-blocker { display: block; }' +
753 '.vjs-youtube .vjs-poster { background-size: cover; }' +
754 '.vjs-youtube-mobile .vjs-big-play-button { display: none; }';
755
756 var head = document.head || document.getElementsByTagName('head')[0];
757
758 var style = document.createElement('style');
759 style.type = 'text/css';
760
761 if (style.styleSheet){
762 style.styleSheet.cssText = css;
763 } else {
764 style.appendChild(document.createTextNode(css));
765 }
766
767 head.appendChild(style);
768 }
769
770 Youtube.apiReadyQueue = [];
771
772 if (typeof document !== 'undefined'){
773 loadScript('https://www.youtube.com/iframe_api', apiLoaded);
774 injectCss();
775 }
776
777 // Older versions of VJS5 doesn't have the registerTech function
778 if (typeof videojs.registerTech !== 'undefined') {
779 videojs.registerTech('Youtube', Youtube);
780 } else {
781 videojs.registerComponent('Youtube', Youtube);
782 }
783}));