Files
twitch4steamdeck/src/main/index.ts
T
Andre 123809fb9a fix: suppress HLS disk cache via Fetch API loader instead of XHR headers
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.
2026-05-15 11:31:32 +02:00

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()
})