From 17498e8e695b44c65626468628f37f16e594ebc7 Mon Sep 17 00:00:00 2001 From: ThaUnknown <6506529+ThaUnknown@users.noreply.github.com> Date: Fri, 22 Mar 2024 01:16:53 +0100 Subject: [PATCH] feat: spawn custom video player process #415 --- capacitor/src/support.js | 3 +- common/modules/support.js | 3 +- common/modules/webtorrent.js | 34 +++++++++++++--- common/views/Player/Player.svelte | 44 +++++++++++++++------ common/views/Settings/PlayerSettings.svelte | 25 ++++++++++++ common/views/Settings/Settings.svelte | 6 +++ electron/src/main/dialog.js | 25 ++++++++++++ electron/src/main/discord.js | 2 +- electron/src/main/main.js | 4 ++ electron/src/main/protocol.js | 2 +- electron/src/main/store.js | 2 +- electron/src/main/updater.js | 2 +- electron/src/main/util.js | 15 ++++--- 13 files changed, 135 insertions(+), 32 deletions(-) create mode 100644 electron/src/main/dialog.js diff --git a/capacitor/src/support.js b/capacitor/src/support.js index 400d0881..3d38a543 100644 --- a/capacitor/src/support.js +++ b/capacitor/src/support.js @@ -11,5 +11,6 @@ export const SUPPORTS = { torrentPath: false, torrentPersist: false, keybinds: false, - isAndroid: true + isAndroid: true, + externalPlayer: false } diff --git a/common/modules/support.js b/common/modules/support.js index a60b7209..17820d28 100644 --- a/common/modules/support.js +++ b/common/modules/support.js @@ -12,5 +12,6 @@ export const SUPPORTS = { torrentPersist: true, keybinds: true, extensions: true, - isAndroid: false + isAndroid: false, + externalPlayer: true } diff --git a/common/modules/webtorrent.js b/common/modules/webtorrent.js index 9b21129e..b759dddc 100644 --- a/common/modules/webtorrent.js +++ b/common/modules/webtorrent.js @@ -1,3 +1,4 @@ +import { spawn } from 'node:child_process' import WebTorrent from 'webtorrent' import HTTPTracker from 'bittorrent-tracker/lib/client/http-tracker.js' import { hex2bin, arr2hex, text2arr } from 'uint8-util' @@ -33,6 +34,10 @@ try { export default class TorrentClient extends WebTorrent { static excludedErrorMessages = ['WebSocket', 'User-Initiated Abort, reason=', 'Connection failed.'] + player = '' + /** @type {ReturnType} */ + playerProcess = null + constructor (ipc, storageQuota, serverMode, settingOverrides = {}, controller) { const settings = { ...defaults, ...storedSettings, ...settingOverrides } super({ @@ -55,6 +60,9 @@ export default class TorrentClient extends WebTorrent { }) ipc.on('destroy', this.destroy.bind(this)) }) + ipc.on('player', (event, data) => { + this.player = data + }) this.settings = settings this.serverMode = serverMode @@ -219,18 +227,34 @@ export default class TorrentClient extends WebTorrent { switch (data.type) { case 'current': { if (data.data) { - const torrent = await this.get(data.data.infoHash) - const found = torrent?.files.find(file => file.path === data.data.path) + const torrent = await this.get(data.data.current.infoHash) + const found = torrent?.files.find(file => file.path === data.data.current.path) if (!found) return + if (this.playerProcess) { + this.playerProcess.kill() + this.playerProcess = null + } if (this.current) { this.current.removeAllListeners('stream') } this.parser?.destroy() found.select() this.current = found - this.parser = new Parser(this, found) - this.findSubtitleFiles(found) - this.findFontFiles(found) + if (data.data.external && this.player) { + this.playerProcess = spawn(this.player, ['http://localhost:' + this.server.address().port + found.streamURL]) + this.playerProcess.stdout.on('data', () => {}) + const startTime = Date.now() + this.playerProcess.once('close', () => { + this.playerProcess = null + const seconds = (Date.now() - startTime) / 1000 + console.log(seconds) + this.dispatch('externalWatched', seconds) + }) + } else { + this.parser = new Parser(this, found) + this.findSubtitleFiles(found) + this.findFontFiles(found) + } } break } diff --git a/common/views/Player/Player.svelte b/common/views/Player/Player.svelte index 14952569..1f377c31 100644 --- a/common/views/Player/Player.svelte +++ b/common/views/Player/Player.svelte @@ -155,6 +155,8 @@ } $: loadDeband($settings.playerDeband, video) + let watchedListener + async function handleCurrent (file) { if (file) { if (thumbnailData.video?.src) URL.revokeObjectURL(video?.src) @@ -168,14 +170,26 @@ chapters = [] currentSkippable = null completed = false - if (subs) subs.destroy() + if (subs) { + subs.destroy() + subs = null + } current = file emit('current', current) - src = file.url - client.send('current', file) - subs = new Subtitles(video, files, current, handleHeaders) - video.load() - await loadAnimeProgress() + client.send('current', { current: file, external: settings.value.enableExternal }) + if (!settings.value.enableExternal) { + src = file.url + subs = new Subtitles(video, files, current, handleHeaders) + video.load() + await loadAnimeProgress() + } else if (current.media?.media?.duration) { + const duration = current.media?.media?.duration + client.removeEventListener('externalWatched', watchedListener) + watchedListener = ({ detail }) => { + checkCompletionByTime(detail, duration) + } + client.addEventListener('externalWatched', watchedListener) + } } } @@ -882,13 +896,17 @@ let completed = false function checkCompletion () { if (!completed && $settings.playerAutocomplete) { - const fromend = Math.max(180, safeduration / 10) - if (safeduration && currentTime && video?.readyState && safeduration - fromend < currentTime) { - if (media?.media?.episodes || media?.media?.nextAiringEpisode?.episode) { - if (media.media.episodes || media.media.nextAiringEpisode?.episode > media.episode) { - completed = true - anilistClient.alEntry(media) - } + checkCompletionByTime(currentTime, safeduration) + } + } + + function checkCompletionByTime (time, duration) { + const fromend = Math.max(180, duration / 10) + if (time && time && video?.readyState && time - fromend < time) { + if (media?.media?.episodes || media?.media?.nextAiringEpisode?.episode) { + if (media.media.episodes || media.media.nextAiringEpisode?.episode > media.episode) { + completed = true + anilistClient.alEntry(media) } } } diff --git a/common/views/Settings/PlayerSettings.svelte b/common/views/Settings/PlayerSettings.svelte index 88885471..c23280ae 100644 --- a/common/views/Settings/PlayerSettings.svelte +++ b/common/views/Settings/PlayerSettings.svelte @@ -2,6 +2,9 @@ import { toast } from 'svelte-sonner' import FontSelect from 'simple-font-select' import SettingCard from './SettingCard.svelte' + import { SUPPORTS } from '@/modules/support.js' + import { click } from '@/modules/click.js' + import IPC from '@/modules/ipc.js' export let settings async function changeFont ({ detail }) { @@ -21,6 +24,9 @@ }) } } + function handleExecutable () { + IPC.emit('player') + } {#if ('queryLocalFonts' in self)} @@ -131,3 +137,22 @@ + +{#if SUPPORTS.externalPlayer} +

