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