mirror of
https://github.com/jellyfin/jellyfin-media-player.git
synced 2024-11-27 00:00:38 +00:00
580 lines
20 KiB
JavaScript
580 lines
20 KiB
JavaScript
const jmpInfo = JSON.parse(window.atob("@@data@@"));
|
|
window.jmpInfo = jmpInfo;
|
|
|
|
const features = [
|
|
"filedownload",
|
|
"displaylanguage",
|
|
"htmlaudioautoplay",
|
|
"htmlvideoautoplay",
|
|
"externallinks",
|
|
"clientsettings",
|
|
"multiserver",
|
|
"exitmenu",
|
|
"remotecontrol",
|
|
"fullscreenchange",
|
|
"filedownload",
|
|
"remotevideo",
|
|
"displaymode",
|
|
"screensaver",
|
|
"fileinput"
|
|
];
|
|
|
|
const plugins = [
|
|
'mpvVideoPlayer',
|
|
'mpvAudioPlayer',
|
|
'jmpInputPlugin',
|
|
'jmpUpdatePlugin',
|
|
'skipIntroPlugin'
|
|
];
|
|
|
|
function loadScript(src) {
|
|
return new Promise((resolve, reject) => {
|
|
const s = document.createElement('script');
|
|
s.src = src;
|
|
s.onload = resolve;
|
|
s.onerror = reject;
|
|
document.head.appendChild(s);
|
|
});
|
|
}
|
|
|
|
// Add plugin loaders
|
|
for (const plugin of plugins) {
|
|
window[plugin] = async () => {
|
|
await loadScript(`${jmpInfo.scriptPath}${plugin}.js`);
|
|
return window["_" + plugin];
|
|
};
|
|
}
|
|
|
|
window.NativeShell = {
|
|
openUrl(url, target) {
|
|
window.api.system.openExternalUrl(url);
|
|
},
|
|
|
|
downloadFile(downloadInfo) {
|
|
window.api.system.openExternalUrl(downloadInfo.url);
|
|
},
|
|
|
|
openClientSettings() {
|
|
showSettingsModal();
|
|
},
|
|
|
|
getPlugins() {
|
|
return plugins;
|
|
}
|
|
};
|
|
|
|
function getDeviceProfile() {
|
|
const CodecProfiles = [];
|
|
|
|
if (jmpInfo.settings.video.force_transcode_dovi) {
|
|
CodecProfiles.push({
|
|
'Type': 'Video',
|
|
'Conditions': [
|
|
{
|
|
'Condition': 'NotEquals',
|
|
'Property': 'VideoRangeType',
|
|
'Value': 'DOVI'
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
if (jmpInfo.settings.video.force_transcode_hdr) {
|
|
CodecProfiles.push({
|
|
'Type': 'Video',
|
|
'Conditions': [
|
|
{
|
|
'Condition': 'Equals',
|
|
'Property': 'VideoRangeType',
|
|
'Value': 'SDR'
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
if (jmpInfo.settings.video.force_transcode_hi10p) {
|
|
CodecProfiles.push({
|
|
'Type': 'Video',
|
|
'Conditions': [
|
|
{
|
|
'Condition': 'LessThanEqual',
|
|
'Property': 'VideoBitDepth',
|
|
'Value': '8',
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
if (jmpInfo.settings.video.force_transcode_hevc) {
|
|
CodecProfiles.push({
|
|
'Type': 'Video',
|
|
'Codec': 'hevc',
|
|
'Conditions': [
|
|
{
|
|
'Condition': 'Equals',
|
|
'Property': 'Width',
|
|
'Value': '0',
|
|
}
|
|
],
|
|
});
|
|
CodecProfiles.push({
|
|
'Type': 'Video',
|
|
'Codec': 'h265',
|
|
'Conditions': [
|
|
{
|
|
'Condition': 'Equals',
|
|
'Property': 'Width',
|
|
'Value': '0',
|
|
}
|
|
],
|
|
});
|
|
}
|
|
|
|
if (jmpInfo.settings.video.force_transcode_av1) {
|
|
CodecProfiles.push({
|
|
'Type': 'Video',
|
|
'Codec': 'av1',
|
|
'Conditions': [
|
|
{
|
|
'Condition': 'Equals',
|
|
'Property': 'Width',
|
|
'Value': '0',
|
|
}
|
|
],
|
|
});
|
|
}
|
|
|
|
if (jmpInfo.settings.video.force_transcode_4k) {
|
|
CodecProfiles.push({
|
|
'Type': 'Video',
|
|
'Conditions': [
|
|
{
|
|
'Condition': 'LessThanEqual',
|
|
'Property': 'Width',
|
|
'Value': '1920',
|
|
},
|
|
{
|
|
'Condition': 'LessThanEqual',
|
|
'Property': 'Height',
|
|
'Value': '1080',
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
const DirectPlayProfiles = [{'Type': 'Audio'}, {'Type': 'Photo'}];
|
|
|
|
if (!jmpInfo.settings.video.always_force_transcode) {
|
|
DirectPlayProfiles.push({'Type': 'Video'});
|
|
}
|
|
|
|
return {
|
|
'Name': 'Jellyfin Media Player',
|
|
'MaxStaticBitrate': 1000000000,
|
|
'MusicStreamingTranscodingBitrate': 1280000,
|
|
'TimelineOffsetSeconds': 5,
|
|
'TranscodingProfiles': [
|
|
{'Type': 'Audio'},
|
|
{
|
|
'Container': 'ts',
|
|
'Type': 'Video',
|
|
'Protocol': 'hls',
|
|
'AudioCodec': 'aac,mp3,ac3,opus,vorbis',
|
|
'VideoCodec': jmpInfo.settings.video.allow_transcode_to_hevc
|
|
? (
|
|
jmpInfo.settings.video.prefer_transcode_to_h265
|
|
? 'h265,hevc,h264,mpeg4,mpeg2video'
|
|
: 'h264,h265,hevc,mpeg4,mpeg2video'
|
|
)
|
|
: 'h264,mpeg4,mpeg2video',
|
|
'MaxAudioChannels': jmpInfo.settings.audio.channels === "2.0" ? '2' : '6'
|
|
},
|
|
{'Container': 'jpeg', 'Type': 'Photo'}
|
|
],
|
|
DirectPlayProfiles,
|
|
'ResponseProfiles': [],
|
|
'ContainerProfiles': [],
|
|
CodecProfiles,
|
|
'SubtitleProfiles': [
|
|
{'Format': 'srt', 'Method': 'External'},
|
|
{'Format': 'srt', 'Method': 'Embed'},
|
|
{'Format': 'ass', 'Method': 'External'},
|
|
{'Format': 'ass', 'Method': 'Embed'},
|
|
{'Format': 'sub', 'Method': 'Embed'},
|
|
{'Format': 'sub', 'Method': 'External'},
|
|
{'Format': 'ssa', 'Method': 'Embed'},
|
|
{'Format': 'ssa', 'Method': 'External'},
|
|
{'Format': 'smi', 'Method': 'Embed'},
|
|
{'Format': 'smi', 'Method': 'External'},
|
|
{'Format': 'pgssub', 'Method': 'Embed'},
|
|
{'Format': 'dvdsub', 'Method': 'Embed'},
|
|
{'Format': 'dvbsub', 'Method': 'Embed'},
|
|
{'Format': 'pgs', 'Method': 'Embed'}
|
|
]
|
|
};
|
|
}
|
|
|
|
async function createApi() {
|
|
try {
|
|
// Can't append script until document exists
|
|
await new Promise(resolve => {
|
|
document.addEventListener('DOMContentLoaded', resolve);
|
|
});
|
|
|
|
await loadScript('qrc:///qtwebchannel/qwebchannel.js');
|
|
} catch (e) {
|
|
// try clearing out any cached CSPs
|
|
let foundCache = false;
|
|
for (const cache of await caches.keys()) {
|
|
const dataDeleted = await caches.delete(cache);
|
|
if (dataDeleted) {
|
|
foundCache = true;
|
|
}
|
|
}
|
|
if (foundCache) {
|
|
window.location.reload();
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
const channel = await new Promise((resolve) => {
|
|
/*global QWebChannel */
|
|
new QWebChannel(window.qt.webChannelTransport, resolve);
|
|
});
|
|
return channel.objects;
|
|
}
|
|
|
|
const sectionsFromStorage = window.sessionStorage.getItem('sections');
|
|
if (sectionsFromStorage) {
|
|
jmpInfo.sections = JSON.parse(sectionsFromStorage);
|
|
}
|
|
|
|
let rawSettings = {};
|
|
Object.assign(rawSettings, jmpInfo.settings);
|
|
const settingsFromStorage = window.sessionStorage.getItem('settings');
|
|
if (settingsFromStorage) {
|
|
rawSettings = JSON.parse(settingsFromStorage);
|
|
Object.assign(jmpInfo.settings, rawSettings);
|
|
}
|
|
|
|
const settingsDescriptionsFromStorage = window.sessionStorage.getItem('settingsDescriptions');
|
|
if (settingsDescriptionsFromStorage) {
|
|
jmpInfo.settingsDescriptions = JSON.parse(settingsDescriptionsFromStorage);
|
|
}
|
|
|
|
jmpInfo.settingsDescriptionsUpdate = [];
|
|
jmpInfo.settingsUpdate = [];
|
|
window.apiPromise = createApi();
|
|
window.initCompleted = new Promise(async (resolve) => {
|
|
window.api = await window.apiPromise;
|
|
const settingUpdate = (section, key) => (
|
|
(data) => new Promise(resolve => {
|
|
rawSettings[section][key] = data;
|
|
window.sessionStorage.setItem("settings", JSON.stringify(rawSettings));
|
|
window.api.settings.setValue(section, key, data, resolve);
|
|
})
|
|
);
|
|
const setSetting = (section, key) => {
|
|
Object.defineProperty(jmpInfo.settings[section], key, {
|
|
set: settingUpdate(section, key),
|
|
get: () => rawSettings[section][key]
|
|
});
|
|
};
|
|
for (const settingGroup of Object.keys(rawSettings)) {
|
|
jmpInfo.settings[settingGroup] = {};
|
|
for (const setting of Object.keys(rawSettings[settingGroup])) {
|
|
setSetting(settingGroup, setting, jmpInfo.settings[settingGroup][setting]);
|
|
}
|
|
}
|
|
window.api.settings.sectionValueUpdate.connect(
|
|
(section, data) => {
|
|
Object.assign(rawSettings[section], data);
|
|
for (const callback of jmpInfo.settingsUpdate) {
|
|
try {
|
|
callback(section, data);
|
|
} catch (e) {
|
|
console.error("Update handler failed:", e);
|
|
}
|
|
}
|
|
|
|
// Settings will be outdated if page reloads, so save them to session storage
|
|
window.sessionStorage.setItem("settings", JSON.stringify(rawSettings));
|
|
}
|
|
);
|
|
window.api.settings.groupUpdate.connect(
|
|
(section, data) => {
|
|
jmpInfo.settingsDescriptions[section] = data.settings;
|
|
for (const callback of jmpInfo.settingsDescriptionsUpdate) {
|
|
try {
|
|
callback(section, data);
|
|
} catch (e) {
|
|
console.error("Description update handler failed:", e);
|
|
}
|
|
}
|
|
|
|
// Settings will be outdated if page reloads, so save them to session storage
|
|
window.sessionStorage.setItem("settingsDescriptions", JSON.stringify(jmpInfo.settingsDescriptions));
|
|
}
|
|
);
|
|
resolve();
|
|
});
|
|
|
|
window.NativeShell.AppHost = {
|
|
init() {},
|
|
getDefaultLayout() {
|
|
return jmpInfo.mode;
|
|
},
|
|
supports(command) {
|
|
return features.includes(command.toLowerCase());
|
|
},
|
|
getDeviceProfile,
|
|
getSyncProfile: getDeviceProfile,
|
|
appName() {
|
|
return "Jellyfin Media Player";
|
|
},
|
|
appVersion() {
|
|
return navigator.userAgent.split(" ")[1];
|
|
},
|
|
deviceName() {
|
|
return jmpInfo.deviceName;
|
|
},
|
|
exit() {
|
|
window.api.system.exit();
|
|
}
|
|
};
|
|
|
|
async function showSettingsModal() {
|
|
await initCompleted;
|
|
|
|
const tooltipCSS = `
|
|
.tooltip {
|
|
position: relative;
|
|
display: inline-block;
|
|
margin-left: 0.5rem;
|
|
font-size: 18px;
|
|
vertical-align: sub;
|
|
}
|
|
|
|
.tooltip .tooltip-text {
|
|
visibility: hidden;
|
|
width: max-content;
|
|
max-width: 40em;
|
|
background-color: black;
|
|
color: white;
|
|
text-align: left;
|
|
position: absolute;
|
|
z-index: 1;
|
|
border-radius: 6px;
|
|
padding: 5px;
|
|
top: -4px;
|
|
left: 25px;
|
|
border: solid 1px grey;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.tooltip:hover .tooltip-text {
|
|
visibility: visible;
|
|
}`;
|
|
|
|
var style = document.createElement('style')
|
|
style.innerText = tooltipCSS
|
|
document.head.appendChild(style)
|
|
|
|
const modalContainer = document.createElement("div");
|
|
modalContainer.className = "dialogContainer";
|
|
modalContainer.style.backgroundColor = "rgba(0,0,0,0.5)";
|
|
modalContainer.addEventListener("click", e => {
|
|
if (e.target == modalContainer) {
|
|
modalContainer.remove();
|
|
}
|
|
});
|
|
document.body.appendChild(modalContainer);
|
|
|
|
const modalContainer2 = document.createElement("div");
|
|
modalContainer2.className = "focuscontainer dialog dialog-fixedSize dialog-small formDialog opened";
|
|
modalContainer.appendChild(modalContainer2);
|
|
|
|
const modalHeader = document.createElement("div");
|
|
modalHeader.className = "formDialogHeader";
|
|
modalContainer2.appendChild(modalHeader);
|
|
|
|
const title = document.createElement("h3");
|
|
title.className = "formDialogHeaderTitle";
|
|
title.textContent = "Jellyfin Media Player Settings";
|
|
modalHeader.appendChild(title);
|
|
|
|
const modalContents = document.createElement("div");
|
|
modalContents.className = "formDialogContent smoothScrollY";
|
|
modalContents.style.paddingTop = "2em";
|
|
modalContents.style.marginBottom = "6.2em";
|
|
modalContainer2.appendChild(modalContents);
|
|
|
|
const settingUpdateHandlers = {};
|
|
for (const sectionOrder of jmpInfo.sections.sort((a, b) => a.order - b.order)) {
|
|
const section = sectionOrder.key;
|
|
const group = document.createElement("fieldset");
|
|
group.className = "editItemMetadataForm editMetadataForm dialog-content-centered";
|
|
group.style.border = 0;
|
|
group.style.outline = 0;
|
|
modalContents.appendChild(group);
|
|
|
|
const createSection = async (clear) => {
|
|
if (clear) {
|
|
group.innerHTML = "";
|
|
}
|
|
|
|
const values = jmpInfo.settings[section];
|
|
const settings = jmpInfo.settingsDescriptions[section];
|
|
|
|
const legend = document.createElement("legend");
|
|
const legendHeader = document.createElement("h2");
|
|
legendHeader.textContent = section;
|
|
legendHeader.style.textTransform = "capitalize";
|
|
legend.appendChild(legendHeader);
|
|
if (section == "plugins") {
|
|
const legendSubHeader = document.createElement("h4");
|
|
legendSubHeader.textContent = "Plugins are UNOFFICIAL and require a restart to take effect.";
|
|
legend.appendChild(legendSubHeader);
|
|
} else if (section == "other") {
|
|
const legendSubHeader = document.createElement("h4");
|
|
legendSubHeader.textContent = "Use this section to input custom MPV configuration. These will override the above settings.";
|
|
legend.appendChild(legendSubHeader);
|
|
}
|
|
group.appendChild(legend);
|
|
|
|
for (const setting of settings) {
|
|
const label = document.createElement("label");
|
|
label.className = "inputContainer";
|
|
label.style.marginBottom = "1.8em";
|
|
label.style.display = "block";
|
|
|
|
let helpElement;
|
|
if (setting.help) {
|
|
helpElement = document.createElement("div");
|
|
helpElement.className = "tooltip";
|
|
const helpIcon = document.createElement("span");
|
|
helpIcon.style.fontSize = "18px"
|
|
helpIcon.className = "material-icons help_outline";
|
|
helpElement.appendChild(helpIcon);
|
|
const tooltipElement = document.createElement("span");
|
|
tooltipElement.className = "tooltip-text";
|
|
tooltipElement.innerText = setting.help;
|
|
helpElement.appendChild(tooltipElement);
|
|
}
|
|
|
|
if (setting.options) {
|
|
const safeValues = {};
|
|
const control = document.createElement("select");
|
|
control.className = "emby-select-withcolor emby-select";
|
|
for (const option of setting.options) {
|
|
safeValues[String(option.value)] = option.value;
|
|
const opt = document.createElement("option");
|
|
opt.value = option.value;
|
|
opt.selected = option.value == values[setting.key];
|
|
let optionName = option.title;
|
|
const swTest = `${section}.${setting.key}.`;
|
|
const swTest2 = `${section}.`;
|
|
if (optionName.startsWith(swTest)) {
|
|
optionName = optionName.substring(swTest.length);
|
|
} else if (optionName.startsWith(swTest2)) {
|
|
optionName = optionName.substring(swTest2.length);
|
|
}
|
|
opt.appendChild(document.createTextNode(optionName));
|
|
control.appendChild(opt);
|
|
}
|
|
control.addEventListener("change", async (e) => {
|
|
jmpInfo.settings[section][setting.key] = safeValues[e.target.value];
|
|
});
|
|
const labelText = document.createElement('label');
|
|
labelText.className = "inputLabel";
|
|
labelText.textContent = (setting.displayName ? setting.displayName : setting.key) + ": ";
|
|
label.appendChild(labelText);
|
|
if (helpElement) label.appendChild(helpElement);
|
|
label.appendChild(control);
|
|
} else if (setting.inputType === "textarea") {
|
|
const control = document.createElement("textarea");
|
|
control.className = "emby-select-withcolor emby-select";
|
|
control.style = "resize: none;"
|
|
control.value = values[setting.key];
|
|
control.rows = 5;
|
|
control.addEventListener("change", e =>
|
|
{
|
|
jmpInfo.settings[section][setting.key] = e.target.value;
|
|
});
|
|
const labelText = document.createElement('label');
|
|
labelText.className = "inputLabel";
|
|
labelText.textContent = (setting.displayName ? setting.displayName : setting.key) + ": ";
|
|
label.appendChild(labelText);
|
|
if (helpElement) label.appendChild(helpElement);
|
|
label.appendChild(control);
|
|
} else {
|
|
const control = document.createElement("input");
|
|
control.type = "checkbox";
|
|
control.checked = values[setting.key];
|
|
control.addEventListener("change", e => {
|
|
jmpInfo.settings[section][setting.key] = e.target.checked;
|
|
});
|
|
label.appendChild(control);
|
|
label.appendChild(document.createTextNode(" " + (setting.displayName ? setting.displayName : setting.key)));
|
|
if (helpElement) label.appendChild(helpElement);
|
|
}
|
|
|
|
group.appendChild(label);
|
|
}
|
|
};
|
|
settingUpdateHandlers[section] = () => createSection(true);
|
|
createSection();
|
|
}
|
|
|
|
const onSectionUpdate = (section) => {
|
|
if (section in settingUpdateHandlers) {
|
|
settingUpdateHandlers[section]();
|
|
}
|
|
};
|
|
jmpInfo.settingsDescriptionsUpdate.push(onSectionUpdate);
|
|
jmpInfo.settingsUpdate.push(onSectionUpdate);
|
|
|
|
if (jmpInfo.settings.main.userWebClient) {
|
|
const group = document.createElement("fieldset");
|
|
group.className = "editItemMetadataForm editMetadataForm dialog-content-centered";
|
|
group.style.border = 0;
|
|
group.style.outline = 0;
|
|
modalContents.appendChild(group);
|
|
const legend = document.createElement("legend");
|
|
const legendHeader = document.createElement("h2");
|
|
legendHeader.textContent = "Saved Server";
|
|
legend.appendChild(legendHeader);
|
|
const legendSubHeader = document.createElement("h4");
|
|
legendSubHeader.textContent = (
|
|
"The server you first connected to is your saved server. " +
|
|
"It provides the web client for Jellyfin Media Player in the absence of a bundled one. " +
|
|
"You can use this option to change it to another one. This does NOT log you off."
|
|
);
|
|
legend.appendChild(legendSubHeader);
|
|
group.appendChild(legend);
|
|
|
|
const resetSavedServer = document.createElement("button");
|
|
resetSavedServer.className = "raised button-cancel block btnCancel emby-button";
|
|
resetSavedServer.textContent = "Reset Saved Server"
|
|
resetSavedServer.style.marginLeft = "auto";
|
|
resetSavedServer.style.marginRight = "auto";
|
|
resetSavedServer.style.maxWidth = "50%";
|
|
resetSavedServer.addEventListener("click", async () => {
|
|
window.jmpInfo.settings.main.userWebClient = '';
|
|
window.location.href = jmpInfo.scriptPath + "/find-webclient.html";
|
|
});
|
|
group.appendChild(resetSavedServer);
|
|
}
|
|
|
|
const closeContainer = document.createElement("div");
|
|
closeContainer.className = "formDialogFooter";
|
|
modalContents.appendChild(closeContainer);
|
|
|
|
const close = document.createElement("button");
|
|
close.className = "raised button-cancel block btnCancel formDialogFooterItem emby-button";
|
|
close.textContent = "Close"
|
|
close.addEventListener("click", () => {
|
|
modalContainer.remove();
|
|
});
|
|
closeContainer.appendChild(close);
|
|
} |