aboutsummaryrefslogtreecommitdiffstats
path: root/src/js/plugins/youtube.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/js/plugins/youtube.js')
-rw-r--r--src/js/plugins/youtube.js246
1 files changed, 105 insertions, 141 deletions
diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js
index 9b067c8a..64b6fff7 100644
--- a/src/js/plugins/youtube.js
+++ b/src/js/plugins/youtube.js
@@ -4,64 +4,54 @@
import controls from './../controls';
import ui from './../ui';
-import utils from './../utils';
+import { dedupe } from './../utils/arrays';
+import { createElement, replaceElement, toggleClass } from './../utils/elements';
+import { triggerEvent } from './../utils/events';
+import fetch from './../utils/fetch';
+import is from './../utils/is';
+import loadImage from './../utils/loadImage';
+import loadScript from './../utils/loadScript';
+import { format, generateId } from './../utils/strings';
+
+// Parse YouTube ID from URL
+function parseId(url) {
+ if (is.empty(url)) {
+ return null;
+ }
+
+ const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
+ return url.match(regex) ? RegExp.$2 : url;
+}
// Standardise YouTube quality unit
function mapQualityUnit(input) {
- switch (input) {
- case 'hd2160':
- return 2160;
-
- case 2160:
- return 'hd2160';
-
- case 'hd1440':
- return 1440;
-
- case 1440:
- return 'hd1440';
-
- case 'hd1080':
- return 1080;
-
- case 1080:
- return 'hd1080';
-
- case 'hd720':
- return 720;
-
- case 720:
- return 'hd720';
-
- case 'large':
- return 480;
-
- case 480:
- return 'large';
-
- case 'medium':
- return 360;
-
- case 360:
- return 'medium';
-
- case 'small':
- return 240;
-
- case 240:
- return 'small';
-
- default:
- return 'default';
+ const qualities = {
+ hd2160: 2160,
+ hd1440: 1440,
+ hd1080: 1080,
+ hd720: 720,
+ large: 480,
+ medium: 360,
+ small: 240,
+ tiny: 144,
+ };
+
+ const entry = Object.entries(qualities).find(entry => entry.includes(input));
+
+ if (entry) {
+ // Get the match corresponding to the input
+ return entry.find(value => value !== input);
}
+
+ return 'default';
}
function mapQualityUnits(levels) {
- if (utils.is.empty(levels)) {
+ if (is.empty(levels)) {
return levels;
}
- return utils.dedupe(levels.map(level => mapQualityUnit(level)));
+ return dedupe(levels.map(level => mapQualityUnit(level)));
}
// Set playback state and trigger change (only on actual change)
@@ -71,24 +61,24 @@ function assurePlaybackState(play) {
}
if (this.media.paused === play) {
this.media.paused = !play;
- utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
+ triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
}
const youtube = {
setup() {
// Add embed class for responsive
- utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
+ toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Set aspect ratio
youtube.setAspectRatio.call(this);
// Setup API
- if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) {
+ if (is.object(window.YT) && is.function(window.YT.Player)) {
youtube.ready.call(this);
} else {
// Load the API
- utils.loadScript(this.config.urls.youtube.sdk).catch(error => {
+ loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
@@ -115,10 +105,10 @@ const youtube = {
// Try via undocumented API method first
// This method disappears now and then though...
// https://github.com/sampotts/plyr/issues/709
- if (utils.is.function(this.embed.getVideoData)) {
+ if (is.function(this.embed.getVideoData)) {
const { title } = this.embed.getVideoData();
- if (utils.is.empty(title)) {
+ if (is.empty(title)) {
this.config.title = title;
ui.setTitle.call(this);
return;
@@ -127,13 +117,12 @@ const youtube = {
// Or via Google API
const key = this.config.keys.google;
- if (utils.is.string(key) && !utils.is.empty(key)) {
- const url = utils.format(this.config.urls.youtube.api, videoId, key);
+ if (is.string(key) && !is.empty(key)) {
+ const url = format(this.config.urls.youtube.api, videoId, key);
- utils
- .fetch(url)
+ fetch(url)
.then(result => {
- if (utils.is.object(result)) {
+ if (is.object(result)) {
this.config.title = result.items[0].snippet.title;
ui.setTitle.call(this);
}
@@ -154,7 +143,7 @@ const youtube = {
// Ignore already setup (race condition)
const currentId = player.media.getAttribute('id');
- if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) {
+ if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return;
}
@@ -162,30 +151,36 @@ const youtube = {
let source = player.media.getAttribute('src');
// Get from <div> if needed
- if (utils.is.empty(source)) {
+ if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}
// Replace the <iframe> with a <div> due to YouTube API issues
- const videoId = utils.parseYouTubeId(source);
- const id = utils.generateId(player.provider);
- const container = utils.createElement('div', { id });
- player.media = utils.replaceElement(container, player.media);
+ const videoId = parseId(source);
+ const id = generateId(player.provider);
+
+ // Get poster, if already set
+ const { poster } = player;
- // Set poster image
+ // Replace media element
+ const container = createElement('div', { id, poster });
+ player.media = replaceElement(container, player.media);
+
+ // Id to poster wrapper
const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
- utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
- .catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
- .catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
+ loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
+ .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
+ .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
.then(image => ui.setPoster.call(player, image.src))
.then(posterSrc => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!posterSrc.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
}
- });
+ })
+ .catch(() => {});
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
@@ -211,49 +206,26 @@ const youtube = {
},
events: {
onError(event) {
- // If we've already fired an error, don't do it again
- // YouTube fires onError twice
- if (utils.is.object(player.media.error)) {
- return;
+ // YouTube may fire onError twice, so only handle it once
+ if (!player.media.error) {
+ const code = event.data;
+ // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
+ const message =
+ {
+ 2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
+ 5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
+ 100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
+ 101: 'The owner of the requested video does not allow it to be played in embedded players.',
+ 150: 'The owner of the requested video does not allow it to be played in embedded players.',
+ }[code] || 'An unknown error occured';
+
+ player.media.error = { code, message };
+
+ triggerEvent.call(player, player.media, 'error');
}
-
- const detail = {
- code: event.data,
- };
-
- // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
- switch (event.data) {
- case 2:
- detail.message =
- 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.';
- break;
-
- case 5:
- detail.message =
- 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.';
- break;
-
- case 100:
- detail.message =
- 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.';
- break;
-
- case 101:
- case 150:
- detail.message = 'The owner of the requested video does not allow it to be played in embedded players.';
- break;
-
- default:
- detail.message = 'An unknown error occured';
- break;
- }
-
- player.media.error = detail;
-
- utils.dispatchEvent.call(player, player.media, 'error');
},
onPlaybackQualityChange() {
- utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
+ triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: player.media.quality,
});
},
@@ -264,7 +236,7 @@ const youtube = {
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
- utils.dispatchEvent.call(player, player.media, 'ratechange');
+ triggerEvent.call(player, player.media, 'ratechange');
},
onReady(event) {
// Get the instance
@@ -305,7 +277,7 @@ const youtube = {
// Set seeking state and trigger event
player.media.seeking = true;
- utils.dispatchEvent.call(player, player.media, 'seeking');
+ triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
@@ -328,15 +300,7 @@ const youtube = {
return mapQualityUnit(instance.getPlaybackQuality());
},
set(input) {
- const quality = input;
-
- // Set via API
- instance.setPlaybackQuality(mapQualityUnit(quality));
-
- // Trigger request event
- utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
- quality,
- });
+ instance.setPlaybackQuality(mapQualityUnit(input));
},
});
@@ -349,7 +313,7 @@ const youtube = {
set(input) {
volume = input;
instance.setVolume(volume * 100);
- utils.dispatchEvent.call(player, player.media, 'volumechange');
+ triggerEvent.call(player, player.media, 'volumechange');
},
});
@@ -360,10 +324,10 @@ const youtube = {
return muted;
},
set(input) {
- const toggle = utils.is.boolean(input) ? input : muted;
+ const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
- utils.dispatchEvent.call(player, player.media, 'volumechange');
+ triggerEvent.call(player, player.media, 'volumechange');
},
});
@@ -389,8 +353,8 @@ const youtube = {
player.media.setAttribute('tabindex', -1);
}
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
- utils.dispatchEvent.call(player, player.media, 'durationchange');
+ triggerEvent.call(player, player.media, 'timeupdate');
+ triggerEvent.call(player, player.media, 'durationchange');
// Reset timer
clearInterval(player.timers.buffering);
@@ -402,7 +366,7 @@ const youtube = {
// 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');
+ triggerEvent.call(player, player.media, 'progress');
}
// Set last buffer point
@@ -413,7 +377,7 @@ const youtube = {
clearInterval(player.timers.buffering);
// Trigger event
- utils.dispatchEvent.call(player, player.media, 'canplaythrough');
+ triggerEvent.call(player, player.media, 'canplaythrough');
}
}, 200);
@@ -427,15 +391,12 @@ const youtube = {
// Reset timer
clearInterval(player.timers.playing);
- const seeked = player.media.seeking && [
- 1,
- 2,
- ].includes(event.data);
+ const seeked = player.media.seeking && [1, 2].includes(event.data);
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
- utils.dispatchEvent.call(player, player.media, 'seeked');
+ triggerEvent.call(player, player.media, 'seeked');
}
// Handle events
@@ -448,11 +409,11 @@ const youtube = {
switch (event.data) {
case -1:
// Update scrubber
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
+ triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
- utils.dispatchEvent.call(player, player.media, 'progress');
+ triggerEvent.call(player, player.media, 'progress');
break;
@@ -465,7 +426,7 @@ const youtube = {
instance.stopVideo();
instance.playVideo();
} else {
- utils.dispatchEvent.call(player, player.media, 'ended');
+ triggerEvent.call(player, player.media, 'ended');
}
break;
@@ -477,11 +438,11 @@ const youtube = {
} else {
assurePlaybackState.call(player, true);
- utils.dispatchEvent.call(player, player.media, 'playing');
+ triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
+ triggerEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
@@ -489,11 +450,14 @@ const youtube = {
// 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');
+ triggerEvent.call(player, player.media, 'durationchange');
}
// Get quality
- controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
+ controls.setQualityMenu.call(
+ player,
+ mapQualityUnits(instance.getAvailableQualityLevels()),
+ );
}
break;
@@ -511,7 +475,7 @@ const youtube = {
break;
}
- utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, {
+ triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},