External Player Settings

+ +
+ + +
+
+ +
+
+ +
+ +
+
+{/if} diff --git a/common/views/Settings/Settings.svelte b/common/views/Settings/Settings.svelte index 6fc7c97d..7ac9e5ca 100644 --- a/common/views/Settings/Settings.svelte +++ b/common/views/Settings/Settings.svelte @@ -80,6 +80,10 @@ $settings.torrentPath = data } + function playerListener (data) { + $settings.playerPath = data + } + function loginButton () { if (anilistClient.userID) { $logout = true @@ -95,9 +99,11 @@ } onDestroy(() => { IPC.off('path', pathListener) + IPC.off('player', playerListener) }) $: IPC.emit('show-discord-status', $settings.showDetailsInRPC) IPC.on('path', pathListener) + IPC.on('player', playerListener) diff --git a/electron/src/main/dialog.js b/electron/src/main/dialog.js new file mode 100644 index 00000000..2d86468d --- /dev/null +++ b/electron/src/main/dialog.js @@ -0,0 +1,25 @@ +import { basename, extname } from 'node:path' +import { ipcMain, dialog } from 'electron' +import store from './store.js' + +export default class Dialog { + /** + * @param {import('electron').BrowserWindow} torrentWindow + */ + constructor (torrentWindow) { + ipcMain.on('player', async ({ sender }) => { + const { filePaths, canceled } = await dialog.showOpenDialog({ + title: 'Select video player executable', + properties: ['openFile'] + }) + if (canceled) return + if (filePaths.length) { + const path = filePaths[0] + + store.set('player', path) + torrentWindow.webContents.send('player', path) + sender.send('player', basename(path, extname(path))) + } + }) + } +} diff --git a/electron/src/main/discord.js b/electron/src/main/discord.js index 127ab125..eafbcab8 100644 --- a/electron/src/main/discord.js +++ b/electron/src/main/discord.js @@ -2,7 +2,7 @@ import { Client } from 'discord-rpc' import { ipcMain } from 'electron' import { debounce } from '@/modules/util.js' -export default class { +export default class Discord { defaultStatus = { activity: { timestamps: { start: Date.now() }, diff --git a/electron/src/main/main.js b/electron/src/main/main.js index cd97ad6b..a18165a8 100644 --- a/electron/src/main/main.js +++ b/electron/src/main/main.js @@ -5,6 +5,8 @@ import Discord from './discord.js' import Updater from './updater.js' import Protocol from './protocol.js' import { development } from './util.js' +import Dialog from './dialog.js' +import store from './store.js' // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. @@ -44,6 +46,7 @@ function createWindow () { new Discord(mainWindow) new Protocol(mainWindow) new Updater(mainWindow) + new Dialog(webtorrentWindow) mainWindow.setMenuBarVisibility(false) mainWindow.webContents.session.webRequest.onHeadersReceived(({ responseHeaders }, fn) => { @@ -107,6 +110,7 @@ function createWindow () { const { port1, port2 } = new MessageChannelMain() await torrentLoad webtorrentWindow.webContents.postMessage('port', null, [port1]) + webtorrentWindow.webContents.postMessage('player', store.get('player')) sender.postMessage('port', null, [port2]) }) } diff --git a/electron/src/main/protocol.js b/electron/src/main/protocol.js index eee0a6c6..669fc12e 100644 --- a/electron/src/main/protocol.js +++ b/electron/src/main/protocol.js @@ -10,7 +10,7 @@ if (process.defaultApp) { app.setAsDefaultProtocolClient('miru') } -export default class { +export default class Protocol { // schema: miru://key/value protocolMap = { auth: token => this.sendToken(token), diff --git a/electron/src/main/store.js b/electron/src/main/store.js index 306f0966..17f2806f 100644 --- a/electron/src/main/store.js +++ b/electron/src/main/store.js @@ -27,4 +27,4 @@ function parseDataFile (filePath, defaults) { } } -export default new Store('settings', { angle: 'default' }) +export default new Store('settings', { angle: 'default', player: '' }) diff --git a/electron/src/main/updater.js b/electron/src/main/updater.js index 03e440f2..ba20f51a 100644 --- a/electron/src/main/updater.js +++ b/electron/src/main/updater.js @@ -9,7 +9,7 @@ ipcMain.on('update', () => { }) autoUpdater.checkForUpdatesAndNotify() -export default class { +export default class Updater { /** * @param {import('electron').BrowserWindow} window */ diff --git a/electron/src/main/util.js b/electron/src/main/util.js index cc74a4c6..754d1d7b 100644 --- a/electron/src/main/util.js +++ b/electron/src/main/util.js @@ -33,11 +33,9 @@ ipcMain.on('open', (event, url) => { ipcMain.on('doh', (event, dns) => { try { - const url = new URL(dns) - app.configureHostResolver({ secureDnsMode: 'secure', - secureDnsServers: [url.toString()] + secureDnsServers: ['' + new URL(dns)] }) } catch (e) {} }) @@ -50,10 +48,11 @@ ipcMain.on('close', () => { app.quit() }) -ipcMain.on('dialog', async (event, data) => { - const { filePaths } = await dialog.showOpenDialog({ +ipcMain.on('dialog', async ({ sender }) => { + const { filePaths, canceled } = await dialog.showOpenDialog({ properties: ['openDirectory'] }) + if (canceled) return if (filePaths.length) { let path = filePaths[0] if (!(path.endsWith('\\') || path.endsWith('/'))) { @@ -63,12 +62,12 @@ ipcMain.on('dialog', async (event, data) => { path += '/' } } - event.sender.send('path', path) + sender.send('path', path) } }) -ipcMain.on('version', (event) => { - event.sender.send('version', app.getVersion()) // fucking stupid +ipcMain.on('version', ({ sender }) => { + sender.send('version', app.getVersion()) // fucking stupid }) app.setJumpList?.([