UNPKG

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