Add extra transcoder settings and make global reactive settings object.

This commit is contained in:
Ian Walton committed 2023-03-12 10:21:25 -04:00
1 parent 7a0f6f903f
commit 43e068c5f7
7 files changed
+244 -99

No files matched your search

+14 -15
View File
@@ -2,13 +2,7 @@
## Detection
You can detect JMP by querying the injected utilities script:
```js
window?.NativeShell?.AppHost?.appName() === "Jellyfin Media Player"
```
This is not async. It can be queried immediately.
You can detect JMP by looking for the object `window.jmpInfo`, which is defined immediately.
## Device Profile
@@ -28,7 +22,7 @@ The following functions are available immediately:
- `window.NativeShell.AppHost.appVersion()` - Returns version number in `x.y.z` format.
- `window.NativeShell.AppHost.deviceName()` - Returns computer hostname.
The following functions require calling `window.NativeShell.AppHost.init()` before using them:
The following functions are available after less than a second:
- `window.NativeShell.openUrl(url)` - Opens a URL in the user's browser.
- `window.NativeShell.openClientSettings()` - Opens prebuilt settings modal.
@@ -210,7 +204,17 @@ Current Settings:
- `size: int`: Controls subtitle size. Default is `32`.
- Provided options: [see enum](https://github.com/jellyfin/jellyfin-media-player/blob/7d5943becc1ca672d599887cac9107836c38d337/resources/settings/settings_description.json#L376-L382)
API methods:
The global `window.jmpInfo` object contains settings for the application in the form of `window.jmpInfo.settings.[section][key] = value`.
Settings descriptions are stored in the form `window.jmpInfo.settingsDescriptions.[section] = [{ key, options }]`
- These are kept up-to-date in response to user changes.
- If you want to change any settings, `await window.initCompleted` first.
- To change a setting, simply set the setting to a new value. Don't overwrite an entire section.
- You can subscribe to settings changes by adding a `callback(section, changes)` to `window.jmpInfo.settingsUpdate`.
- Similarly, you can subscribe to settings description updates by adding a `callback(section, changes)` to `window.jmpInfo.settingsDescriptionsUpdate`.
- This is useful for reactive settings, for instance when you change the audio device type.
You can also use the API to set and query settings. This is largely not needed unless you want to be sure a setting change is complete before you do something:
- `api.settings.settingDescriptions(callback)`: Get list of all setting descriptions and options.
- You may need to re-query this if you update certain settings.
@@ -218,13 +222,8 @@ API methods:
- `api.settings.value(section, key, callback)`: Get a specific setting.
- `api.settings.setValue(section, key, value, callback)`: Set a specific setting.
You can also listen for setting and setting group option updates. I don't currently use this, but you can see
details of how to do that [from the signals here](https://github.com/jellyfin/jellyfin-media-player/blob/7d5943becc1ca672d599887cac9107836c38d337/src/settings/SettingsComponent.h#L76-L83).
## Checking for Updates
You need to call `window.NativeShell.AppHost.init()` before you can use this.
```js
const updatePlugin = await window.jmpUpdatePlugin();
updatePlugin({
@@ -261,7 +260,7 @@ async function createApi() {
```
The injected utilities script makes `window.apiPromise` available immediately and `window.api` available
after less than a second available if you call `window.NativeShell.AppHost.init()`.
after less than a second.
### Client API Usage
+4 -29
View File
@@ -1,24 +1,3 @@
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);
});
}
async function createApi() {
await loadScript('qrc:///qtwebchannel/qwebchannel.js');
const channel = await new Promise((resolve) => {
/*global QWebChannel */
new QWebChannel(window.qt.webChannelTransport, resolve);
});
return channel.objects;
}
window.apiPromise = createApi();
async function tryConnect(server) {
document.getElementById('connect-button').disabled = true;
@@ -70,10 +49,9 @@ async function tryConnect(server) {
window.location = server;
}
const api = await window.apiPromise;
await new Promise(resolve => {
api.settings.setValue('main', 'userWebClient', server, resolve);
});
await window.initCompleted;
window.jmpInfo.settings.main.userWebClient = server;
return true;
}
} catch (e) {
@@ -99,10 +77,7 @@ document.getElementById('connect-fail-button').addEventListener('click', () => {
// load the server if we have one
(async() => {
const api = await window.apiPromise;
const savedServer = await new Promise(resolve => {
api.settings.value('main', 'userWebClient', resolve);
});
const savedServer = window.jmpInfo.settings.main.userWebClient;
if (!savedServer || !(await tryConnect(savedServer))) {
document.getElementById('splash').style.display = 'none';
+3 -4
View File
@@ -5,10 +5,9 @@ class jellyscrubPlugin {
this.id = 'jellyscrubPlugin';
(async() => {
const api = await window.apiPromise;
const enabled = await new Promise(resolve => {
api.settings.value('plugins', 'jellyscrub', resolve);
});
await window.initCompleted;
const enabled = window.jmpInfo.settings.plugins.jellyscrub;
console.log("JellyScrub Plugin enabled: " + enabled);
if (!enabled) return;
+188 -45
View File
@@ -1,4 +1,5 @@
const viewdata = JSON.parse(window.atob("@@data@@"));
const jmpInfo = JSON.parse(window.atob("@@data@@"));
window.jmpInfo = jmpInfo;
const features = [
"filedownload",
@@ -40,7 +41,7 @@ function loadScript(src) {
// Add plugin loaders
for (const plugin of plugins) {
window[plugin] = async () => {
await loadScript(`${viewdata.scriptPath}${plugin}.js`);
await loadScript(`${jmpInfo.scriptPath}${plugin}.js`);
return window["_" + plugin];
};
}
@@ -77,7 +78,7 @@ function getDeviceProfile() {
}
];
if (viewdata.force_transcode_hdr) {
if (jmpInfo.settings.video.force_transcode_hdr) {
CodecProfiles.push({
'Type': 'Video',
'Conditions': [
@@ -90,6 +91,82 @@ function getDeviceProfile() {
});
}
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,
@@ -102,14 +179,18 @@ function getDeviceProfile() {
'Type': 'Video',
'Protocol': 'hls',
'AudioCodec': 'aac,mp3,ac3,opus,flac,vorbis',
'VideoCodec': viewdata.allow_transcode_to_hevc
? 'h264,h265,hevc,mpeg4,mpeg2video'
'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': '6'
},
{'Container': 'jpeg', 'Type': 'Photo'}
],
'DirectPlayProfiles': [{'Type': 'Video'}, {'Type': 'Audio'}, {'Type': 'Photo'}],
DirectPlayProfiles,
'ResponseProfiles': [],
'ContainerProfiles': [],
CodecProfiles,
@@ -134,6 +215,11 @@ function getDeviceProfile() {
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
@@ -157,15 +243,80 @@ async function createApi() {
return channel.objects;
}
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() {
window.apiPromise = createApi();
(async () => {
window.api = await window.apiPromise;
})();
},
init() {},
getDefaultLayout() {
return viewdata.mode;
return jmpInfo.mode;
},
supports(command) {
return features.includes(command.toLowerCase());
@@ -179,7 +330,7 @@ window.NativeShell.AppHost = {
return navigator.userAgent.split(" ")[1];
},
deviceName() {
return viewdata.deviceName;
return jmpInfo.deviceName;
},
exit() {
window.api.system.exit();
@@ -187,9 +338,7 @@ window.NativeShell.AppHost = {
};
async function showSettingsModal() {
let settings = await new Promise(resolve => {
window.api.settings.settingDescriptions(resolve);
});
await initCompleted;
const modalContainer = document.createElement("div");
modalContainer.className = "dialogContainer";
@@ -220,7 +369,8 @@ async function showSettingsModal() {
modalContents.style.marginBottom = "6.2em";
modalContainer2.appendChild(modalContents);
for (let section of settings) {
const settingUpdateHandlers = {};
for (const section of Object.keys(jmpInfo.settingsDescriptions)) {
const group = document.createElement("fieldset");
group.className = "editItemMetadataForm editMetadataForm dialog-content-centered";
group.style.border = 0;
@@ -232,23 +382,22 @@ async function showSettingsModal() {
group.innerHTML = "";
}
const values = await new Promise(resolve => {
window.api.settings.allValues(section.key, resolve);
});
const values = jmpInfo.settings[section];
const settings = jmpInfo.settingsDescriptions[section];
const legend = document.createElement("legend");
const legendHeader = document.createElement("h2");
legendHeader.textContent = section.key;
legendHeader.textContent = section;
legendHeader.style.textTransform = "capitalize";
legend.appendChild(legendHeader);
if (section.key == "plugins") {
if (section == "plugins") {
const legendSubHeader = document.createElement("h4");
legendSubHeader.textContent = "Plugins are UNOFFICIAL and require a restart to take effect.";
legend.appendChild(legendSubHeader);
}
group.appendChild(legend);
for (const setting of section.settings) {
for (const setting of settings) {
const label = document.createElement("label");
label.className = "inputContainer";
label.style.marginBottom = "1.8em";
@@ -264,8 +413,8 @@ async function showSettingsModal() {
opt.value = option.value;
opt.selected = option.value == values[setting.key];
let optionName = option.title;
const swTest = `${section.key}.${setting.key}.`;
const swTest2 = `${section.key}.`;
const swTest = `${section}.${setting.key}.`;
const swTest2 = `${section}.`;
if (optionName.startsWith(swTest)) {
optionName = optionName.substring(swTest.length);
} else if (optionName.startsWith(swTest2)) {
@@ -275,16 +424,7 @@ async function showSettingsModal() {
control.appendChild(opt);
}
control.addEventListener("change", async (e) => {
await new Promise(resolve => {
window.api.settings.setValue(section.key, setting.key, safeValues[e.target.value], resolve);
});
if (setting.key == "devicetype") {
section = (await new Promise(resolve => {
window.api.settings.settingDescriptions(resolve);
})).filter(x => x.key == section.key)[0];
createSection(true);
}
jmpInfo.settings[section][setting.key] = safeValues[e.target.value];
});
const labelText = document.createElement('label');
labelText.className = "inputLabel";
@@ -296,7 +436,7 @@ async function showSettingsModal() {
control.type = "checkbox";
control.checked = values[setting.key];
control.addEventListener("change", e => {
window.api.settings.setValue(section.key, setting.key, e.target.checked);
jmpInfo.settings[section][setting.key] = e.target.checked;
});
label.appendChild(control);
label.appendChild(document.createTextNode(" " + setting.key));
@@ -304,14 +444,19 @@ async function showSettingsModal() {
group.appendChild(label);
}
};
settingUpdateHandlers[section] = () => createSection(true);
createSection();
}
const savedServer = await new Promise(resolve => {
window.api.settings.value('main', 'userWebClient', resolve);
});
const onSectionUpdate = (section) => {
if (section in settingUpdateHandlers) {
settingUpdateHandlers[section]();
}
};
jmpInfo.settingsDescriptionsUpdate.push(onSectionUpdate);
jmpInfo.settingsUpdate.push(onSectionUpdate);
if (savedServer) {
if (jmpInfo.settings.main.userWebClient) {
const group = document.createElement("fieldset");
group.className = "editItemMetadataForm editMetadataForm dialog-content-centered";
group.style.border = 0;
@@ -337,10 +482,8 @@ async function showSettingsModal() {
resetSavedServer.style.marginRight = "auto";
resetSavedServer.style.maxWidth = "50%";
resetSavedServer.addEventListener("click", async () => {
await new Promise(resolve => {
window.api.settings.setValue('main', 'userWebClient', '', resolve);
});
window.location.href = viewdata.scriptPath + "/find-webclient.html";
window.jmpInfo.settings.main.userWebClient = '';
window.location.href = jmpInfo.scriptPath + "/find-webclient.html";
});
group.appendChild(resetSavedServer);
}
+3 -4
View File
@@ -7,10 +7,9 @@ class skipIntroPlugin {
this.id = 'skipIntroPlugin';
(async() => {
const api = await window.apiPromise;
const enabled = await new Promise(resolve => {
api.settings.value('plugins', 'skipintro', resolve);
});
await window.initCompleted;
const enabled = window.jmpInfo.settings.plugins.skipintro;
console.log("Skip Intro Plugin enabled: " + enabled);
if (!enabled) return;
@@ -268,6 +268,30 @@
"value": "force_transcode_hdr",
"default": false
},
{
"value": "prefer_transcode_to_h265",
"default": false
},
{
"value": "force_transcode_hi10p",
"default": false
},
{
"value": "force_transcode_hevc",
"default": false
},
{
"value": "force_transcode_av1",
"default": false
},
{
"value": "force_transcode_4k",
"default": false
},
{
"value": "always_force_transcode",
"default": false
},
{
"value": "sync_mode",
"default": "audio",
+8 -2
View File
@@ -331,8 +331,14 @@ QString SystemComponent::getNativeShellScript()
clientData.insert("deviceName", QJsonValue::fromVariant(SettingsComponent::Get().getClientName()));
clientData.insert("scriptPath", QJsonValue::fromVariant("file:///" + path));
clientData.insert("mode", QJsonValue::fromVariant(SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "layout").toString()));
clientData.insert("allow_transcode_to_hevc", QJsonValue::fromVariant(SettingsComponent::Get().value(SETTINGS_SECTION_VIDEO, "allow_transcode_to_hevc").toBool()));
clientData.insert("force_transcode_hdr", QJsonValue::fromVariant(SettingsComponent::Get().value(SETTINGS_SECTION_VIDEO, "force_transcode_hdr").toBool()));
QVariantList settingsDescriptionsList = SettingsComponent::Get().settingDescriptions();
QVariantMap settingsDescriptions = QVariantMap();
for (auto setting : settingsDescriptionsList) {
QVariantMap settingMap = setting.toMap();
settingsDescriptions.insert(settingMap["key"].toString(), settingMap["settings"]);
}
clientData.insert("settingsDescriptions", QJsonValue::fromVariant(settingsDescriptions));
clientData.insert("settings", QJsonValue::fromVariant(SettingsComponent::Get().allValues()));
nativeshellString.replace("@@data@@", QJsonDocument(clientData).toJson(QJsonDocument::Compact).toBase64());
return nativeshellString;
}