Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/js/captions.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,65 @@ const captions = {
on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
}

const { upload } = this.config.captions;
if (upload && upload.enabled) {
const addTrack = ({ text, label, src, kind = 'captions', srclang = '--', type = 'text/vtt;charset=utf-8' }) => {
if (!src) {
if (!text) {
throw new Error(`Must specify either 'text' or 'src'`);
}
const blob = new Blob([text], {
type,
});
src = window.URL.createObjectURL(blob);
}
const track = createElement('track', {
src,
kind,
srclang,
label,
});
this.media.appendChild(track);
}
const { formats, callback, onInput, onProcessed } = upload;
const accept = formats.map(x => `.${x}`).join(',');
const parent = this.elements.settings.panels.captions;

if (!parent.querySelector('#upload-captions')) {
const fileInput = createElement('input', {
id: 'upload-captions',
type: 'file',
accept,
});
fileInput.style.display = 'none';
parent.appendChild(fileInput);
fileInput.addEventListener('change', e => {
const file = e.target.files[0];
const { name: label } = file;
const reader = new FileReader();
reader.onload = () => {
const text = reader.result;
const hasProcessing = Boolean(onInput) && Boolean(onProcessed);
if (callback && Boolean(onInput)) {
// We need to trigger this event even if onProcessed is false
// since the user may just want to be informed of this event.
triggerEvent.call(this, this.media, onInput, true, { label, text });
}
if (!callback || !hasProcessing) {
// The user is not processing the file. Just add the track
addTrack({ label, text });
}
};
reader.readAsText(file);
});
if (Boolean(onProcessed)) {
on.call(this, this.media, onProcessed, e => {
addTrack(e.detail);
});
}
}
}

// Update available languages in list next tick (the event must not be triggered before the listeners)
setTimeout(captions.update.bind(this), 0);
},
Expand Down Expand Up @@ -219,6 +278,14 @@ const captions = {
return;
}

if (this.config.captions.upload && this.config.captions.upload.enabled) {
if (index === -2) {
// Show the input file dialog allowing the user to pick a file
const input = this.elements.settings.panels.captions.querySelector('#upload-captions');
input.click();
}
}

if (!is.number(index)) {
this.debug.warn('Invalid caption argument', index);
return;
Expand Down
20 changes: 20 additions & 0 deletions src/js/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,26 @@ const defaults = {
// Listen to new tracks added after Plyr is initialized.
// This is needed for streaming captions, but may result in unselectable options
update: false,
// Allow uploading captions from local filesystem.
// Once a file is selected,
// This either requires update to be true or the developer needs to call
upload: {
enabled: false,
// The formats that are valid
formats: ['vtt'],
// Set to true if you're allowing formats other than vtt where the input needs to be processed.
// This is useful in cases where the developer allows selecting of
// formats such as srt, ssa, etc, which need to be converted into vtt
// before plyr can add it as a track element.
callback: false,
// The event that plyr will trigger when the user has selected a file and callback is true.
// If null, then plyr will treat the file as a VTT file and directly add it
onInput: 'trackinput',
// The event that plyr will listen for when callback is true.
// If null, then plyr will treat the file as a VTT file and directly add it.
// You can also add tracks manually
onProcessed: 'inserttrack',
},
},

// Fullscreen settings
Expand Down
30 changes: 23 additions & 7 deletions src/js/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -462,14 +462,14 @@ const controls = {
},

// Create a settings menu item
createMenuItem({ value, list, type, title, badge = null, checked = false }) {
createMenuItem({ value, list, type, title, badge = null, checked = false, role = 'menuitemradio' }) {
const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);

const menuItem = createElement(
'button',
extend(attributes, {
type: 'button',
role: 'menuitemradio',
role,
class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),
'aria-checked': checked,
value,
Expand Down Expand Up @@ -515,11 +515,15 @@ const controls = {

event.preventDefault();
event.stopPropagation();

// Should we navigate back to home after this event?
let shouldMenuPanelGoHome = true;
menuItem.checked = true;

switch (type) {
case 'language':
if (value === -2) {
// We're uploading captions. Don't make the menu go back
shouldMenuPanelGoHome = false;
}
this.currentTrack = Number(value);
break;

Expand All @@ -534,8 +538,9 @@ const controls = {
default:
break;
}

controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));
if (shouldMenuPanelGoHome) {
controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));
}
},
type,
false,
Expand Down Expand Up @@ -1004,7 +1009,8 @@ const controls = {
const type = 'captions';
const list = this.elements.settings.panels.captions.querySelector('[role="menu"]');
const tracks = captions.getTracks.call(this);
const toggle = Boolean(tracks.length);
// We need to show the menu if there are captions or if upload captions is enabled
const toggle = Boolean(tracks.length) || (Boolean(this.config.captions.upload) && Boolean(this.config.captions.upload.enabled));

// Toggle the pane and tab
controls.toggleMenuButton.call(this, type, toggle);
Expand Down Expand Up @@ -1039,6 +1045,16 @@ const controls = {
type: 'language',
});

if (this.config.captions.upload && this.config.captions.upload.enabled) {
options.unshift({
value: -2,
title: 'Upload Captions',
list,
type: 'language',
role: 'button',
});
}

// Generate options
options.forEach(controls.createMenuItem.bind(this));

Expand Down