// ========================================================================== // YouTube plugin // ========================================================================== import utils from './../utils'; import controls from './../controls'; import ui from './../ui'; const youtube = { setup() { const videoId = utils.parseYouTubeId(this.embedId); // Remove old containers const containers = utils.getElements.call(this, `[id^="${this.type}-"]`); Array.from(containers).forEach(utils.removeElement); // Add embed class for responsive utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); // Set aspect ratio youtube.setAspectRatio.call(this); // Set ID this.media.setAttribute('id', utils.generateId(this.type)); // Setup API if (utils.is.object(window.YT)) { youtube.ready.call(this, videoId); } else { // Load the API utils.loadScript(this.config.urls.youtube.api); // Setup callback for the API window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || []; // Add to queue window.onYouTubeReadyCallbacks.push(() => { youtube.ready.call(this, videoId); }); // Set callback to process queue window.onYouTubeIframeAPIReady = () => { window.onYouTubeReadyCallbacks.forEach(callback => { callback(); }); }; } }, // Set aspect ratio setAspectRatio() { const ratio = this.config.ratio.split(':'); this.elements.wrapper.style.paddingBottom = `${100 / ratio[0] * ratio[1]}%`; }, // API ready ready(videoId) { const player = this; // Setup instance // https://developers.google.com/youtube/iframe_api_reference player.embed = new window.YT.Player(player.media.id, { videoId, playerVars: { autoplay: player.config.autoplay ? 1 : 0, // Autoplay controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported rel: 0, // No related vids showinfo: 0, // Hide info iv_load_policy: 3, // Hide annotations modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused) disablekb: 1, // Disable keyboard as we handle it playsinline: 1, // Allow iOS inline playback // Tracking for stats origin: window && window.location.hostname, widget_referrer: window && window.location.href, // Captions are flaky on YouTube cc_load_policy: (this.captions.active ? 1 : 0), cc_lang_pref: this.config.captions.language, }, events: { onError(event) { utils.dispatchEvent.call(player, player.media, 'error', true, { code: event.data, embed: event.target, }); }, onPlaybackQualityChange(event) { // Get the instance const instance = event.target; // Get current quality player.media.quality = instance.getPlaybackQuality(); utils.dispatchEvent.call(player, player.media, 'qualitychange'); }, onPlaybackRateChange(event) { // Get the instance const instance = event.target; // Get current speed player.media.playbackRate = instance.getPlaybackRate(); utils.dispatchEvent.call(player, player.media, 'ratechange'); }, onReady(event) { // Get the instance const instance = event.target; // Create a faux HTML5 API using the YouTube API player.media.play = () => { instance.playVideo(); player.media.paused = false; }; player.media.pause = () => { instance.pauseVideo(); player.media.paused = true; }; player.media.stop = () => { instance.stopVideo(); player.media.paused = true; }; player.media.duration = instance.getDuration(); player.media.paused = true; player.media.muted = instance.isMuted(); player.media.currentTime = 0; // Seeking Object.defineProperty(player.media, 'currentTime', { get() { return Number(instance.getCurrentTime()); }, set(time) { // Set seeking flag player.media.seeking = true; // Trigger seeking utils.dispatchEvent.call(player, player.media, 'seeking'); // Seek after events sent instance.seekTo(time); }, }); // Playback speed Object.defineProperty(player.media, 'playbackRate', { get() { return instance.getPlaybackRate(); }, set(input) { instance.setPlaybackRate(input); }, }); // Volume let volume = instance.getVolume() / 100; Object.defineProperty(player.media, 'volume', { get() { return volume; }, set(input) { volume = input; instance.setVolume(volume * 100); utils.dispatchEvent.call(player, player.media, 'volumechange'); }, }); // Muted Object.defineProperty(player.media, 'muted', { get() { return instance.isMuted(); }, set(input) { const toggle = utils.is.boolean(input) ? input : false; instance[toggle ? 'mute' : 'unMute'](); utils.dispatchEvent.call(player, player.media, 'volumechange'); }, }); // Source Object.defineProperty(player.media, 'currentSrc', { get() { return instance.getVideoUrl(); }, }); // Get available speeds if (player.config.controls.includes('settings') && player.config.settings.includes('speed')) { controls.setSpeedMenu.call(player, instance.getAvailablePlaybackRates()); } // Set title player.config.title = instance.getVideoData().title; // Set the tabindex to avoid focus entering iframe if (player.supported.ui) { player.media.setAttribute('tabindex', -1); } utils.dispatchEvent.call(player, player.media, 'timeupdate'); utils.dispatchEvent.call(player, player.media, 'durationchange'); // Reset timer window.clearInterval(player.timers.buffering); // Setup buffering player.timers.buffering = window.setInterval(() => { // Get loaded % from YouTube player.media.buffered = instance.getVideoLoadedFraction(); // Trigger progress only when we actually buffer something if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) { utils.dispatchEvent.call(player, player.media, 'progress'); } // Set last buffer point player.media.lastBuffered = player.media.buffered; // Bail if we're at 100% if (player.media.buffered === 1) { window.clearInterval(player.timers.buffering); // Trigger event utils.dispatchEvent.call(player, player.media, 'canplaythrough'); } }, 200); // Rebuild UI window.setTimeout(() => ui.build.call(player), 50); }, onStateChange(event) { // Get the instance const instance = event.target; // Reset timer window.clearInterval(player.timers.playing); // Handle events // -1 Unstarted // 0 Ended // 1 Playing // 2 Paused // 3 Buffering // 5 Video cued switch (event.data) { case 0: // YouTube doesn't support loop for a single video, so mimick it. if (player.config.loop.active) { // YouTube needs a call to `stopVideo` before playing again instance.stopVideo(); instance.playVideo(); break; } player.media.paused = true; utils.dispatchEvent.call(player, player.media, 'ended'); break; case 1: player.media.paused = false; // If we were seeking, fire seeked event if (player.media.seeking) { utils.dispatchEvent.call(player, player.media, 'seeked'); } player.media.seeking = false; utils.dispatchEvent.call(player, player.media, 'play'); utils.dispatchEvent.call(player, player.media, 'playing'); // Poll to get playback progress player.timers.playing = window.setInterval(() => { utils.dispatchEvent.call(player, player.media, 'timeupdate'); }, 50); // Check duration again due to YouTube bug // https://github.com/sampotts/plyr/issues/374 // https://code.google.com/p/gdata-issues/issues/detail?id=8690 if (player.media.duration !== instance.getDuration()) { player.media.duration = instance.getDuration(); utils.dispatchEvent.call(player, player.media, 'durationchange'); } // Get quality controls.setQualityMenu.call(player, instance.getAvailableQualityLevels()); break; case 2: player.media.paused = true; utils.dispatchEvent.call(player, player.media, 'pause'); break; default: break; } utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, { code: event.data, }); }, }, }); }, }; export default youtube;