nativeshell.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. const jmpInfo = JSON.parse(window.atob("@@data@@"));
  2. window.jmpInfo = jmpInfo;
  3. const features = [
  4. "filedownload",
  5. "displaylanguage",
  6. "htmlaudioautoplay",
  7. "htmlvideoautoplay",
  8. "externallinks",
  9. "clientsettings",
  10. "multiserver",
  11. "exitmenu",
  12. "remotecontrol",
  13. "fullscreenchange",
  14. "filedownload",
  15. "remotevideo",
  16. "displaymode",
  17. "screensaver",
  18. "fileinput"
  19. ];
  20. const plugins = [
  21. 'mpvVideoPlayer',
  22. 'mpvAudioPlayer',
  23. 'jmpInputPlugin',
  24. 'jmpUpdatePlugin',
  25. 'jellyscrubPlugin',
  26. 'skipIntroPlugin'
  27. ];
  28. function loadScript(src) {
  29. return new Promise((resolve, reject) => {
  30. const s = document.createElement('script');
  31. s.src = src;
  32. s.onload = resolve;
  33. s.onerror = reject;
  34. document.head.appendChild(s);
  35. });
  36. }
  37. // Add plugin loaders
  38. for (const plugin of plugins) {
  39. window[plugin] = async () => {
  40. await loadScript(`${jmpInfo.scriptPath}${plugin}.js`);
  41. return window["_" + plugin];
  42. };
  43. }
  44. window.NativeShell = {
  45. openUrl(url, target) {
  46. window.api.system.openExternalUrl(url);
  47. },
  48. downloadFile(downloadInfo) {
  49. window.api.system.openExternalUrl(downloadInfo.url);
  50. },
  51. openClientSettings() {
  52. showSettingsModal();
  53. },
  54. getPlugins() {
  55. return plugins;
  56. }
  57. };
  58. function getDeviceProfile() {
  59. const CodecProfiles = [
  60. {
  61. 'Type': 'Video',
  62. 'Conditions': [
  63. {
  64. 'Condition': 'NotEquals',
  65. 'Property': 'VideoRangeType',
  66. 'Value': 'DOVI'
  67. }
  68. ]
  69. }
  70. ];
  71. if (jmpInfo.settings.video.force_transcode_hdr) {
  72. CodecProfiles.push({
  73. 'Type': 'Video',
  74. 'Conditions': [
  75. {
  76. 'Condition': 'Equals',
  77. 'Property': 'VideoRangeType',
  78. 'Value': 'SDR'
  79. }
  80. ]
  81. });
  82. }
  83. if (jmpInfo.settings.video.force_transcode_hi10p) {
  84. CodecProfiles.push({
  85. 'Type': 'Video',
  86. 'Conditions': [
  87. {
  88. 'Condition': 'LessThanEqual',
  89. 'Property': 'VideoBitDepth',
  90. 'Value': '8',
  91. }
  92. ]
  93. });
  94. }
  95. if (jmpInfo.settings.video.force_transcode_hevc) {
  96. CodecProfiles.push({
  97. 'Type': 'Video',
  98. 'Codec': 'hevc',
  99. 'Conditions': [
  100. {
  101. 'Condition': 'Equals',
  102. 'Property': 'Width',
  103. 'Value': '0',
  104. }
  105. ],
  106. });
  107. CodecProfiles.push({
  108. 'Type': 'Video',
  109. 'Codec': 'h265',
  110. 'Conditions': [
  111. {
  112. 'Condition': 'Equals',
  113. 'Property': 'Width',
  114. 'Value': '0',
  115. }
  116. ],
  117. });
  118. }
  119. if (jmpInfo.settings.video.force_transcode_av1) {
  120. CodecProfiles.push({
  121. 'Type': 'Video',
  122. 'Codec': 'av1',
  123. 'Conditions': [
  124. {
  125. 'Condition': 'Equals',
  126. 'Property': 'Width',
  127. 'Value': '0',
  128. }
  129. ],
  130. });
  131. }
  132. if (jmpInfo.settings.video.force_transcode_4k) {
  133. CodecProfiles.push({
  134. 'Type': 'Video',
  135. 'Conditions': [
  136. {
  137. 'Condition': 'LessThanEqual',
  138. 'Property': 'Width',
  139. 'Value': '1920',
  140. },
  141. {
  142. 'Condition': 'LessThanEqual',
  143. 'Property': 'Height',
  144. 'Value': '1080',
  145. }
  146. ]
  147. });
  148. }
  149. const DirectPlayProfiles = [{'Type': 'Audio'}, {'Type': 'Photo'}];
  150. if (!jmpInfo.settings.video.always_force_transcode) {
  151. DirectPlayProfiles.push({'Type': 'Video'});
  152. }
  153. return {
  154. 'Name': 'Jellyfin Media Player',
  155. 'MaxStaticBitrate': 1000000000,
  156. 'MusicStreamingTranscodingBitrate': 1280000,
  157. 'TimelineOffsetSeconds': 5,
  158. 'TranscodingProfiles': [
  159. {'Type': 'Audio'},
  160. {
  161. 'Container': 'ts',
  162. 'Type': 'Video',
  163. 'Protocol': 'hls',
  164. 'AudioCodec': 'aac,mp3,ac3,opus,flac,vorbis',
  165. 'VideoCodec': jmpInfo.settings.video.allow_transcode_to_hevc
  166. ? (
  167. jmpInfo.settings.video.prefer_transcode_to_h265
  168. ? 'h265,hevc,h264,mpeg4,mpeg2video'
  169. : 'h264,h265,hevc,mpeg4,mpeg2video'
  170. )
  171. : 'h264,mpeg4,mpeg2video',
  172. 'MaxAudioChannels': '6'
  173. },
  174. {'Container': 'jpeg', 'Type': 'Photo'}
  175. ],
  176. DirectPlayProfiles,
  177. 'ResponseProfiles': [],
  178. 'ContainerProfiles': [],
  179. CodecProfiles,
  180. 'SubtitleProfiles': [
  181. {'Format': 'srt', 'Method': 'External'},
  182. {'Format': 'srt', 'Method': 'Embed'},
  183. {'Format': 'ass', 'Method': 'External'},
  184. {'Format': 'ass', 'Method': 'Embed'},
  185. {'Format': 'sub', 'Method': 'Embed'},
  186. {'Format': 'sub', 'Method': 'External'},
  187. {'Format': 'ssa', 'Method': 'Embed'},
  188. {'Format': 'ssa', 'Method': 'External'},
  189. {'Format': 'smi', 'Method': 'Embed'},
  190. {'Format': 'smi', 'Method': 'External'},
  191. {'Format': 'pgssub', 'Method': 'Embed'},
  192. {'Format': 'dvdsub', 'Method': 'Embed'},
  193. {'Format': 'dvbsub', 'Method': 'Embed'},
  194. {'Format': 'pgs', 'Method': 'Embed'}
  195. ]
  196. };
  197. }
  198. async function createApi() {
  199. try {
  200. // Can't append script until document exists
  201. await new Promise(resolve => {
  202. document.addEventListener('DOMContentLoaded', resolve);
  203. });
  204. await loadScript('qrc:///qtwebchannel/qwebchannel.js');
  205. } catch (e) {
  206. // try clearing out any cached CSPs
  207. let foundCache = false;
  208. for (const cache of await caches.keys()) {
  209. const dataDeleted = await caches.delete(cache);
  210. if (dataDeleted) {
  211. foundCache = true;
  212. }
  213. }
  214. if (foundCache) {
  215. window.location.reload();
  216. }
  217. throw e;
  218. }
  219. const channel = await new Promise((resolve) => {
  220. /*global QWebChannel */
  221. new QWebChannel(window.qt.webChannelTransport, resolve);
  222. });
  223. return channel.objects;
  224. }
  225. let rawSettings = {};
  226. Object.assign(rawSettings, jmpInfo.settings);
  227. const settingsFromStorage = window.sessionStorage.getItem('settings');
  228. if (settingsFromStorage) {
  229. rawSettings = JSON.parse(settingsFromStorage);
  230. Object.assign(jmpInfo.settings, rawSettings);
  231. }
  232. const settingsDescriptionsFromStorage = window.sessionStorage.getItem('settingsDescriptions');
  233. if (settingsDescriptionsFromStorage) {
  234. jmpInfo.settingsDescriptions = JSON.parse(settingsDescriptionsFromStorage);
  235. }
  236. jmpInfo.settingsDescriptionsUpdate = [];
  237. jmpInfo.settingsUpdate = [];
  238. window.apiPromise = createApi();
  239. window.initCompleted = new Promise(async (resolve) => {
  240. window.api = await window.apiPromise;
  241. const settingUpdate = (section, key) => (
  242. (data) => new Promise(resolve => {
  243. rawSettings[section][key] = data;
  244. window.sessionStorage.setItem("settings", JSON.stringify(rawSettings));
  245. window.api.settings.setValue(section, key, data, resolve);
  246. })
  247. );
  248. const setSetting = (section, key) => {
  249. Object.defineProperty(jmpInfo.settings[section], key, {
  250. set: settingUpdate(section, key),
  251. get: () => rawSettings[section][key]
  252. });
  253. };
  254. for (const settingGroup of Object.keys(rawSettings)) {
  255. jmpInfo.settings[settingGroup] = {};
  256. for (const setting of Object.keys(rawSettings[settingGroup])) {
  257. setSetting(settingGroup, setting, jmpInfo.settings[settingGroup][setting]);
  258. }
  259. }
  260. window.api.settings.sectionValueUpdate.connect(
  261. (section, data) => {
  262. Object.assign(rawSettings[section], data);
  263. for (const callback of jmpInfo.settingsUpdate) {
  264. try {
  265. callback(section, data);
  266. } catch (e) {
  267. console.error("Update handler failed:", e);
  268. }
  269. }
  270. // Settings will be outdated if page reloads, so save them to session storage
  271. window.sessionStorage.setItem("settings", JSON.stringify(rawSettings));
  272. }
  273. );
  274. window.api.settings.groupUpdate.connect(
  275. (section, data) => {
  276. jmpInfo.settingsDescriptions[section] = data.settings;
  277. for (const callback of jmpInfo.settingsDescriptionsUpdate) {
  278. try {
  279. callback(section, data);
  280. } catch (e) {
  281. console.error("Description update handler failed:", e);
  282. }
  283. }
  284. // Settings will be outdated if page reloads, so save them to session storage
  285. window.sessionStorage.setItem("settingsDescriptions", JSON.stringify(jmpInfo.settingsDescriptions));
  286. }
  287. );
  288. resolve();
  289. });
  290. window.NativeShell.AppHost = {
  291. init() {},
  292. getDefaultLayout() {
  293. return jmpInfo.mode;
  294. },
  295. supports(command) {
  296. return features.includes(command.toLowerCase());
  297. },
  298. getDeviceProfile,
  299. getSyncProfile: getDeviceProfile,
  300. appName() {
  301. return "Jellyfin Media Player";
  302. },
  303. appVersion() {
  304. return navigator.userAgent.split(" ")[1];
  305. },
  306. deviceName() {
  307. return jmpInfo.deviceName;
  308. },
  309. exit() {
  310. window.api.system.exit();
  311. }
  312. };
  313. async function showSettingsModal() {
  314. await initCompleted;
  315. const modalContainer = document.createElement("div");
  316. modalContainer.className = "dialogContainer";
  317. modalContainer.style.backgroundColor = "rgba(0,0,0,0.5)";
  318. modalContainer.addEventListener("click", e => {
  319. if (e.target == modalContainer) {
  320. modalContainer.remove();
  321. }
  322. });
  323. document.body.appendChild(modalContainer);
  324. const modalContainer2 = document.createElement("div");
  325. modalContainer2.className = "focuscontainer dialog dialog-fixedSize dialog-small formDialog opened";
  326. modalContainer.appendChild(modalContainer2);
  327. const modalHeader = document.createElement("div");
  328. modalHeader.className = "formDialogHeader";
  329. modalContainer2.appendChild(modalHeader);
  330. const title = document.createElement("h3");
  331. title.className = "formDialogHeaderTitle";
  332. title.textContent = "Jellyfin Media Player Settings";
  333. modalHeader.appendChild(title);
  334. const modalContents = document.createElement("div");
  335. modalContents.className = "formDialogContent smoothScrollY";
  336. modalContents.style.paddingTop = "2em";
  337. modalContents.style.marginBottom = "6.2em";
  338. modalContainer2.appendChild(modalContents);
  339. const settingUpdateHandlers = {};
  340. for (const section of Object.keys(jmpInfo.settingsDescriptions)) {
  341. const group = document.createElement("fieldset");
  342. group.className = "editItemMetadataForm editMetadataForm dialog-content-centered";
  343. group.style.border = 0;
  344. group.style.outline = 0;
  345. modalContents.appendChild(group);
  346. const createSection = async (clear) => {
  347. if (clear) {
  348. group.innerHTML = "";
  349. }
  350. const values = jmpInfo.settings[section];
  351. const settings = jmpInfo.settingsDescriptions[section];
  352. const legend = document.createElement("legend");
  353. const legendHeader = document.createElement("h2");
  354. legendHeader.textContent = section;
  355. legendHeader.style.textTransform = "capitalize";
  356. legend.appendChild(legendHeader);
  357. if (section == "plugins") {
  358. const legendSubHeader = document.createElement("h4");
  359. legendSubHeader.textContent = "Plugins are UNOFFICIAL and require a restart to take effect.";
  360. legend.appendChild(legendSubHeader);
  361. }
  362. group.appendChild(legend);
  363. for (const setting of settings) {
  364. const label = document.createElement("label");
  365. label.className = "inputContainer";
  366. label.style.marginBottom = "1.8em";
  367. label.style.display = "block";
  368. label.style.textTransform = "capitalize";
  369. if (setting.options) {
  370. const safeValues = {};
  371. const control = document.createElement("select");
  372. control.className = "emby-select-withcolor emby-select";
  373. for (const option of setting.options) {
  374. safeValues[String(option.value)] = option.value;
  375. const opt = document.createElement("option");
  376. opt.value = option.value;
  377. opt.selected = option.value == values[setting.key];
  378. let optionName = option.title;
  379. const swTest = `${section}.${setting.key}.`;
  380. const swTest2 = `${section}.`;
  381. if (optionName.startsWith(swTest)) {
  382. optionName = optionName.substring(swTest.length);
  383. } else if (optionName.startsWith(swTest2)) {
  384. optionName = optionName.substring(swTest2.length);
  385. }
  386. opt.appendChild(document.createTextNode(optionName));
  387. control.appendChild(opt);
  388. }
  389. control.addEventListener("change", async (e) => {
  390. jmpInfo.settings[section][setting.key] = safeValues[e.target.value];
  391. });
  392. const labelText = document.createElement('label');
  393. labelText.className = "inputLabel";
  394. labelText.textContent = setting.key + ": ";
  395. label.appendChild(labelText);
  396. label.appendChild(control);
  397. } else {
  398. const control = document.createElement("input");
  399. control.type = "checkbox";
  400. control.checked = values[setting.key];
  401. control.addEventListener("change", e => {
  402. jmpInfo.settings[section][setting.key] = e.target.checked;
  403. });
  404. label.appendChild(control);
  405. label.appendChild(document.createTextNode(" " + setting.key));
  406. }
  407. group.appendChild(label);
  408. }
  409. };
  410. settingUpdateHandlers[section] = () => createSection(true);
  411. createSection();
  412. }
  413. const onSectionUpdate = (section) => {
  414. if (section in settingUpdateHandlers) {
  415. settingUpdateHandlers[section]();
  416. }
  417. };
  418. jmpInfo.settingsDescriptionsUpdate.push(onSectionUpdate);
  419. jmpInfo.settingsUpdate.push(onSectionUpdate);
  420. if (jmpInfo.settings.main.userWebClient) {
  421. const group = document.createElement("fieldset");
  422. group.className = "editItemMetadataForm editMetadataForm dialog-content-centered";
  423. group.style.border = 0;
  424. group.style.outline = 0;
  425. modalContents.appendChild(group);
  426. const legend = document.createElement("legend");
  427. const legendHeader = document.createElement("h2");
  428. legendHeader.textContent = "Saved Server";
  429. legend.appendChild(legendHeader);
  430. const legendSubHeader = document.createElement("h4");
  431. legendSubHeader.textContent = (
  432. "The server you first connected to is your saved server. " +
  433. "It provides the web client for Jellyfin Media Player in the absence of a bundled one. " +
  434. "You can use this option to change it to another one. This does NOT log you off."
  435. );
  436. legend.appendChild(legendSubHeader);
  437. group.appendChild(legend);
  438. const resetSavedServer = document.createElement("button");
  439. resetSavedServer.className = "raised button-cancel block btnCancel emby-button";
  440. resetSavedServer.textContent = "Reset Saved Server"
  441. resetSavedServer.style.marginLeft = "auto";
  442. resetSavedServer.style.marginRight = "auto";
  443. resetSavedServer.style.maxWidth = "50%";
  444. resetSavedServer.addEventListener("click", async () => {
  445. window.jmpInfo.settings.main.userWebClient = '';
  446. window.location.href = jmpInfo.scriptPath + "/find-webclient.html";
  447. });
  448. group.appendChild(resetSavedServer);
  449. }
  450. const closeContainer = document.createElement("div");
  451. closeContainer.className = "formDialogFooter";
  452. modalContents.appendChild(closeContainer);
  453. const close = document.createElement("button");
  454. close.className = "raised button-cancel block btnCancel formDialogFooterItem emby-button";
  455. close.textContent = "Close"
  456. close.addEventListener("click", () => {
  457. modalContainer.remove();
  458. });
  459. closeContainer.appendChild(close);
  460. }