feat: WebRTC support, better rendering performace

This commit is contained in:
ThaUnknown 2022-11-30 22:08:56 +01:00
parent 41617bd556
commit ae17be05a6
13 changed files with 200 additions and 129 deletions

View file

@ -1,6 +1,6 @@
{
"name": "Miru",
"version": "3.2.3",
"version": "3.3.0",
"author": "ThaUnknown_ <ThaUnknown@users.noreply.github.com>",
"description": "Stream anime torrents, real-time with no waiting for downloads.",
"main": "src/index.js",
@ -109,7 +109,7 @@
"discord-rpc": "4.0.1",
"electron-log": "^4.4.6",
"electron-updater": "^4.6.5",
"jassub": "1.2.0",
"jassub": "1.2.1",
"js-levenshtein": "^1.1.6",
"matroska-subtitles": "github:ThaUnknown/matroska-subtitles#patch",
"mime": "^3.0.0",

View file

@ -7,12 +7,12 @@ specifiers:
browser-event-target-emitter: ^1.0.0
concurrently: ^7.0.0
discord-rpc: 4.0.1
electron: 20.1.1
electron: 21.3.1
electron-builder: ^23.3.3
electron-log: ^4.4.6
electron-notarize: ^1.1.1
electron-updater: ^4.6.5
jassub: 1.2.0
jassub: 1.2.1
js-levenshtein: ^1.1.6
matroska-subtitles: github:ThaUnknown/matroska-subtitles#patch
mime: ^3.0.0
@ -34,7 +34,7 @@ dependencies:
discord-rpc: 4.0.1
electron-log: 4.4.8
electron-updater: 4.6.5
jassub: 1.2.0
jassub: 1.2.1
js-levenshtein: 1.1.6
matroska-subtitles: github.com/ThaUnknown/matroska-subtitles/70bee097ad540e07d9e31b8f91f1dd865f7f2b45
mime: 3.0.0
@ -49,7 +49,7 @@ dependencies:
devDependencies:
'@sveltejs/vite-plugin-svelte': 1.0.1_svelte@3.49.0+vite@3.2.4
concurrently: 7.3.0
electron: 20.1.1
electron: 21.3.1
electron-builder: 23.3.3
electron-notarize: 1.2.1
svelte: 3.49.0
@ -1222,8 +1222,8 @@ packages:
- supports-color
dev: false
/electron/20.1.1:
resolution: {integrity: sha512-cFTfP4R2O5onaXiu+S20xK7eLpyX/H7PYk7lj9mlHS0ui1+w1jDDWD3RhvjmPgeksPfMAZiRLK8lAQvzSBAKdg==}
/electron/21.3.1:
resolution: {integrity: sha512-Ik/I9oFHA1h32JRtRm6GMgYdUctFpF/tPnHyATg4r3LXBTUT6habGh3GxSdmmTa5JgtA7uJUEm8EjjZItk7T3g==}
engines: {node: '>= 10.17.0'}
hasBin: true
requiresBuild: true
@ -2032,8 +2032,8 @@ packages:
minimatch: 3.1.2
dev: true
/jassub/1.2.0:
resolution: {integrity: sha512-ijYA+pGiIKhCPQtOpO+jisQbszeyTeDMQA9qvVJC1/4UbDl1sKsM1yCwqKjvv60XiGt/glyCdda5W6b/K4taFg==}
/jassub/1.2.1:
resolution: {integrity: sha512-bA8s0RAGNwjuqzrcPElXEPnAN/flPFP2sbw5/Q3C5kJX52YYqyZoFAhsCumy/FY6HUWmDz1HA/Y3ep6V26ol3A==}
dependencies:
rvfc-polyfill: 1.0.4
dev: false

View file

@ -1,6 +1,8 @@
const { app, BrowserWindow, protocol, shell, ipcMain, dialog } = require('electron')
const { app, BrowserWindow, protocol, shell, ipcMain, dialog, MessageChannelMain } = require('electron')
const path = require('path')
require('./main/misc.js')
const { Client } = require('discord-rpc')
const log = require('electron-log')
const { autoUpdater } = require('electron-updater')
if (process.defaultApp) {
if (process.argv.length >= 2) {
@ -73,6 +75,7 @@ ipcMain.on('open', (event, url) => {
// 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.
let mainWindow
let webtorrentWindow
function UpsertKeyValue (obj, keyToChange, value) {
const keyToChangeLower = keyToChange.toLowerCase()
@ -88,6 +91,10 @@ function UpsertKeyValue (obj, keyToChange, value) {
obj[keyToChange] = value
}
ipcMain.on('devtools', () => {
webtorrentWindow.webContents.openDevTools()
})
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({
@ -99,12 +106,20 @@ function createWindow () {
webPreferences: {
enableBlinkFeatures: 'FontAccess, AudioVideoTracks',
backgroundThrottling: false,
nodeIntegrationInWorker: true,
preload: path.join(__dirname, '/preload.js')
},
icon: path.join(__dirname, '/renderer/public/logo.ico'),
show: false
})
webtorrentWindow = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true,
backgroundThrottling: false
}
})
mainWindow.setMenuBarVisibility(false)
protocol.registerHttpProtocol('miru', (req, cb) => {
@ -130,10 +145,13 @@ function createWindow () {
// Delete this entire block of code when you are ready to package the application.
if (process.env.NODE_ENV !== 'development ') {
// Load production build
webtorrentWindow.loadFile(path.join(__dirname, '/renderer/dist/webtorrent.html'))
mainWindow.loadFile(path.join(__dirname, '/renderer/dist/index.html'))
} else {
// Load vite dev server page
console.log('Development mode')
webtorrentWindow.loadURL('http://localhost:5173/webtorrent.html')
webtorrentWindow.webContents.openDevTools()
mainWindow.loadURL('http://localhost:5173/')
mainWindow.webContents.openDevTools()
}
@ -166,6 +184,11 @@ function createWindow () {
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})
ipcMain.on('portRequest', ({ sender }) => {
const { port1, port2 } = new MessageChannelMain()
webtorrentWindow.webContents.postMessage('port', null, [port1])
sender.postMessage('port', null, [port2])
})
}
// This method will be called when Electron has finished
@ -185,3 +208,85 @@ app.on('activate', () => {
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) createWindow()
})
ipcMain.on('dialog', async (event, data) => {
const { filePaths } = await dialog.showOpenDialog({
properties: ['openDirectory']
})
if (filePaths.length) {
let path = filePaths[0]
if (!(path.endsWith('\\') || path.endsWith('/'))) {
if (path.indexOf('\\') !== -1) {
path += '\\'
} else if (path.indexOf('/') !== -1) {
path += '/'
}
}
event.sender.send('path', path)
}
})
ipcMain.on('minimize', (event) => {
BrowserWindow.fromWebContents(event.sender).minimize()
})
ipcMain.on('maximize', (event) => {
const window = BrowserWindow.fromWebContents(event.sender)
if (window.isMaximized()) {
window.unmaximize()
} else {
window.maximize()
}
})
ipcMain.on('close', () => {
app.quit()
})
let status = null
const discord = new Client({
transport: 'ipc'
})
function setDiscordRPC (event, data) {
status = data
if (discord?.user && status) {
status.pid = process.pid
discord.request('SET_ACTIVITY', status)
}
}
ipcMain.on('discord', setDiscordRPC)
discord.on('ready', async () => {
setDiscordRPC(null, status)
discord.subscribe('ACTIVITY_JOIN_REQUEST')
discord.subscribe('ACTIVITY_JOIN')
discord.subscribe('ACTIVITY_SPECTATE')
})
discord.on('ACTIVITY_JOIN_REQUEST', console.log)
discord.on('ACTIVITY_SPECTATE', console.log)
discord.on('ACTIVITY_JOIN', (args) => {
console.log('ACTIVITY_JOIN')
console.log(args)
console.log('------')
BrowserWindow.getAllWindows()[0]?.send('w2glink', args.secret)
})
function loginRPC () {
discord.login({ clientId: '954855428355915797' }).catch(() => {
setTimeout(loginRPC, 5000).unref()
})
}
loginRPC()
ipcMain.on('version', (event) => {
event.sender.send('version', app.getVersion()) // fucking stupid
})
autoUpdater.logger = log
autoUpdater.logger.transports.file.level = 'info'
ipcMain.on('update', () => {
autoUpdater.checkForUpdatesAndNotify()
})
autoUpdater.checkForUpdatesAndNotify()
autoUpdater.on('update-available', () => {
BrowserWindow.getAllWindows()[0]?.send('update', true)
})

View file

@ -1,83 +0,0 @@
const { dialog, ipcMain, BrowserWindow, app } = require('electron')
const { Client } = require('discord-rpc')
const log = require('electron-log')
const { autoUpdater } = require('electron-updater')
ipcMain.on('dialog', async (event, data) => {
const { filePaths } = await dialog.showOpenDialog({
properties: ['openDirectory']
})
if (filePaths.length) {
let path = filePaths[0]
if (!(path.endsWith('\\') || path.endsWith('/'))) {
if (path.indexOf('\\') !== -1) {
path += '\\'
} else if (path.indexOf('/') !== -1) {
path += '/'
}
}
event.sender.send('path', path)
}
})
ipcMain.on('minimize', (event) => {
BrowserWindow.fromWebContents(event.sender).minimize()
})
ipcMain.on('maximize', (event) => {
const window = BrowserWindow.fromWebContents(event.sender)
if (window.isMaximized()) {
window.unmaximize()
} else {
window.maximize()
}
})
let status = null
const discord = new Client({
transport: 'ipc'
})
function setDiscordRPC (event, data) {
status = data
if (discord?.user && status) {
status.pid = process.pid
discord.request('SET_ACTIVITY', status)
}
}
ipcMain.on('discord', setDiscordRPC)
discord.on('ready', async () => {
setDiscordRPC(null, status)
discord.subscribe('ACTIVITY_JOIN_REQUEST')
discord.subscribe('ACTIVITY_JOIN')
discord.subscribe('ACTIVITY_SPECTATE')
})
discord.on('ACTIVITY_JOIN_REQUEST', console.log)
discord.on('ACTIVITY_SPECTATE', console.log)
discord.on('ACTIVITY_JOIN', (args) => {
console.log('ACTIVITY_JOIN')
console.log(args)
console.log('------')
BrowserWindow.getAllWindows()[0]?.send('w2glink', args.secret)
})
function loginRPC () {
discord.login({ clientId: '954855428355915797' }).catch(() => {
setTimeout(loginRPC, 5000).unref()
})
}
loginRPC()
ipcMain.on('version', (event) => {
event.sender.send('version', app.getVersion()) // fucking stupid
})
autoUpdater.logger = log
autoUpdater.logger.transports.file.level = 'info'
ipcMain.on('update', () => {
autoUpdater.checkForUpdatesAndNotify()
})
autoUpdater.checkForUpdatesAndNotify()
autoUpdater.on('update-available', () => {
BrowserWindow.getAllWindows()[0]?.send('update', true)
})

View file

@ -8,6 +8,9 @@ contextBridge.exposeInMainWorld('IPC', {
on: (event, callback) => {
ipcRenderer.on(event, (event, ...args) => callback(...args))
},
once: (event, callback) => {
ipcRenderer.once(event, (event, ...args) => callback(...args))
},
off: (event) => {
ipcRenderer.removeAllListeners(event)
}
@ -16,3 +19,14 @@ contextBridge.exposeInMainWorld('version', {
arch: process.arch,
platform: process.platform
})
ipcRenderer.once('port', ({ ports }) => {
contextBridge.exposeInMainWorld('port', {
onmessage: (cb) => {
ports[0].onmessage = ({ type, data }) => cb({ type, data })
},
postMessage: (...args) => {
ports[0].postMessage(...args)
}
})
})

View file

@ -10,7 +10,7 @@
<link rel='icon' href='/logo.ico'>
<link href="/lib/Material-Icons.css" rel="stylesheet">
<script defer type="module" src="/src/main.js" async></script>
<script defer type="module" src="./src/main.js" async></script>
</head>
<body class="dark-mode with-custom-webkit-scrollbars with-custom-css-scrollbars">

View file

@ -4,6 +4,7 @@ const pump = require('pump')
const rangeParser = require('range-parser')
const mime = require('mime')
const { SubtitleParser, SubtitleStream } = require('matroska-subtitles')
const { ipcRenderer } = require('electron')
class TorrentClient extends WebTorrent {
constructor (settings) {
@ -96,8 +97,6 @@ class TorrentClient extends WebTorrent {
this.server.on('error', console.warn)
this.server.listen(0)
onmessage = this.handleMessage.bind(this)
}
handleMessage ({ data }) {
@ -116,7 +115,7 @@ class TorrentClient extends WebTorrent {
this.current.on('done', this.parseSubtitles.bind(this))
this.parseFonts(this.current)
}
// findSubtitleFiles(current) TODO:
// TODO: findSubtitleFiles(current)
}
break
}
@ -140,7 +139,7 @@ class TorrentClient extends WebTorrent {
}
dispatch (type, data) {
postMessage({ type, data })
message({ type, data })
}
parseSubtitles () {
@ -217,8 +216,14 @@ class TorrentClient extends WebTorrent {
}
let client = null
let message = null
onmessage = ({ data }) => {
if (!client && data.type === 'settings') client = new TorrentClient(data.data)
if (data.type === 'destroy') client?.predestroy()
}
ipcRenderer.on('port', (e) => {
e.ports[0].onmessage = ({ data }) => {
if (!client && data.type === 'settings') window.client = client = new TorrentClient(data.data)
if (data.type === 'destroy') client?.predestroy()
client.handleMessage({ data })
}
message = e.ports[0].postMessage.bind(e.ports[0])
})

View file

@ -1,6 +1,6 @@
<script context='module'>
import { writable } from 'svelte/store'
export const title = writable('Miru')
import { writable } from 'svelte/store'
export const title = writable('Miru')
</script>
<div class='w-full navbar border-0 bg-dark position-relative p-0'>
@ -21,7 +21,7 @@ export const title = writable('Miru')
<path d='M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z' />
</svg>
</div>
<div class='d-flex align-items-center close' on:click={window.close}>
<div class='d-flex align-items-center close' on:click={() => window.IPC.emit('close')}>
<svg viewBox='0 0 24 24'>
<path d='M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z' />
</svg>

View file

@ -180,7 +180,11 @@
}
let currentTime = 0
$: progress = currentTime / safeduration
let progress = 0
// needs to be slow in order to not spam repaints
function updateProgress () {
progress = video.currentTime / safeduration
}
$: targetTime = (!paused && currentTime) || targetTime
function handleMouseDown ({ target }) {
wasPaused = paused
@ -858,6 +862,7 @@
on:seeked={updatew2g}
on:timeupdate={() => createThumbnail()}
on:timeupdate={checkCompletion}
on:timeupdate={updateProgress}
on:waiting={showBuffering}
on:loadeddata={hideBuffering}
on:canplay={hideBuffering}
@ -894,16 +899,14 @@
</div>
<span class='material-icons ctrl' title='Keybinds [`]' on:click={() => (showKeybinds = true)}> help_outline </span>
</div>
<div class='middle d-flex align-items-center justify-content-center flex-grow-1 position-relative'>
<div class='position-absolute w-full h-full' on:dblclick={toggleFullscreen} on:click|self={() => (page = 'player')}>
<div class='play-overlay w-full h-full' on:click={playPause} />
</div>
<div class='middle d-flex align-items-center justify-content-center flex-grow-1'>
<div class='w-full h-full position-absolute' on:dblclick={toggleFullscreen} on:click|self={() => { if (page === 'player') playPause(); page = 'player' }} />
<span class='material-icons ctrl' class:text-muted={!hasLast} class:disabled={!hasLast} data-name='playLast' on:click={playLast}> skip_previous </span>
<span class='material-icons ctrl' data-name='rewind' on:click={rewind}> fast_rewind </span>
<span class='material-icons ctrl' data-name='playPause' on:click={playPause}> {ended ? 'replay' : paused ? 'play_arrow' : 'pause'} </span>
<span class='material-icons ctrl' data-name='forward' on:click={forward}> fast_forward </span>
<span class='material-icons ctrl' class:text-muted={!hasNext} class:disabled={!hasNext} data-name='playNext' on:click={playNext}> skip_next </span>
<div data-name='bufferingDisplay' class='position-absolute' />
<div class='position-absolute bufferingDisplay' />
</div>
<div class='bottom d-flex z-40 flex-column px-20'>
<div class='w-full d-flex align-items-center h-20 mb--5'>
@ -1036,6 +1039,7 @@
}
.miniplayer {
height: auto !important;
cursor: pointer !important;
}
.miniplayer .top,
.miniplayer .bottom {
@ -1105,7 +1109,7 @@
opacity: 0.1%;
}
.middle div[data-name='bufferingDisplay'] {
.middle .bufferingDisplay {
border: 4px solid #ffffff00;
border-top: 4px solid #fff;
border-radius: 50%;
@ -1113,6 +1117,7 @@
height: 40px;
animation: spin 1s linear infinite;
opacity: 0;
visibility: hidden;
transition: 0.5s opacity ease;
filter: drop-shadow(0 0 8px #000);
}
@ -1120,8 +1125,12 @@
cursor: not-allowed !important;
}
.buffering .middle div[data-name='bufferingDisplay'] {
.buffering .middle .bufferingDisplay {
opacity: 1 !important;
visibility: visible !important;
}
.pip .bufferingDisplay {
display: none;
}
@keyframes spin {
@ -1163,9 +1172,6 @@
font-size: 2.8rem;
margin: 0.6rem;
}
.miniplayer .middle .play-overlay {
display: none !important;
}
.miniplayer .middle .ctrl[data-name='playPause'] {
font-size: 5.625rem;
}

View file

@ -138,7 +138,6 @@ export default class Subtitles {
availableFonts: {
'roboto medium': './Roboto.ttf'
},
useLocalFonts: true,
workerUrl
})
this.selectCaptions(this.current)

View file

@ -1,25 +1,32 @@
// import WebTorrent from 'webtorrent'
import { set } from '@/lib/Settings.svelte'
import { files } from '@/lib/Player/MediaHandler.svelte'
import { page } from '@/App.svelte'
import 'browser-event-target-emitter'
class TorrentWorker extends Worker {
constructor (opts) {
super(opts)
this.onmessage = this.handleMessage.bind(this)
class TorrentWorker extends EventTarget {
constructor () {
super()
this.ready = new Promise((resolve) => {
window.IPC.once('port', () => {
this.port = window.port
this.port.onmessage((...args) => this.handleMessage(...args))
resolve()
})
window.IPC.emit('portRequest')
})
}
handleMessage ({ data }) {
this.emit(data.type, data.data)
}
send (type, data) {
this.postMessage({ type, data })
async send (type, data) {
await this.ready
this.port.postMessage({ type, data })
}
}
export const client = new TorrentWorker(new URL('./torrentworker.js', import.meta.url))
export const client = new TorrentWorker()
client.send('settings', { ...set })

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<meta name="theme-color" content="#191c20">
<title>WebTorrent Hidden Window</title>
<script defer src="./lib/webtorrent.js" async></script>
</head>
<body></body>
</html>

View file

@ -4,6 +4,7 @@ import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import commonjs from 'vite-plugin-commonjs'
const root = path.resolve(process.cwd(), 'src/renderer')
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
return {
@ -13,12 +14,16 @@ export default defineConfig(({ mode }) => {
}
},
plugins: [mode !== 'development' && commonjs(), svelte()],
root: path.resolve(process.cwd(), 'src/renderer'),
root,
base: './',
build: {
rollupOptions: {
output: {
assetFileNames: '[name].[ext]'
},
input: {
index: root + '/index.html',
torrent: root + '/webtorrent.html'
}
}
}