mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 09:33:09 +01:00
Chromium evaluates the Fetch API's cache mode before the disk cache storage decision; XHR request headers and onHeadersReceived patches do not reliably prevent storage. The old approach silently allowed caching regardless of the setting. Moving cache suppression to a custom hls.js NoCacheLoader (Fetch with cache: 'no-store') also eliminates the CORS preflight that Cache-Control as a request header was triggering, which lets the main-process CORS handler drop the applyNoStore branch entirely.
142 lines
4.6 KiB
TypeScript
142 lines
4.6 KiB
TypeScript
import { app, BrowserWindow, shell, screen, session } from 'electron'
|
|
import { join } from 'node:path'
|
|
import { AuthService } from './auth/authService'
|
|
import { HelixClient } from './twitch/helixClient'
|
|
import { PlaybackService } from './playback/playbackService'
|
|
import { registerIpcHandlers } from './ipc/handlers'
|
|
import { startGamepadReader } from './input/gamepadReader'
|
|
|
|
import log from 'electron-log/main'
|
|
|
|
log.initialize()
|
|
log.transports.file.level = 'error'
|
|
log.transports.console.level = 'debug'
|
|
|
|
const isDev = !app.isPackaged
|
|
|
|
// Allows video.play() without a prior user gesture — play() is called from hls.js callbacks
|
|
// that fire asynchronously long after the original click event.
|
|
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required')
|
|
// Explicit cache size prevents Chromium's dynamic sizing, which causes "Invalid cache (current)
|
|
// size" errors when available disk space fluctuates.
|
|
|
|
app.commandLine.appendSwitch('disk-cache-size', String(500 * 1024 * 1024))
|
|
|
|
function createWindow(): BrowserWindow {
|
|
const isLinux = process.platform === 'linux'
|
|
const { width, height } = isLinux
|
|
? screen.getPrimaryDisplay().workAreaSize
|
|
: { width: 1280, height: 800 }
|
|
|
|
const win = new BrowserWindow({
|
|
width,
|
|
height,
|
|
minWidth: 1024,
|
|
minHeight: 640,
|
|
show: false,
|
|
autoHideMenuBar: true,
|
|
backgroundColor: '#0e0e10',
|
|
webPreferences: {
|
|
preload: join(__dirname, '../preload/index.js'),
|
|
sandbox: false,
|
|
contextIsolation: true,
|
|
nodeIntegration: false
|
|
}
|
|
})
|
|
|
|
win.once('ready-to-show', () => {
|
|
if (isLinux) win.maximize()
|
|
win.show()
|
|
})
|
|
|
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
shell.openExternal(url)
|
|
return { action: 'deny' }
|
|
})
|
|
|
|
if (isDev && process.env.ELECTRON_RENDERER_URL) {
|
|
win.loadURL(process.env.ELECTRON_RENDERER_URL)
|
|
} else {
|
|
win.loadFile(join(__dirname, '../renderer/index.html'))
|
|
}
|
|
|
|
return win
|
|
}
|
|
|
|
/**
|
|
* CORS-Bypass für Twitch CDN und HLS-Streams.
|
|
* hls.js im Renderer sendet Requests von localhost/file:// — Twitch CDN erwartet
|
|
* Origin: https://www.twitch.tv. Wir setzen Origin + Referer auf ausgehenden Requests
|
|
* und ergänzen CORS-Response-Header, damit Chromium die Antworten akzeptiert.
|
|
*
|
|
* HLS-Cache-Unterdrückung: xhrSetup in hls.js setzt Cache-Control: no-store auf jedem
|
|
* XHR. Das löst einen CORS-Preflight aus (Cache-Control ist kein safelisted header).
|
|
* Damit der Preflight durchkommt, muss onHeadersReceived auch ttvnw.net-Domains abdecken
|
|
* (Segment-CDN: *.j.cloudfront.hls.ttvnw.net — enthält nicht "cloudfront.net").
|
|
*/
|
|
function setupTwitchCors(): void {
|
|
const isTwitchCdn = (url: string): boolean =>
|
|
url.includes('twitchsvc.net') ||
|
|
url.includes('cloudfront.net') ||
|
|
url.includes('ttvnw.net') ||
|
|
url.includes('twitch.tv') ||
|
|
url.includes('twitch.amazon.com')
|
|
|
|
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
|
if (isTwitchCdn(details.url)) {
|
|
details.requestHeaders['Origin'] = 'https://www.twitch.tv'
|
|
details.requestHeaders['Referer'] = 'https://www.twitch.tv/'
|
|
}
|
|
callback({ requestHeaders: details.requestHeaders })
|
|
})
|
|
|
|
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
|
if (!isTwitchCdn(details.url)) { callback({}); return }
|
|
|
|
const headers: Record<string, string[]> = {}
|
|
for (const [k, v] of Object.entries(details.responseHeaders ?? {})) {
|
|
headers[k.toLowerCase()] = Array.isArray(v) ? v : [v]
|
|
}
|
|
headers['access-control-allow-origin'] = ['*']
|
|
headers['access-control-allow-headers'] = ['*']
|
|
headers['access-control-allow-methods'] = ['GET, HEAD, OPTIONS']
|
|
callback({ responseHeaders: headers })
|
|
})
|
|
}
|
|
|
|
app.whenReady().then(async () => {
|
|
setupTwitchCors()
|
|
const clientId = import.meta.env.MAIN_VITE_TWITCH_CLIENT_ID ?? ''
|
|
const auth = new AuthService(clientId)
|
|
await auth.init()
|
|
const helix = new HelixClient(clientId, async () => {
|
|
const token = await auth.getValidAccessToken()
|
|
if (!token) throw new Error('Kein gueltiges Twitch-Access-Token vorhanden')
|
|
return token
|
|
})
|
|
const playback = new PlaybackService()
|
|
|
|
registerIpcHandlers(auth, helix, playback)
|
|
|
|
const stopGamepad = startGamepadReader(() => BrowserWindow.getAllWindows()[0] ?? null)
|
|
|
|
app.on('before-quit', () => {
|
|
stopGamepad()
|
|
playback.stop()
|
|
})
|
|
|
|
if (!clientId) {
|
|
log.warn('[twitch4steamdeck] MAIN_VITE_TWITCH_CLIENT_ID nicht gesetzt — Login wird im UI als nicht konfiguriert angezeigt.')
|
|
}
|
|
|
|
createWindow()
|
|
|
|
app.on('activate', () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
|
})
|
|
})
|
|
|
|
app.on('window-all-closed', () => {
|
|
if (process.platform !== 'darwin') app.quit()
|
|
})
|