1/*! @license html5-youtube.js by Ginpei https://github.com/ginpei/html5-youtube.js MIT License */ 2 3/* global YT */ 4(function(window, document) { 5 /** 6 * The interface. 7 */ 8 window.youtube = function(options) { 9 return new Player(options); 10 }; 11 12 /** 13 * The constructor. 14 */ 15 var Player = window.youtube.Player = function(options) { 16 // define property's getters and setters if they have not yet 17 if (Player._undefinedProperties) { 18 Player._execDefineProperties(); 19 delete Player._undefinedProperties; 20 } 21 22 this.initialize(options); 23 }; 24 25 /** 26 * The definition of available playbackRate values on YouTube API. 27 * @see https://developers.google.com/youtube/js_api_reference#Playback_rate 28 */ 29 window.youtube.availablePlaybackRates = [0.25, 0.5, 1, 1.5, 2]; 30 31 // shortcut 32 var prototype = Player.prototype; 33 34 // ---------------------------------------------------------------- 35 // Statics 36 37 /** 38 * These values are used to detect states in onStateChange event. 39 * They are same as YouTube API's `YT.PlayerState`. 40 * @see https://developers.google.com/youtube/iframe_api_reference#onStateChange 41 */ 42 Player.PlayerState = { UNSTARTED:-1, ENDED:0, PLAYING:1, PAUSED:2, BUFFERING:3, CUED:5 }; 43 44 /** 45 * Proxy for `Function#bind`. 46 * It can be placed for compat. 47 * @param {Function} fn 48 * @returns {Function} 49 */ 50 Player.bind = function(fn) { 51 var args = Array.prototype.slice.call(arguments, 1); 52 return Function.prototype.bind.apply(fn, args); 53 }; 54 55 /** 56 * Execute property definig for stored values in `_undefinedProperties`. 57 * This method is called only once when the first instance is created. 58 */ 59 Player._execDefineProperties = function() { 60 var obj = this.prototype; 61 var properties = this._undefinedProperties; 62 for (var name in properties) { 63 this._execDefineProperty(obj, name, properties[name]); 64 } 65 }; 66 67 /** 68 * Proxy for `Object.defineProperty`. 69 * It can be placed for compat. 70 */ 71 Player._execDefineProperty = function(obj, prop, descriptor) { 72 Object.defineProperty(obj, prop, descriptor); 73 }; 74 75 /** 76 * Parse data attributes to number or string 77 * Examples: 78 * Player._parseDataAttribute('true') // 1 79 * Player._parseDataAttribute('0') // 0 80 * Player._parseDataAttribute('2EEsa_pqGAs') // '2EEsa_pqGAs' 81 * @param {String} the data-attribute content 82 * @returns {Number or String} 83 */ 84 Player._parseDataAttribute = function(string) { 85 var isNaN = function(val){ 86 // NaN is the only value to return false when compared to itself 87 return val !== val; 88 } 89 90 if (typeof(string) === 'string'){ 91 var toNum = Number(string); 92 if (!isNaN(toNum) && typeof toNum === 'number'){ 93 return Number(string); 94 } else if (string === 'true') { 95 return true; 96 } else if (string === 'false') { 97 return false; 98 } else { 99 return string; 100 } 101 } 102 } 103 104 /** 105 * Load YouTube API script. 106 * @param {Function} callback 107 */ 108 Player.prepareYTScript = function(callback) { 109 // Status is changed as: initial->loading->ready. 110 // * The callback will run later if initial 111 // * The callback is queued and will run if loading 112 // * The callback run immediately if ready 113 // 114 115 var status = this._ytStatus; 116 if (status === undefined) { // initial; not started 117 // initialize the callback queue 118 var callbacks = this._ytCallbacks = []; 119 callbacks.push(callback); 120 121 // load YouTube script 122 var url = 'https://www.youtube.com/iframe_api'; 123 var elScript = document.createElement('script'); 124 elScript.src = url; 125 document.body.appendChild(elScript); 126 127 // set callbacks 128 window.onYouTubeIframeAPIReady = Player.bind(function() { 129 callbacks.forEach(function(callback, index) { 130 callback(); 131 }); 132 delete this._ytCallbacks; 133 this._ytStatus = 2; 134 }, this); 135 136 // update status 137 this._ytStatus = 1; 138 } 139 else if (status === 1) { // loading; started but not loaded yet 140 this._ytCallbacks.push(callback); 141 } 142 else if (status === 2) { // ready; script is completely loaded 143 callback(); 144 } 145 }; 146 147 // ---------------------------------------------------------------- 148 // Constructing 149 150 /** 151 * Initialize the instance ownself. 152 * @param {Object} options 153 */ 154 prototype.initialize = function(options) { 155 if (!this.player) { 156 this._events = []; 157 this._resetProperties(); 158 159 this._initializeEventer(); 160 this._buildPlayer(options); 161 } 162 }; 163 164 /** 165 * Good bye! 166 */ 167 prototype.destroy = function() { 168 if (this.player) { 169 this._removeAllEventListeners(); 170 this._clearEventer(); 171 this._stopAllObservings();
172 this._resetProperties(); 173 this._destroyPlayer(); 174 } 175 }; 176 177 /** 178 * Load YouTube API and setup video UI. 179 * It can be placed for compat. 180 * @param {Object} options 181 */ 182 prototype._buildPlayer = function(options) { 183 Player.prepareYTScript(Player.bind(this._setupVideo, this, options)); 184 }; 185 186 /** 187 * @see #destroy 188 */ 189 prototype._destroyPlayer = function() { 190 this.player.destroy(); 191 this.player = null; 192 }; 193 194 /** 195 * YT.Player has add/removeEventListener methods but they doesn't work correctly 196 * It can be placed for compat. 197 */ 198 prototype._initializeEventer = function() { 199 this._eventer = document.createElement('ytapiplayer'); 200 document.body.appendChild(this._eventer); 201 }; 202 203 /** 204 * It can be placed for compat. 205 * @see #destroy 206 */ 207 prototype._clearEventer = function() { 208 document.body.removeChild(this._eventer); 209 this._eventer = null; 210 }; 211 212 /** 213 * Setup viode UI. 214 * @param {Object} options 215 */ 216 prototype._setupVideo = function(options) { 217 var videoOptions = this._getVideoOptions(options); 218 this.player = this._createPlayer(videoOptions.el, { 219 events: this._getVideoEvents(), 220 height: videoOptions.height, 221 playerVars: videoOptions.playerVars, 222 videoId: videoOptions.videoId, 223 width: videoOptions.width 224 }); 225 }; 226 227 prototype._getVideoOptions = function(options) { 228 var el = options && options.el; 229 if (!el || !el.getAttribute) { 230 throw new Error('`options.el` is require.'); 231 } 232 233 var ytPlayerVars = [ 234 'autohide', 235 'autoplay', 236 'cc_load_policy', 237 'color', 238 'controls', 239 'disablekb', 240 'enablejsapi', 241 'end', 242 'fs', 243 'hl', 244 'iv_load_policy', 245 'list', 246 'listType', 247 'loop', 248 'modestbranding', 249 'origin', 250 'playerapiid', 251 'playlist', 252 'playsinline', 253 'rel', 254 'showinfo', 255 'start', 256 'theme', 257 'vq' 258 ]; 259 260 var videoId = options.id || el.getAttribute('data-youtube-videoid'); 261 var playerVars = {}; 262 ytPlayerVars.forEach(function(propName){ 263 playerVars[propName] = this._getPlayerVarsOption(options, propName) 264 }.bind(this)) 265 266 var width; 267 var height = el.clientHeight; 268 if (height) { 269 width = el.clientWidth; 270 } 271 else { 272 height = 390; 273 width = 640; 274 } 275 276 return { 277 el: el, 278 height: height, 279 playerVars: playerVars, 280 videoId: videoId, 281 width: width 282 }; 283 }; 284 285 prototype._getPlayerVarsOption = function(options, name) { 286 var value; 287 288 if (options[name] == undefined) { // or null 289 var attribute = options.el.getAttribute('data-youtube-' + name); 290 value = Player._parseDataAttribute(attribute); 291 } 292 else { 293 value = options[name]; 294 } 295 296 if ((typeof value == 'number' && value >= 0) || typeof value == 'string') { 297 // OK, nothing to do 298 } 299 else if (typeof value == 'boolean') { 300 // Convert booleans to number 301 value = Number(value); 302 } 303 else { 304 // Let's set the value to nothing and let the youtube player fallback to defaults 305 value = undefined; 306 } 307 308 return value; 309 }; 310 311 prototype._getVideoEvents = function() { 312 var events = {}; 313 314 [ 315 'onApiChange', 316 'onError', 317 'onPlaybackQualityChange', 318 'onPlaybackRateChange', 319 'onReady', 320 'onStateChange', 321 ].forEach(function(type, index) { 322 events[type] = Player.bind(this[type], this); 323 }.bind(this)); 324 325 return events; 326 }; 327 328 prototype._createPlayer = function(el, options) { 329 return new YT.Player(el, options); 330 }; 331 332 // ---------------------------------------------------------------- 333 // Events 334 335 /** 336 * Attach an event handler function. 337 * @param {String} type A event type like `"play"`, '"timeupdate"` or `"onReady"`. 338 * @param {Function} listener A function to execute when the event is triggered. 339 * @see {#removeEventListener} 340 */ 341 prototype.addEventListener = function(type, listener) { 342 var binded = this._pushListener(type, listener); 343 this._eventer.addEventListener(type, binded); 344 }; 345 346 /** 347 * Dettach an event handler function. 348 * @param {String} type 349 * @param {Function} listener 350 * @see {#addEventListener} 351 */ 352 prototype.removeEventListener = function(type, listener) { 353 var data = this._popListener(type, listener); 354 if (data) { 355 this._eventer.removeEventListener(type, data.binded); 356 } 357 }; 358 359 /** 360 * A shortcut for `addEventListener` and returns `this`. 361 * You can use method chaining. 362 * @param {String} type 363 * @param {Function} listener 364 * @returns {Player} 365 */ 366 prototype.on = function(type, listener) { 367 this.addEventListener(type, listener); 368 return this; 369 }; 370 371 /** 372 * A shortcut for `removeEventListener` and returns `this`. 373 * You can use method chaining. 374 * @param {String} type 375 * @param {Function} listener 376 * @returns {Player} 377 */ 378 prototype.off = function(type, listener) { 379 this.removeEventListener(type, listener); 380 return this; 381 }; 382 383 /** 384 * Trigger an event.
385 * It can be placed for compat. 386 * @param {String} type A event type like `"play"`, '"timeupdate"` or `"onReady"`. 387 */ 388 prototype.trigger = function(type, originalEvent) { 389 var event = document.createEvent('CustomEvent'); 390 event.initEvent(type, false, true); 391 event.player = this; 392 393 if (originalEvent) { 394 event.playerData = originalEvent.data; 395 event.originalEvent = originalEvent; 396 } 397 398 this._eventer.dispatchEvent(event); 399 }; 400 401 /** 402 * @see #destroy 403 */ 404 prototype._removeAllEventListeners = function() { 405 var allEvents = this._events; 406 for (var type in allEvents) { 407 var events = allEvents[type]; 408 for (var i=0, l=events.length; i<l; i++) { 409 var data = events[i]; 410 if (data) { 411 this.removeEventListener(type, data.listener); 412 delete data.listener; 413 delete data.binded; 414 events[i] = null; 415 } 416 } 417 delete allEvents[type]; 418 } 419 }; 420 421 prototype._pushListener = function(type, listener) { 422 var binded = Player.bind(listener, this); 423 424 var events = this._events[type]; 425 if (!events) { 426 events = this._events[type] = []; 427 } 428 429 events.push({ 430 binded: binded, 431 listener: listener 432 }); 433 434 return binded; 435 }; 436 437 prototype._popListener = function(type, listener) { 438 var events = this._events[type]; 439 if (events) { 440 for (var i=0, l=events.length; i<l; i++) { 441 var data = events[i]; 442 if (data && data.listener === listener) { 443 events[i] = null; 444 return data; 445 } 446 } 447 } 448 return undefined; 449 }; 450 451 prototype.onApiChange = function(event) { 452 this.trigger('onApiChange', event); 453 }; 454 455 /** 456 * @param {Number} event.playerData The error ID. 457 * @see https://developers.google.com/youtube/iframe_api_reference#onError 458 */ 459 prototype.onError = function(event) { 460 this.trigger('onError', event); 461 this.trigger('error', event); 462 }; 463 464 prototype.onPlaybackQualityChange = function(event) { 465 this.trigger('onPlaybackQualityChange', event); 466 }; 467 468 prototype.onPlaybackRateChange = function(event) { 469 this.trigger('onPlaybackRateChange', event); 470 }; 471 472 prototype.onReady = function(event) { 473 this.trigger('onReady', event); 474 475 if (this._unsetVideoId) { 476 this.player.cueVideoById(this._unsetVideoId); 477 delete this._unsetVideoId; 478 } 479 480 this._updateMeta(); 481 this._observeTimeUpdate(); 482 this._observeVolume(); 483 this._observePlaybackRate(); 484 this._observeDuration(); 485 this.trigger('ready', event); 486 this.trigger('canplay', event); 487 this.trigger('canplaythrough'); 488 }; 489 490 prototype.onStateChange = function(event) { 491 this.trigger('onStateChange', event); 492 493 var state = event.data; 494 495 this.played = this.paused = this.ended = false; 496 497 if (state === Player.PlayerState.UNSTARTED) { 498 this.trigger('emptied', event); 499 } 500 else if (state === Player.PlayerState.ENDED) { 501 this.ended = true; 502 this.trigger('ended', event); 503 } 504 else if (state === Player.PlayerState.PLAYING) { 505 this.played = true; 506 this.trigger('play', event); 507 this.trigger('playing', event); 508 } 509 else if (state === Player.PlayerState.PAUSED) { 510 this.paused = true; 511 this.trigger('pause', event); 512 } 513 else if (state === Player.PlayerState.BUFFERING) { 514 this.trigger('buffer', event); 515 } 516 else if (state === Player.PlayerState.CUED) { 517 this._updateMeta(); 518 this.trigger('canplay'); 519 this.trigger('canplaythrough'); 520 } 521 }; 522 523 // ---------------------------------------------------------------- 524 // Manip 525 526 /** 527 * Play the video. 528 */ 529 prototype.play = function() { 530 if (this.player) { 531 this.player.playVideo(); 532 } 533 }; 534 535 /** 536 * Stop the video. 537 */ 538 prototype.pause = function() { 539 if (this.player) { 540 this.player.pauseVideo(); 541 } 542 }; 543 544 prototype.getIframe = function() { 545 if (this.player) { 546 return this.player.getIframe(); 547 } 548 }; 549 550 /** 551 * This function returns the set of playback rates in which the current video is available. The default value is 1, which indicates that the video is playing in normal speed. 552 * 553 * The function returns an array of numbers ordered from slowest to fastest playback speed. Even if the player does not support variable playback speeds, the array should always contain at least one value (1). 554 * @returns {Array} 555 * @see https://developers.google.com/youtube/iframe_api_reference#getAvailablePlaybackRates 556 */ 557 prototype.getAvailablePlaybackRates = function() { 558 if (this.player) { 559 return this.player.getAvailablePlaybackRates();
560 } 561 else { 562 return undefined; 563 } 564 }; 565 566 // ---------------------------------------------------------------- 567 // Properties 568 569 prototype._updateMeta = function() { 570 this._src = this.currentSrc = this.player.getVideoUrl(); 571 }; 572 573 /** 574 * Start observing timeupdate's change. 575 */ 576 prototype._observeTimeUpdate = function() { 577 this._tmTimeUpdate = setInterval(Player.bind(function() { 578 var time = this.player.getCurrentTime(); 579 if (time !== this._currentTime) { 580 this._currentTime = time; 581 this.trigger('timeupdate'); 582 } 583 }, this), 100); 584 }; 585 586 /** 587 * Start observing volume's change. 588 */ 589 prototype._observeVolume = function() { 590 this._tmVolume = setInterval(Player.bind(function() { 591 var muted = this.player.isMuted(); 592 var volume = this.player.getVolume(); 593 if (muted !== this._muted || volume !== this._volume) { 594 this._muted = muted; 595 this._volume = volume; 596 this.trigger('volumechange'); 597 } 598 }, this), 100); 599 }; 600 601 /** 602 * Start observing playbackRate's change. 603 */ 604 prototype._observePlaybackRate = function() { 605 this._tmPlaybackRate = setInterval(Player.bind(function() { 606 var playbackRate = this.player.getPlaybackRate(); 607 if (playbackRate !== this._playbackRate) { 608 this._playbackRate = playbackRate; 609 this.trigger('ratechange'); 610 } 611 }, this), 100); 612 }; 613 614 /** 615 * Start observing duration's change. 616 */ 617 prototype._observeDuration = function() { 618 this._tmDuration = setInterval(Player.bind(function() { 619 var duration = this.player.getDuration() || 0; 620 if (duration !== this.duration) { 621 this.duration = duration; 622 this.trigger('durationchange'); 623 } 624 }, this), 100); 625 }; 626 627 /** 628 * @see #destroy 629 */ 630 prototype._stopAllObservings = function() { 631 clearInterval(this._tmTimeUpdate); 632 clearInterval(this._tmVolume); 633 clearInterval(this._tmPlaybackRate); 634 clearInterval(this._tmDuration); 635 }; 636 637 prototype._resetProperties = function() { 638 this._currentTime = null; 639 this._volume = null; 640 this._muted = null; 641 this._playbackRate = null; 642 this._src = null; 643 this.duration = null; 644 this.currentSrc = null; 645 this.played = null; 646 this.paused = null; 647 this.ended = null; 648 }; 649 650 /** 651 * Definitions are stored here. 652 * @type Array 653 */ 654 Player._undefinedProperties = { 655 /** 656 * Returns the current playback position, in seconds, as a position between zero time and the current duration. 657 * Can be set, to seek to the given time. 658 * @type number 659 */ 660 currentTime: { 661 get: function() { 662 return this._currentTime; 663 }, 664 set: function(value) { 665 if (this.player) { 666 this.player.seekTo(value, true); 667 } 668 } 669 }, 670 671 /** 672 * Returns the current playback volume multiplier, as a number in the range 0.0 to 1.0, where 0.0 is the quietest and 1.0 the loudest. 673 * Can be set, to change the volume multiplier. 674 * @type number 675 */ 676 volume: { 677 get: function() { 678 return this._volume / 100; 679 }, 680 set: function(value) { 681 if (this.player) { 682 this.player.setVolume(value * 100); 683 } 684 } 685 }, 686 687 /** 688 * Returns true if all audio is muted (regardless of other attributes either on the controller or on any media elements slaved to this controller), and false otherwise. 689 * Can be set, to change whether the audio is muted or not. 690 * @type number 691 */ 692 muted: { 693 get: function() { 694 return this._muted; 695 }, 696 set: function(value) { 697 if (this.player) { 698 this.player[value?'mute':'unMute'](); 699 } 700 } 701 }, 702 703 /** 704 * Returns the default rate of playback, for when the user is not fast-forwarding or reversing through the media resource. 705 * Can be set, to change the default rate of playback. 706 * @type number 707 */ 708 playbackRate: { 709 get: function() { 710 return this._playbackRate; 711 }, 712 set: function(value) { 713 if (this.player) { 714 this.player.setPlaybackRate(value); 715 } 716 } 717 }, 718 719 /** 720 * Returns the address of the current media resource. 721 * Can be set, to change the video URL. 722 * @type number 723 */ 724 src: { 725 get: function() { 726 return this._src; 727 },
728 set: function(value) { 729 if (this.player) { 730 this.player.cueVideoById(value); 731 } 732 else { 733 this._unsetVideoId = value; 734 } 735 } 736 } 737 }; 738})(window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.