aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSam Potts <me@sampotts.me>2017-04-25 18:37:31 +1000
committerSam Potts <me@sampotts.me>2017-04-25 18:37:31 +1000
commit1960d35d8b8319614adb4a7852a9614dfa8c1eb1 (patch)
treece5d6cce58149fb7942c44c616ab47e3560d8816 /src
parentbbe4b7e565f6fd8acc946065934bdeae8d0dfc0e (diff)
downloadplyr-1960d35d8b8319614adb4a7852a9614dfa8c1eb1.tar.lz
plyr-1960d35d8b8319614adb4a7852a9614dfa8c1eb1.tar.xz
plyr-1960d35d8b8319614adb4a7852a9614dfa8c1eb1.zip
More work on menus and tidy up
Diffstat (limited to 'src')
-rw-r--r--src/js/plyr.js887
-rw-r--r--src/less/plyr.less5
2 files changed, 314 insertions, 578 deletions
diff --git a/src/js/plyr.js b/src/js/plyr.js
index 55bdd252..99a70b88 100644
--- a/src/js/plyr.js
+++ b/src/js/plyr.js
@@ -37,20 +37,8 @@
enabled: true,
debug: false,
autoplay: false,
- loop: {
- active: false,
- start: 0,
- end: null,
- indicator: {
- start: 0,
- end: 0
- }
- },
seekTime: 10,
volume: 10,
- defaultSpeed: 1.0,
- currentSpeed: 1,
- speeds: [0.5, 1.0, 1.5, 2.0],
duration: null,
displayDuration: true,
loadSprite: true,
@@ -60,18 +48,44 @@
hideControls: true,
showPosterOnEnd: false,
disableContextMenu: true,
+
+ // Quality settings
quality: {
- options: false
+ default: 'auto',
+ selected: 'auto'
},
+
+ // Set loops
+ loop: {
+ active: false,
+ start: 0,
+ end: null,
+ indicator: {
+ start: 0,
+ end: 0
+ }
+ },
+
+ // Speed up/down
+ speed: {
+ selected: 1.0,
+ options: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0]
+ },
+
+ // Keyboard shortcut settings
keyboardShortcuts: {
focused: true,
global: false
},
+
+ // Display tooltips
tooltips: {
controls: false,
seek: true
},
- tracks: [],
+
+ // Selectors
+ // Change these to match your template if using custom HTML
selectors: {
html5: 'video, audio',
embed: '[data-type]',
@@ -94,14 +108,14 @@
pip: '[data-plyr="pip"]',
airplay: '[data-plyr="airplay"]',
settings: '[data-plyr="settings"]',
- speed: '[data-plyr="speed"]',
- loop: '[data-plyr="loop"]',
- language: '[data-plyr="language"]',
- quality: '[data-plyr="quality"]'
+ loop: '[data-plyr="loop"]'
},
inputs: {
seek: '[data-plyr="seek"]',
- volume: '[data-plyr="volume"]'
+ volume: '[data-plyr="volume"]',
+ speed: '[data-plyr="speed"]',
+ language: '[data-plyr="language"]',
+ quality: '[data-plyr="quality"]'
},
display: {
currentTime: '.plyr__time--current',
@@ -117,6 +131,8 @@
quality: '.js-plyr__menu__list--quality'
}
},
+
+ // Class hooks added to the player in different states
classes: {
setup: 'plyr--setup',
ready: 'plyr--ready',
@@ -156,19 +172,27 @@
},
tabFocus: 'tab-focus'
},
+
+ // Captions settings
captions: {
defaultActive: false,
- selectedIndex: 0
+ language: window.navigator.language.split("-")[0]
},
+
+ // Fullscreen settings
fullscreen: {
enabled: true,
fallback: true,
allowAudio: false
},
+
+ // Local storage
storage: {
enabled: true,
key: 'plyr'
},
+
+ // Default controls
controls: [
'play-large',
'play',
@@ -182,6 +206,8 @@
'airplay',
'fullscreen'
],
+
+ // Localisation
i18n: {
restart: 'Restart',
rewind: 'Rewind {seektime} secs',
@@ -208,10 +234,7 @@
all: 'All',
reset: 'Reset',
},
- types: {
- embed: ['youtube', 'vimeo', 'soundcloud'],
- html5: ['video', 'audio']
- },
+
// URLs
urls: {
vimeo: {
@@ -224,6 +247,7 @@
api: 'https://w.soundcloud.com/player/api.js'
}
},
+
// Custom control listeners
listeners: {
seek: null,
@@ -243,7 +267,8 @@
loop: null,
language: null
},
- // Events to watch on HTML5 media elements
+
+ // Events to watch on HTML5 media elements and bubble
events: [
'ready',
'ended',
@@ -265,10 +290,17 @@
'seeked',
'emptied'
],
+
// Logging
logPrefix: ''
};
+ // Types
+ var types = {
+ embed: ['youtube', 'vimeo', 'soundcloud'],
+ html5: ['video', 'audio']
+ };
+
// Check variable types
var is = {
object: function(input) {
@@ -298,6 +330,12 @@
event: function(input) {
return input !== null && input instanceof Event;
},
+ cue: function(input) {
+ return input !== null && (input instanceof window.TextTrackCue || input instanceof window.VTTCue);
+ },
+ track: function(input) {
+ return input !== null && input instanceof window.TextTrack;
+ },
undefined: function(input) {
return input !== null && typeof input === 'undefined';
},
@@ -389,13 +427,14 @@
}
// Inject a script
- function injectScript(source) {
- if (document.querySelectorAll('script[src="' + source + '"]').length) {
+ function injectScript(url) {
+ // Check script is not already referenced
+ if (document.querySelectorAll('script[src="' + url + '"]').length) {
return;
}
var tag = document.createElement('script');
- tag.src = source;
+ tag.src = url;
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
@@ -569,11 +608,6 @@
}
}
- // Get a classname from selector
- function getClassname(selector) {
- return selector.replace('.', '');
- }
-
// Toggle class on an element
function toggleClass(element, className, state) {
if (element) {
@@ -860,7 +894,8 @@
var support = {
// Fullscreen support and set prefix
fullscreen: fullscreen.prefix !== false,
- // Local storage mode
+
+ // Local storage
// We can't assume if local storage is present that we can use it
storage: (function() {
if (!('localStorage' in window)) {
@@ -887,16 +922,19 @@
return false;
})(),
+
// Picture-in-picture support
// Safari only currently
pip: (function() {
return is.function(createElement('video').webkitSetPresentationMode);
})(),
+
// Airplay support
// Safari only currently
airplay: (function() {
return is.function(window.WebKitPlaybackTargetAvailabilityEvent);
})(),
+
// Check for mime type support against a player instance
// Credits: http://diveintohtml5.info/everything.html
// Related: http://www.leanbackplayer.com/test/h5mt.html
@@ -935,7 +973,12 @@
// If we got this far, we're stuffed
return false;
- }
+ },
+
+ // Check for textTracks support
+ textTracks: (function() {
+ return 'textTracks' in document.createElement('video');
+ })()
};
// Player instance
@@ -950,6 +993,7 @@
// Elements cache
player.elements = {
+ container: null,
buttons: {},
display: {},
progress: {},
@@ -959,37 +1003,29 @@
panes: {},
tabs: {}
},
- media: media
+ media: media,
+ captions: null
};
// Captions
player.captions = {
enabled: false,
- textTracks: false,
- captions: []
+ captions: [],
+ tracks: [],
+ currentTrack: null
};
// Set media
var original = media.cloneNode(true);
// Debugging
- function logger(type, args) {
- if (config.debug && window.console) {
- args = Array.prototype.slice.call(args);
-
- if (is.string(config.logPrefix) && config.logPrefix.length) {
- args.unshift(config.logPrefix);
- }
-
- window.console[type].apply(window.console, args);
- }
+ var log = function() {};
+ var warn = function() {};
+ if (config.debug && 'console' in window) {
+ log = window.console.log;
+ warn = window.console.warn;
}
- var log = function() {
- logger('log', arguments);
- };
- var warn = function() {
- logger('warn', arguments);
- };
+
// Log config options and support
log('Config', config);
log('Support', support);
@@ -1200,7 +1236,8 @@
min: 0,
max: 100,
step: 0.1,
- value: 0
+ value: 0,
+ autocomplete: 'off'
}, attributes));
player.elements.inputs[type] = input;
@@ -1394,8 +1431,7 @@
id: 'plyr-settings-' + data.id + '-home',
'aria-hidden': false,
'aria-labelled-by': 'plyr-settings-toggle-' + data.id,
- role: 'tabpanel',
- tabindex: -1
+ role: 'tabpanel'
});
var tabs = createElement('ul', {
@@ -1440,7 +1476,7 @@
var pane = createElement('div', {
id: 'plyr-settings-' + data.id + '-' + type,
'aria-hidden': true,
- 'aria-labelled-by': 'plyr-settings-tab-' + data.id,
+ 'aria-labelled-by': 'plyr-settings-' + data.id + '-' + type + '-tab',
role: 'tabpanel',
tabindex: -1
});
@@ -1457,29 +1493,6 @@
var options = createElement('ul');
- /*switch (type) {
- case 'captions':
- if (is.array(config.tracks)) {
- config.tracks.forEach(function(track, index) {
- if (is.function(track)) {
- return;
- }
-
- var option = createElement('li');
-
- var button = createButton('language', {
- 'data-language': track.srclang,
- 'data-index': index
- }, track.label);
-
- option.appendChild(button);
-
- options.appendChild(options);
- });
- }
- break;
- }*/
-
pane.appendChild(options);
inner.appendChild(pane);
@@ -1494,183 +1507,6 @@
controls.appendChild(menu);
player.elements.settings.menu = menu;
-
- /*html.push(
- '<div class="plyr__menu" data-plyr="settings">',
- '<button type="button" id="plyr-settings-toggle-{id}" class="plyr__control" aria-haspopup="true" aria-controls="plyr-settings-{id}" aria-expanded="false">',
- '<svg><use xlink:href="' + iconPath + '-settings" /></svg>',
- '<span class="plyr__sr-only">' + config.i18n.settings + '</span>',
- '</button>',
- '<form class="plyr__menu__container" id="plyr-settings-{id}" aria-hidden="true" aria-labelled-by="plyr-settings-toggle-{id}" role="tablist" tabindex="-1">',
- '<div>',
- '<div id="plyr-settings-{id}-primary" aria-hidden="false" aria-labelled-by="plyr-settings-toggle-{id}" role="tabpanel" tabindex="-1">',
- '<ul>',
- captionsMenuItem,
- '<li role="tab">',
- '<button type="button" class="plyr__control plyr__control--forward" id="plyr-settings-{id}-speed-toggle" aria-haspopup="true" aria-controls="plyr-settings-{id}-speed" aria-expanded="false">',
- config.i18n.speed +
- '<span class="plyr__menu__value" data-menu="speed">{speed}</span>',
- '</button>',
- '</li>',
- '<li role="tab">',
-
- //showQuality,
-
- '<button type="button" class="plyr__control plyr__control--forward" id="plyr-settings-{id}-quality-toggle" aria-haspopup="true" aria-controls="plyr-settings-{id}-quality" aria-expanded="false">',
- config.i18n.quality,
- '<span class="plyr__menu__value">{quality}</span>',
- '</button>',
-
- '</li>',
- '<li role="tab">',
- '<button type="button" class="plyr__control plyr__control--forward" id="plyr-settings-{id}-loop-toggle" aria-haspopup="true" aria-controls="plyr-settings-{id}-loop" aria-expanded="false">',
- config.i18n.loop +
- '<span class="plyr__menu__value" data-menu="loop">{loop}</span>',
- '</button>',
- '</li>',
- '</ul>',
- '</div>',
- '<div id="plyr-settings-{id}-captions" aria-hidden="true" aria-labelled-by="plyr-settings-{id}-captions-toggle" role="tabpanel" tabindex="-1">',
- '<ul>',
- '<li role="tab">',
- '<button type="button" class="plyr__control plyr__control--back" aria-haspopup="true" aria-controls="plyr-settings-{id}-primary" aria-expanded="false">',
- config.i18n.captions,
- '</button>',
- '</li>',
- '<li data-captions="langs">',
- buildCaptionsMenu(),
- '</li>',
- '<li>',
- '<button type="button" class="plyr__control" data-plyr="captions_menu">Off</button>',
- '</li>',
- '</ul>',
- '</div>',
- '<div id="plyr-settings-{id}-speed" aria-hidden="true" aria-labelled-by="plyr-settings-{id}-speed-toggle" role="tabpanel" tabindex="-1">',
- '<ul>',
- '<li role="tab">',
- '<button type="button" class="plyr__control plyr__control--back" aria-haspopup="true" aria-controls="plyr-settings-{id}-primary" aria-expanded="false">',
- config.i18n.speed,
- '</button>',
- '</li>',
- '<li>',
- '<label class="plyr__control">',
- '<input type="radio" name="speed" data-plyr="speed" value="2.0" '+ (config.currentSpeed === 2 ? 'checked' : '') +'>',
- '2.0&times;',
- '</label>',
- '</li>',
- '<li>',
- '<label class="plyr__control">',
- '<input type="radio" name="speed" data-plyr="speed" value="1.5" '+ (config.currentSpeed === 1.5 ? 'checked' : '') +'>',
- '1.5&times;',
- '</label>',
- '</li>',
- '<li>',
- '<label class="plyr__control">',
- '<input type="radio" name="speed" data-plyr="speed" value="1.0" '+ (config.currentSpeed === 1 ? 'checked' : '') +'>',
- '1.0&times;',
- '</label>',
- '</li>',
- '<li>',
- '<label class="plyr__control">',
- '<input type="radio" name="speed" data-plyr="speed" value="0.5" '+ (config.currentSpeed === 0.5 ? 'checked' : '') +'>',
- '0.5&times;',
- '</label>',
- '</li>',
- '</ul>',
- '</div>',
- '<div id="plyr-settings-{id}-quality" aria-hidden="true" aria-labelled-by="plyr-settings-{id}-quality-toggle" role="tabpanel" tabindex="-1">',
- '<ul>',
- '<li role="tab">',
- '<button type="button" class="plyr__control plyr__control--back" aria-haspopup="true" aria-controls="plyr-settings-{id}-primary" aria-expanded="false">',
- config.i18n.quality,
- '</button>',
- '</li>',
- '<li>',
- '<label class="plyr__control">',
- '<input type="radio" name="quality">',
- '2160P',
- '<span class="plyr__menu__value">',
- '<span class="plyr__badge">4K</span>',
- '</span>',
- '</label>',
- '</li>',
- '<li>',
- '<label class="plyr__control">',
- '<input type="radio" name="quality">',
- '1440P',
- '<span class="plyr__menu__value">',
- '<span class="plyr__badge">WQHD</span>',
- '</span>',
- '</label>',
- '</li>',
- '<li>',
- '<label class="plyr__control">',
- '<input type="radio" name="quality">',
- '1080P',
- '<span class="plyr__menu__value">',
- '<span class="plyr__badge">HD</span>',
- '</span>',
- '</label>',
- '</li>',
- '<li>',
- '<label class="plyr__control">',
- '<input type="radio" name="quality">',
- '720P',
- '<span class="plyr__menu__value">',
- '<span class="plyr__badge">HD</span>',
- '</span>',
- '</label>',
- '</li>',
- '<li>',
- '<label class="plyr__control">',
- '<input type="radio" name="quality">',
- '480P',
- '</label>',
- '</li>',
- '<li>',
- '<label class="plyr__control">',
- '<input type="radio" name="quality">',
- '360P',
- '</label>',
- '</li>',
- '</ul>',
- '</div>',
- '<div id="plyr-settings-{id}-loop" aria-hidden="true" aria-labelled-by="plyr-settings-{id}-loop-toggle" role="tabpanel" tabindex="-1">',
- '<ul>',
- '<li role="tab">',
- '<button type="button" class="plyr__control plyr__control--back" aria-haspopup="true" aria-controls="plyr-settings-{id}-primary" aria-expanded="false">',
- config.i18n.loop,
- '</button>',
- '</li>',
- '<li>',
- '<button type="button" class="plyr__control" data-plyr="loop" data-plyr-loop="all">',
- config.i18n.loopAll,
- '<span></span>',
- '</button>',
- '</li>',
- '<li>',
- '<button type="button" class="plyr__control" data-plyr="loop" data-plyr-loop="start">',
- config.i18n.loopStart,
- '<span></span>',
- '</button>',
- '</li>',
- '<li>',
- '<button type="button" class="plyr__control" data-plyr="loop" data-plyr-loop="end">',
- config.i18n.loopEnd,
- '<span></span>',
- '</button>',
- '</li>',
- '<li>',
- '<button type="button" class="plyr__control" data-plyr="loop" data-plyr-loop="none">',
- config.i18n.loopNone,
- '</button>',
- '</li>',
- '</ul>',
- '</div>',
- '</div>',
- '</form>',
- '</div>'
- ); */
}
// Picture in picture button
@@ -1691,6 +1527,7 @@
player.elements.controls = controls;
setLoopMenu();
+ setSpeedMenu();
return controls;
}
@@ -1698,15 +1535,11 @@
// Set the YouTube quality menu
// TODO: Support for HTML5
// YouTube: "hd2160", "hd1440", "hd1080", "hd720", "large", "medium", "small", "tiny", "auto"
- function setQualityMenu(available, current) {
- if (is.object(player.quality)) {
- return;
- }
+ function setQualityMenu(options, current) {
+ var list = player.elements.settings.panes.quality.querySelector('ul');
- player.quality = {
- available: available,
- current: current
- };
+ // Empty the menu
+ emptyElement(list);
// Get the badge HTML for HD, 4K etc
function getBadge(quality) {
@@ -1756,28 +1589,28 @@
}
}
- if (is.array(available) && available.length) {
+ if (is.array(options) && !is.empty(options)) {
// Remove any unwanted quality levels
- var filtered = available.filter(function(quality) {
+ var filtered = options.filter(function(quality) {
return ['tiny', 'small'].indexOf(quality) === -1;
});
- var list = player.elements.settings.panes.quality.querySelector('ul');
-
filtered.forEach(function(quality) {
var item = createElement('li');
var label = createElement('label', {
- class: config.classes.control
+ class: config.classes.control,
+ for: 'plyr-quality-' + quality
});
- var radio = createElement('input', {
+ var radio = createElement('input', extend(getAttributesFromSelector(config.selectors.inputs.quality), {
type: 'radio',
- name: 'quality',
+ id: 'plyr-quality-' + quality,
+ name: 'plyr-quality',
value: quality,
- });
+ }));
- if (quality === player.quality.current) {
+ if (quality === config.quality.selected) {
radio.setAttribute('checked', '');
}
@@ -1801,15 +1634,17 @@
var options = ['start', 'end', 'all', 'reset'];
var list = player.elements.settings.panes.loop.querySelector('ul');
+ // Empty the menu
+ emptyElement(list);
+
options.forEach(function(option) {
var item = createElement('li');
- var button = createElement('button', {
+ var button = createElement('button', extend(getAttributesFromSelector(config.selectors.buttons.loop), {
type: 'button',
class: config.classes.control,
- 'data-plyr': 'loop',
'data-plyr-loop-action': option
- }, config.i18n[option]);
+ }), config.i18n[option]);
if (inArray(['start', 'end'], option)) {
var badge = createBadge('0:00');
@@ -1822,6 +1657,91 @@
});
}
+ // Set a list of available captions languages
+ function setCaptionsMenu() {
+ var list = player.elements.settings.panes.captions.querySelector('ul');
+
+ // Empty the menu
+ emptyElement(list);
+
+ // If there's no captions, bail
+ if (is.empty(player.captions.tracks)) {
+ return;
+ }
+
+ [].forEach.call(player.captions.tracks, function(track) {
+ if (is.function(track)) {
+ return;
+ }
+
+ var item = createElement('li');
+
+ var label = createElement('label', {
+ class: config.classes.control,
+ for: 'plyr-language-' + track.language
+ });
+
+ var radio = createElement('input', extend(getAttributesFromSelector(config.selectors.inputs.language), {
+ type: 'radio',
+ id: 'plyr-language-' + track.language,
+ name: 'plyr-language',
+ value: track.language,
+ }));
+
+ if (track.language === config.captions.language.toLowerCase()) {
+ radio.setAttribute('checked', '');
+ }
+
+ label.appendChild(radio);
+ label.appendChild(document.createTextNode(track.label || track.language));
+ label.appendChild(createBadge(track.language.toUpperCase()));
+
+ item.appendChild(label);
+
+ list.appendChild(item);
+ });
+ }
+
+ // Set a list of available captions languages
+ function setSpeedMenu(options) {
+ var list = player.elements.settings.panes.speed.querySelector('ul');
+
+ // Empty the menu
+ emptyElement(list);
+
+ // If there's no captions, bail
+ if (!is.array(options)) {
+ options = config.speed.options;
+ }
+
+ options.forEach(function(speed) {
+ var item = createElement('li');
+
+ var label = createElement('label', {
+ class: config.classes.control,
+ for: 'plyr-speed-' + speed.toString().replace('.', '-')
+ });
+
+ var radio = createElement('input', extend(getAttributesFromSelector(config.selectors.inputs.speed), {
+ type: 'radio',
+ id: 'plyr-speed-' + speed.toString().replace('.', '-'),
+ name: 'plyr-speed',
+ value: speed,
+ }));
+
+ if (speed === config.speed.selected) {
+ radio.setAttribute('checked', '');
+ }
+
+ label.appendChild(radio);
+ label.insertAdjacentHTML('beforeend', '&times;' + speed);
+
+ item.appendChild(label);
+
+ list.appendChild(item);
+ });
+ }
+
// Setup fullscreen
function setupFullscreen() {
if (!player.supported.full) {
@@ -1851,194 +1771,111 @@
}
}
- // Display active caption if it contains text
- function setActiveCue(track) {
- // Get the track from the event if needed
- if (is.event(track)) {
- track = track.target;
- }
-
- // Display a cue, if there is one
- if (track.activeCues[0] && 'text' in track.activeCues[0]) {
- setCaption(track.activeCues[0].getCueAsHTML());
- } else {
- setCaption();
- }
- }
-
// Setup captions
function setupCaptions() {
- // Bail if not HTML5 video
- if (player.type !== 'video') {
+ // Bail if not HTML5 video or textTracks not supported
+ if (player.type !== 'video' || !support.textTracks) {
return;
}
// Inject the container
- if (!getElement(config.selectors.captions)) {
- player.elements.wrapper.insertAdjacentHTML('afterbegin', '<div class="' + getClassname(config.selectors.captions) + '"></div>');
+ if (!is.htmlElement(player.elements.captions)) {
+ player.elements.captions = createElement('div', getAttributesFromSelector(config.selectors.captions));
+ player.elements.wrapper.appendChild(player.elements.captions);
}
- // Determine if HTML5 textTracks is supported
- player.captions.textTracks = false;
- if (player.elements.media.textTracks) {
- player.captions.textTracks = true;
- }
-
- // Get URL of caption file if exists
- var captionSources = [];
- var captionSrc = '';
-
- player.elements.media.childNodes.forEach(function(child) {
- if (child.nodeName.toLowerCase() === 'track') {
- if (child.kind === 'captions' || child.kind === 'subtitles') {
- captionSources.push(child.getAttribute('src'));
- }
- }
- });
-
- // Record if caption file exists or not
- player.captions.exist = true;
- if (captionSources.length === 0) {
- player.captions.exist = false;
- log('No caption track found');
- } else if ((Number(config.captions.selectedIndex) + 1) > captionSources.length) {
- player.captions.exist = false;
- log('Caption index out of bound');
- } else {
- captionSrc = captionSources[config.captions.selectedIndex];
- log('Caption track found; URI: ' + captionSrc);
- }
+ // Get tracks
+ player.captions.tracks = player.elements.media.textTracks;
// If no caption file exists, hide container for caption text
- if (!player.captions.exist) {
+ if (is.empty(player.captions.tracks)) {
toggleClass(player.elements.container, config.classes.captions.enabled);
} else {
- var tracks = player.elements.media.textTracks;
+ var language = config.captions.language.toLowerCase();
// Turn off native caption rendering to avoid double captions
- // This doesn't seem to work in Safari 7+, so the <track> elements are removed from the dom below
- [].forEach.call(tracks, function(track) {
- // Remove the listener to prevent event overlapping
+ [].forEach.call(player.captions.tracks, function(track) {
+ // Remove previous bindings (if we've changed source or language)
off(track, 'cuechange', setActiveCue);
// Hide captions
track.mode = 'hidden';
+
+ // If language matches, it's the selected track
+ if (track.language === language) {
+ player.captions.currentTrack = track;
+ }
});
+ // If we couldn't get the requested language, we get the first
+ if (!is.track(player.captions.currentTrack)) {
+ warn('No language found to match ' + language + ' in tracks');
+ player.captions.currentTrack = player.captions.tracks[0];
+ }
+
// Enable UI
showCaptions(player);
- // Disable unsupported browsers than report false positive
- // Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1033144
- if ((player.browser.isIE && player.browser.version >= 10) ||
- (player.browser.isFirefox && player.browser.version >= 31)) {
-
- // Debugging
- log('Detected browser with known TextTrack issues - using manual fallback');
+ // If it's a caption or subtitle, render it
+ var track = player.captions.currentTrack;
+ if (is.track(track) && inArray(['captions', 'subtitles'], track.kind)) {
+ on(track, 'cuechange', setActiveCue);
- // Set to false so skips to 'manual' captioning
- player.captions.textTracks = false;
+ // If we change the active track while a cue is already displayed we need to update it
+ if (track.activeCues && track.activeCues.length > 0) {
+ setActiveCue(track);
+ }
}
- // Rendering caption tracks
- // Native support required - http://caniuse.com/webvtt
- if (player.captions.textTracks) {
- log('TextTracks supported');
+ // Set available languages in list
+ setCaptionsMenu();
+ }
+ }
- var track = tracks[config.captions.selectedIndex];
+ // Get current selected caption language
+ function getLanguage() {
+ if (!support.textTracks || is.empty(player.captions.tracks)) {
+ return 'No Subs';
+ }
- if (track.kind === 'captions' || track.kind === 'subtitles') {
- on(track, 'cuechange', setActiveCue);
+ if (player.captions.enabled) {
+ return player.captions.currentTrack.label;
+ } else {
+ return 'Disabled';
+ }
+ }
- // If we change the active track while a cue is already displayed we need to update it
- if (track.activeCues && track.activeCues.length > 0) {
- setActiveCue(track);
- }
- }
- } else {
- // Caption tracks not natively supported
- log('TextTracks not supported so rendering captions manually');
-
- // Render captions from array at appropriate time
- player.captions.current = '';
- player.captions.captions = [];
-
- if (captionSrc !== '') {
- // Create XMLHttpRequest Object
- var xhr = new XMLHttpRequest();
-
- xhr.onreadystatechange = function() {
- if (xhr.readyState === 4) {
- if (xhr.status === 200) {
- var response = xhr.responseText;
-
- // According to webvtt spec, line terminator consists of one of the following
- // CRLF (U+000D U+000A), LF (U+000A) or CR (U+000D)
- var lineSeparator = '\r\n';
- if (response.indexOf(lineSeparator + lineSeparator) === -1) {
- if (response.indexOf('\r\r') !== -1) {
- lineSeparator = '\r';
- } else {
- lineSeparator = '\n';
- }
- }
-
- var captions = response.split(lineSeparator + lineSeparator);
-
- player.captions.captions = captions.map(function(caption) {
- var parts = caption.split(lineSeparator);
- var index = 0;
-
- // Incase caption numbers are added
- if (parts[index].indexOf(":") !== -1) {
- index = 1;
- }
-
- return [parts[index], parts[index + 1]];
- });
-
- player.captions.captions.shift();
-
- log('Successfully loaded the caption file via AJAX');
- } else {
- warn(config.logPrefix + 'There was a problem loading the caption file via AJAX');
- }
- }
- };
+ // Display active caption if it contains text
+ function setActiveCue(track) {
+ // Get the track from the event if needed
+ if (is.event(track)) {
+ track = track.target;
+ }
- xhr.open('get', captionSrc, true);
+ var active = track.activeCues[0];
- xhr.send();
- }
- }
+ // Display a cue, if there is one
+ if (is.cue(active)) {
+ setCaption(active.getCueAsHTML());
+ } else {
+ setCaption();
}
}
// Select active caption
- function setCaptionIndex(index) {
- // Save active caption
- config.captions.selectedIndex = index || config.captions.selectedIndex;
+ function setLanguage(language) {
+ // Save config
+ if (is.string(language)) {
+ config.captions.language = language.toLowerCase();
+ } else if (is.event(language)) {
+ config.captions.language = language.target.value.toLowerCase();
+ }
// Clear caption
setCaption();
// Re-run setup
setupCaptions();
-
- //getElement('[data-captions="settings"]').innerHTML = getSelectedLanguage();
- }
-
- // Get current selected caption language
- function getSelectedLanguage() {
- if (config.tracks.length === 0) {
- return 'No Subs';
- }
-
- if (player.captions.enabled || !is.boolean(player.captions.enabled) && player.storage.captions) {
- return config.tracks[config.captions.selectedIndex].label;
- } else {
- return 'Disabled';
- }
}
// Set the current caption
@@ -2058,7 +1895,7 @@
// Set the span content
if (is.string(caption)) {
- content.innerHTML = caption.trim();
+ content.textContent = caption.trim();
} else {
content.appendChild(caption);
}
@@ -2071,86 +1908,6 @@
}
}
- // Captions functions
- // Seek the manual caption time and update UI
- function seekManualCaptions(time) {
- // Utilities for caption time codes
- function timecodeCommon(timecode, pos) {
- var parts = [];
- parts = timecode.split(' --> ');
- for (var i = 0; i < parts.length; i++) {
- // WebVTT allows for extra meta data after the timestamp line
- // So get rid of this if it exists
- parts[i] = parts[i].replace(/(\d+:\d+:\d+\.\d+).*/, "$1");
- }
- return subTcSecs(parts[pos]);
- }
-
- function timecodeMin(timecode) {
- return timecodeCommon(timecode, 0);
- }
-
- function timecodeMax(timecode) {
- return timecodeCommon(timecode, 1);
- }
-
- function subTcSecs(timecode) {
- if (is.undefined(timecode)) {
- return 0;
- } else {
- var tc1 = [];
- var tc2 = [];
- var seconds = 0;
- tc1 = timecode.split(',');
- tc2 = tc1[0].split(':');
-
- for (var i = 0, len = tc2.length; i < len; i++) {
- seconds += Math.floor(tc2[i] * (Math.pow(60, len - (i + 1))));
- }
-
- return seconds;
- }
- }
-
- // If it's not video, or we're using textTracks, bail.
- if (player.captions.textTracks || player.type !== 'video' || !player.supported.full) {
- return;
- }
-
- // Reset subcount
- player.captions.count = 0;
-
- // Check time is a number, if not use currentTime
- // IE has a bug where currentTime doesn't go to 0
- // https://twitter.com/Sam_Potts/status/573715746506731521
- time = is.number(time) ? time : player.elements.media.currentTime;
-
- // If there's no subs available, bail
- if (!player.captions.captions[player.captions.count]) {
- return;
- }
-
- while (timecodeMax(player.captions.captions[player.captions.count][0]) < time.toFixed(1)) {
- player.captions.count++;
-
- if (player.captions.count > player.captions.captions.length - 1) {
- player.captions.count = player.captions.captions.length - 1;
- break;
- }
- }
-
- // Check if the next caption is in the current time range
- if (player.elements.media.currentTime.toFixed(1) >= timecodeMin(player.captions[player.subcount][0]) &&
- player.elements.media.currentTime.toFixed(1) <= timecodeMax(player.captions[player.subcount][0])) {
- player.captions.current = player.captions.captions[player.captions.count][1];
-
- // Render the caption
- setCaption(player.captions.current);
- } else {
- setCaption();
- }
- }
-
// Display captions container and button (for initialization)
function showCaptions() {
// If there's no caption toggle, bail
@@ -2188,12 +1945,6 @@
// Set global
player.captions.enabled = show;
- //player.elements.buttons.captions_menu.innerHTML = show ? 'Off' : 'On';
- //TODO: display lang getElement('[data-captions="settings"]').innerHTML = getSubsLangValue();
-
- // Set current language etc
- //elements.buttons.captions_menu.innerHTML = show ? 'Off' : 'On';
- //getElement('[data-captions="settings"]').innerHTML = getSubsLangValue();
// Toggle state
toggleState(player.elements.buttons.captions, player.captions.enabled);
@@ -2238,11 +1989,11 @@
var controls = createControls({
id: player.id,
seektime: config.seekTime,
- speed: getSpeedDisplayValue(),
+ speed: getSpeed(),
// TODO: Get current quality
quality: 'HD',
// TODO: Set language automatically based on UA?
- captions: 'English',
+ captions: getLanguage(),
// TODO: Get loop
loop: 'None'
});
@@ -2278,7 +2029,7 @@
}
// Find the UI controls and store references
- // TODO: Restore when re-enabling custom HTML
+ // TODO: Re-configure for new elements
/*function findElements() {
try {
player.elements.controls = getElement(config.selectors.controls.wrapper);
@@ -2341,7 +2092,7 @@
// Toggle native controls
function toggleNativeControls(toggle) {
- if (toggle && inArray(config.types.html5, player.type)) {
+ if (toggle && inArray(types.html5, player.type)) {
player.elements.media.setAttribute('controls', '');
} else {
player.elements.media.removeAttribute('controls');
@@ -2439,7 +2190,7 @@
// Add video class for embeds
// This will require changes if audio embeds are added
- if (inArray(config.types.embed, player.type)) {
+ if (inArray(types.embed, player.type)) {
toggleClass(player.elements.container, config.classes.type.replace('{0}', 'video'), true);
}
@@ -2447,7 +2198,7 @@
toggleClass(player.elements.container, config.classes.pip.enabled, support.pip && player.type === 'video');
// Check for airplay support
- toggleClass(player.elements.container, config.classes.airplay.enabled, support.airplay && inArray(config.types.html5, player.type));
+ toggleClass(player.elements.container, config.classes.airplay.enabled, support.airplay && inArray(types.html5, player.type));
// If there's no autoplay attribute, assume the video is stopped and add state class
toggleClass(player.elements.container, config.classes.stopped, config.autoplay);
@@ -2473,7 +2224,7 @@
}
// Embeds
- if (inArray(config.types.embed, player.type)) {
+ if (inArray(types.embed, player.type)) {
setupEmbed();
}
}
@@ -2625,7 +2376,7 @@
wmode: 'transparent',
modestbranding: 1,
disablekb: 1,
- origin: '*' // https://code.google.com/p/gdata-issues/issues/detail?id=5788#c45
+ origin: 'https://plyr.io'
},
events: {
'onError': function(event) {
@@ -2677,7 +2428,7 @@
// Set the tabindex
if (player.supported.full) {
- player.elements.media.querySelector('iframe').setAttribute('tabindex', '-1');
+ player.elements.media.querySelector('iframe').setAttribute('tabindex', -1);
}
// Update UI
@@ -2840,7 +2591,7 @@
// Fix keyboard focus issues
// https://github.com/Selz/plyr/issues/317
if (is.htmlElement(player.embed.element) && player.supported.full) {
- player.embed.element.setAttribute('tabindex', '-1');
+ player.embed.element.setAttribute('tabindex', -1);
}
});
@@ -3068,47 +2819,47 @@
// Set playback speed
function setSpeed(speed) {
// Load speed from storage or default value
- if (is.undefined(speed)) {
- speed = player.storage.speed || config.defaultSpeed;
+ if (is.event(speed)) {
+ speed = parseFloat(speed.target.value);
+ } else if (!is.number(speed)) {
+ speed = parseFloat(player.storage.speed || config.speed.selected);
}
- if (!is.array(config.speeds)) {
+ if (!is.array(config.speed.options)) {
warn('Invalid speeds format');
return;
}
if (!is.number(speed)) {
- var index = config.speeds.indexOf(config.currentSpeed);
+ var index = config.speed.options.indexOf(config.speed.selected);
if (index !== -1) {
- var nextIndex = index + 1;
- if (nextIndex >= config.speeds.length) {
- nextIndex = 0;
+ var next = index + 1;
+ if (next >= config.speeds.length) {
+ next = 0;
}
- speed = config.speeds[nextIndex];
+ speed = config.speed.options[next];
} else {
- speed = config.defaultSpeed;
+ speed = config.speed.selected;
}
}
// Store current speed
- config.currentSpeed = speed;
+ config.speed.selected = speed;
// Set HTML5 speed
+ // TODO: set YouTube
player.elements.media.playbackRate = speed;
// Save speed to localStorage
updateStorage({
speed: speed
});
-
- // Update current value of menu
- // document.querySelector('[data-menu="speed"]').innerHTML = getSpeedDisplayValue();
}
// Get the current speed value
- function getSpeedDisplayValue() {
- return config.currentSpeed.toFixed(1).toString().replace('.0', '') + '&times;'
+ function getSpeed() {
+ return config.speed.selected.toFixed(1).toString().replace('.0', '') + '&times;'
}
// Rewind
@@ -3161,7 +2912,7 @@
} catch (e) {}
// Embeds
- if (inArray(config.types.embed, player.type)) {
+ if (inArray(types.embed, player.type)) {
switch (player.type) {
case 'youtube':
player.embed.seekTo(targetTime);
@@ -3193,9 +2944,6 @@
// Logging
log('Seeking to ' + player.elements.media.currentTime + ' seconds');
-
- // Special handling for 'manual' captions
- seekManualCaptions(targetTime);
}
// Get the duration (or custom if set)
@@ -3339,7 +3087,7 @@
target.setAttribute('aria-hidden', !show);
toggle.setAttribute('aria-expanded', show);
- target.setAttribute('tabindex', 0);
+ target.removeAttribute('tabindex');
if (isTab) {
container.style.width = targetWidth + 'px';
@@ -3371,7 +3119,7 @@
}
// Embeds
- if (inArray(config.types.embed, player.type)) {
+ if (inArray(types.embed, player.type)) {
// YouTube
switch (player.type) {
case 'youtube':
@@ -3427,7 +3175,7 @@
}
// Embeds
- if (inArray(config.types.embed, player.type)) {
+ if (inArray(types.embed, player.type)) {
switch (player.type) {
case 'youtube':
player.embed.setVolume(player.elements.media.volume * 100);
@@ -3894,7 +3642,7 @@
if (player.type === 'video') {
var firstSource = source.sources[0];
- if ('type' in firstSource && inArray(config.types.embed, firstSource.type)) {
+ if ('type' in firstSource && inArray(types.embed, firstSource.type)) {
player.type = firstSource.type;
}
}
@@ -3930,7 +3678,7 @@
}
// Set attributes for audio and video
- if (inArray(config.types.html5, player.type)) {
+ if (inArray(types.html5, player.type)) {
if (config.crossorigin) {
player.elements.media.setAttribute('crossorigin', '');
}
@@ -3951,7 +3699,7 @@
toggleStyleHook();
// Set new sources for html5
- if (inArray(config.types.html5, player.type)) {
+ if (inArray(types.html5, player.type)) {
insertElements('source', source.sources);
}
@@ -3959,7 +3707,7 @@
setupMedia();
// HTML5 stuff
- if (inArray(config.types.html5, player.type)) {
+ if (inArray(types.html5, player.type)) {
// Setup captions
if ('tracks' in source) {
insertElements('track', source.tracks);
@@ -3970,7 +3718,7 @@
}
// If HTML5 or embed but not fully supported, setupInterface and call ready now
- if (inArray(config.types.html5, player.type) || (inArray(config.types.embed, player.type) && !player.supported.full)) {
+ if (inArray(types.html5, player.type) || (inArray(types.embed, player.type) && !player.supported.full)) {
// Setup interface
setupInterface();
@@ -4030,16 +3778,10 @@
// Detect tab focus
function checkTabFocus(focused) {
- for (var button in player.elements.buttons) {
- var element = player.elements.buttons[button];
+ toggleClass(getElements('.' + config.classes.tabFocus), config.classes.tabFocus, false);
- if (is.nodeList(element)) {
- for (var i = 0; i < element.length; i++) {
- toggleClass(element[i], config.classes.tabFocus, (element[i] === focused));
- }
- } else {
- toggleClass(element, config.classes.tabFocus, (element === focused));
- }
+ if (player.elements.container.contains(focused)) {
+ toggleClass(focused, config.classes.tabFocus, true);
}
}
@@ -4295,22 +4037,19 @@
// Settings menu items - use event delegation as items are added/removed
on(player.elements.settings.menu, 'click', function(event) {
// Settings - Speed
- if (matches(event.target, config.selectors.buttons.speed)) {
- handlerProxy.call(this, event, config.listeners.speed, function() {
- //var speedValue = document.querySelector('[data-plyr="speed"]:checked').value;
- //setSpeed(Number(speedValue));
- console.warn("Set speed");
- });
+ if (matches(event.target, config.selectors.inputs.speed)) {
+ handlerProxy.call(this, event, config.listeners.speed, setSpeed);
}
// Settings - Quality
- else if (matches(event.target, config.selectors.buttons.quality)) {
+ else if (matches(event.target, config.selectors.inputs.quality)) {
handlerProxy.call(this, event, config.listeners.quality, function() {
console.warn("Set quality");
});
}
// Settings - Looping
+ // TODO: use toggle buttons
else if (matches(event.target, config.selectors.buttons.loop)) {
handlerProxy.call(this, event, config.listeners.loop, function() {
// TODO: This should be done in the method itself I think
@@ -4323,12 +4062,8 @@
}
// Settings - Language
- else if (matches(event.target, config.selectors.buttons.language)) {
- handlerProxy.call(this, event, config.listeners.language, function(event) {
- // TODO: This should be done in the method itself I think
- var index = event.target.attributes.getNamedItem("data-index").value;
- setCaptionIndex(index);
- });
+ else if (matches(event.target, config.selectors.inputs.language)) {
+ handlerProxy.call(this, event, config.listeners.language, setLanguage);
}
});
@@ -4408,9 +4143,6 @@
// Time change on media
on(player.elements.media, 'timeupdate seeking', timeUpdate);
- // Update manual captions
- on(player.elements.media, 'timeupdate', seekManualCaptions);
-
// Display duration
on(player.elements.media, 'durationchange loadedmetadata', displayDuration);
@@ -4491,7 +4223,7 @@
// Cancel current network requests
// See https://github.com/Selz/plyr/issues/174
function cancelRequests() {
- if (!inArray(config.types.html5, player.type)) {
+ if (!inArray(types.html5, player.type)) {
return;
}
@@ -4650,7 +4382,7 @@
// Setup interface
// If embed but not fully supported, setupInterface (to avoid flash of controls) and call ready now
- if (inArray(config.types.html5, player.type) || (inArray(config.types.embed, player.type) && !player.supported.full)) {
+ if (inArray(types.html5, player.type) || (inArray(types.embed, player.type) && !player.supported.full)) {
// Setup UI
setupInterface();
@@ -4708,7 +4440,6 @@
// Captions
setupCaptions();
- setCaptionIndex();
// Set volume
setVolume();
@@ -4789,7 +4520,7 @@
toggleCaptions: toggleCaptions,
toggleFullscreen: toggleFullscreen,
toggleControls: toggleControls,
- setCaptionIndex: setCaptionIndex,
+ setLanguage: setLanguage,
isFullscreen: function() {
return player.fullscreen.active || false;
},
diff --git a/src/less/plyr.less b/src/less/plyr.less
index 8e08cf40..98917f96 100644
--- a/src/less/plyr.less
+++ b/src/less/plyr.less
@@ -251,6 +251,11 @@
background: @plyr-captions-bg;
box-decoration-break: clone;
line-height: 150%;
+
+ // Firefox adds a <div> when using getCueAsHTML()
+ div {
+ display: inline;
+ }
}
span:empty {
display: none;