mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 15:43:08 +01:00
feat: unify logging via electron-log and add IPC error boundary
Bug reports from Steam Deck (Flatpak) arrived without any useful context: no terminal, no log file, no trace of what failed. Silent catches in helixClient hid quota and token errors entirely. IPC handler exceptions reached the renderer as rejected promises with no server-side record. electron-log now writes to a rotating log file (~1 MB, 5 files) that users can attach to bug reports. All IPC handlers are wrapped in safeHandle, which logs and re-throws at the boundary. Optional-feature failures (chapters, profile images, viewer counts) emit log.warn with enough context to diagnose. A React ErrorBoundary catches render crashes app-wide and logs them before showing a reload prompt.
This commit is contained in:
Generated
+12
-2
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch4steamdeck",
|
"name": "twitch4steamdeck",
|
||||||
"version": "0.3.4",
|
"version": "0.4.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch4steamdeck",
|
"name": "twitch4steamdeck",
|
||||||
"version": "0.3.4",
|
"version": "0.4.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^12.8.0",
|
"better-sqlite3": "^12.8.0",
|
||||||
|
"electron-log": "^5.4.4",
|
||||||
"hls.js": "^1.6.16",
|
"hls.js": "^1.6.16",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -4237,6 +4238,15 @@
|
|||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-log": {
|
||||||
|
"version": "5.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.4.tgz",
|
||||||
|
"integrity": "sha512-istWgaXjBfURBSS8LWVW9C3jsc6+ac+tY1lXrQEOTp0lVj+a4OlO1Tmqb36GgnEUDv92DGC9VI1HNXwJinWpgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-publish": {
|
"node_modules/electron-publish": {
|
||||||
"version": "25.1.7",
|
"version": "25.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^12.8.0",
|
"better-sqlite3": "^12.8.0",
|
||||||
|
"electron-log": "^5.4.4",
|
||||||
"hls.js": "^1.6.16",
|
"hls.js": "^1.6.16",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
+6
-3
@@ -6,6 +6,11 @@ import { PlaybackService } from './playback/playbackService'
|
|||||||
import { registerIpcHandlers } from './ipc/handlers'
|
import { registerIpcHandlers } from './ipc/handlers'
|
||||||
import { startGamepadReader } from './input/gamepadReader'
|
import { startGamepadReader } from './input/gamepadReader'
|
||||||
import { getHlsCacheEnabled } from './prefs/hlsCachePref'
|
import { getHlsCacheEnabled } from './prefs/hlsCachePref'
|
||||||
|
import log from 'electron-log/main'
|
||||||
|
|
||||||
|
log.initialize()
|
||||||
|
log.transports.file.level = 'info'
|
||||||
|
log.transports.console.level = 'debug'
|
||||||
|
|
||||||
const isDev = !app.isPackaged
|
const isDev = !app.isPackaged
|
||||||
|
|
||||||
@@ -130,9 +135,7 @@ app.whenReady().then(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
console.warn(
|
log.warn('[twitch4steamdeck] MAIN_VITE_TWITCH_CLIENT_ID nicht gesetzt — Login wird im UI als nicht konfiguriert angezeigt.')
|
||||||
'[twitch4steamdeck] MAIN_VITE_TWITCH_CLIENT_ID nicht gesetzt — Login wird im UI als nicht konfiguriert angezeigt.'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createWindow()
|
createWindow()
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import * as fs from 'node:fs'
|
|||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
import type { BrowserWindow } from 'electron'
|
import type { BrowserWindow } from 'electron'
|
||||||
import { DEDUP_WINDOW_MS, HOTPLUG_SCAN_INTERVAL_MS, TRIGGER_THRESHOLD } from '../constants/input'
|
import { DEDUP_WINDOW_MS, HOTPLUG_SCAN_INTERVAL_MS, TRIGGER_THRESHOLD } from '../constants/input'
|
||||||
|
import log from 'electron-log/main'
|
||||||
|
|
||||||
// evdev input_event auf 64-bit Linux:
|
// evdev input_event auf 64-bit Linux:
|
||||||
// { u64 tv_sec (8 Bytes), u64 tv_usec (8 Bytes), u16 type (2), u16 code (2), s32 value (4) } = 24 Bytes
|
// { u64 tv_sec (8 Bytes), u64 tv_usec (8 Bytes), u16 type (2), u16 code (2), s32 value (4) } = 24 Bytes
|
||||||
@@ -397,21 +398,21 @@ export function startGamepadReader(getWindow: () => BrowserWindow | null): () =>
|
|||||||
reader = new EvdevReader(eventPath, onKey)
|
reader = new EvdevReader(eventPath, onKey)
|
||||||
if (reader.start()) {
|
if (reader.start()) {
|
||||||
readers.set(jsPath, reader)
|
readers.set(jsPath, reader)
|
||||||
console.log(`[gamepad] geöffnet (evdev): ${jsPath} → ${eventPath}`)
|
log.info(`[gamepad] geöffnet (evdev): ${jsPath} → ${eventPath}`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to legacy joystick API when no /sys mapping is found
|
// Fallback to legacy joystick API when no /sys mapping is found
|
||||||
reader = new JoystickFallbackReader(jsPath, onKey)
|
reader = new JoystickFallbackReader(jsPath, onKey)
|
||||||
if (reader.start()) {
|
if (reader.start()) {
|
||||||
readers.set(jsPath, reader)
|
readers.set(jsPath, reader)
|
||||||
console.log(`[gamepad] geöffnet (js-fallback): ${jsPath}`)
|
log.info(`[gamepad] geöffnet (js-fallback): ${jsPath}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [jsPath, reader] of readers) {
|
for (const [jsPath, reader] of readers) {
|
||||||
if (!seen.has(jsPath)) {
|
if (!seen.has(jsPath)) {
|
||||||
console.log(`[gamepad] getrennt: ${jsPath}`)
|
log.info(`[gamepad] getrennt: ${jsPath}`)
|
||||||
reader.stop()
|
reader.stop()
|
||||||
readers.delete(jsPath)
|
readers.delete(jsPath)
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-31
@@ -9,31 +9,42 @@ import type { HlsUrlPayload } from '../playback/playbackService'
|
|||||||
import type { PlaybackEvent } from '../playback/types'
|
import type { PlaybackEvent } from '../playback/types'
|
||||||
import { getProgressMap } from '../store/historyRepo'
|
import { getProgressMap } from '../store/historyRepo'
|
||||||
import { IPC } from './channels'
|
import { IPC } from './channels'
|
||||||
|
import log from 'electron-log/main'
|
||||||
|
|
||||||
export { IPC }
|
export { IPC }
|
||||||
|
|
||||||
|
function safeHandle<A extends unknown[], R>(
|
||||||
|
channel: string,
|
||||||
|
fn: (...args: A) => R | Promise<R>,
|
||||||
|
): void {
|
||||||
|
ipcMain.handle(channel, async (_e, ...args) => {
|
||||||
|
try { return await fn(...(args as A)) }
|
||||||
|
catch (err) { log.error(`[ipc:${channel}]`, err); throw err }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function registerIpcHandlers(
|
export function registerIpcHandlers(
|
||||||
auth: AuthService,
|
auth: AuthService,
|
||||||
helix: HelixClient,
|
helix: HelixClient,
|
||||||
playback: PlaybackService
|
playback: PlaybackService
|
||||||
): void {
|
): void {
|
||||||
ipcMain.handle(IPC.appQuit, () => {
|
safeHandle(IPC.appQuit, () => {
|
||||||
app.quit()
|
app.quit()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle(IPC.appGetCacheSize, () => session.defaultSession.getCacheSize())
|
safeHandle(IPC.appGetCacheSize, () => session.defaultSession.getCacheSize())
|
||||||
ipcMain.handle(IPC.appClearCache, () => session.defaultSession.clearCache())
|
safeHandle(IPC.appClearCache, () => session.defaultSession.clearCache())
|
||||||
ipcMain.handle(IPC.appSetHlsCacheEnabled, (_e, enabled: boolean) => {
|
safeHandle(IPC.appSetHlsCacheEnabled, (enabled: boolean) => {
|
||||||
setHlsCacheEnabled(Boolean(enabled))
|
setHlsCacheEnabled(Boolean(enabled))
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle(IPC.authStatus, () => auth.getStatus())
|
safeHandle(IPC.authStatus, () => auth.getStatus())
|
||||||
ipcMain.handle(IPC.authConfigured, () => auth.isConfigured())
|
safeHandle(IPC.authConfigured, () => auth.isConfigured())
|
||||||
ipcMain.handle(IPC.authStart, () => auth.startDeviceFlow())
|
safeHandle(IPC.authStart, () => auth.startDeviceFlow())
|
||||||
ipcMain.handle(IPC.authCancel, () => {
|
safeHandle(IPC.authCancel, () => {
|
||||||
auth.cancelFlow()
|
auth.cancelFlow()
|
||||||
})
|
})
|
||||||
ipcMain.handle(IPC.authLogout, () => auth.logout())
|
safeHandle(IPC.authLogout, () => auth.logout())
|
||||||
|
|
||||||
auth.on('auth-event', (event: AuthEvent) => {
|
auth.on('auth-event', (event: AuthEvent) => {
|
||||||
for (const win of BrowserWindow.getAllWindows()) {
|
for (const win of BrowserWindow.getAllWindows()) {
|
||||||
@@ -41,15 +52,15 @@ export function registerIpcHandlers(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle(IPC.twitchGetFollowed, () => helix.getFollowedWithLiveStatus())
|
safeHandle(IPC.twitchGetFollowed, () => helix.getFollowedWithLiveStatus())
|
||||||
ipcMain.handle(IPC.twitchGetOwnUser, () => helix.getOwnUserInfo())
|
safeHandle(IPC.twitchGetOwnUser, () => helix.getOwnUserInfo())
|
||||||
ipcMain.handle(
|
safeHandle(
|
||||||
IPC.twitchGetTopGames,
|
IPC.twitchGetTopGames,
|
||||||
(_e, cursor?: string) => helix.getTopGames(40, cursor)
|
(cursor?: string) => helix.getTopGames(40, cursor)
|
||||||
)
|
)
|
||||||
ipcMain.handle(
|
safeHandle(
|
||||||
IPC.twitchGetTopStreams,
|
IPC.twitchGetTopStreams,
|
||||||
(_e, options?: { gameId?: string; language?: string; cursor?: string; limit?: number }) =>
|
(options?: { gameId?: string; language?: string; cursor?: string; limit?: number }) =>
|
||||||
helix.getTopStreams({
|
helix.getTopStreams({
|
||||||
gameId: options?.gameId,
|
gameId: options?.gameId,
|
||||||
language: options?.language,
|
language: options?.language,
|
||||||
@@ -57,40 +68,40 @@ export function registerIpcHandlers(
|
|||||||
limit: options?.limit ?? (options?.gameId ? 40 : 100)
|
limit: options?.limit ?? (options?.gameId ? 40 : 100)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
ipcMain.handle(
|
safeHandle(
|
||||||
IPC.twitchGetVideos,
|
IPC.twitchGetVideos,
|
||||||
(_e, broadcasterId: string) => helix.getVideos(broadcasterId)
|
(broadcasterId: string) => helix.getVideos(broadcasterId)
|
||||||
)
|
)
|
||||||
ipcMain.handle(
|
safeHandle(
|
||||||
IPC.twitchGetVodChapters,
|
IPC.twitchGetVodChapters,
|
||||||
(_e, videoId: string) => helix.getVodChapters(videoId)
|
(videoId: string) => helix.getVodChapters(videoId)
|
||||||
)
|
)
|
||||||
ipcMain.handle(
|
safeHandle(
|
||||||
IPC.historyGetProgress,
|
IPC.historyGetProgress,
|
||||||
(_e, vodIds: string[]) => getProgressMap(vodIds)
|
(vodIds: string[]) => getProgressMap(vodIds)
|
||||||
)
|
)
|
||||||
|
|
||||||
ipcMain.handle(
|
safeHandle(
|
||||||
IPC.playbackStartLive,
|
IPC.playbackStartLive,
|
||||||
(_e, channelLogin: string, quality?: string) => playback.startLive(channelLogin, quality)
|
(channelLogin: string, quality?: string) => playback.startLive(channelLogin, quality)
|
||||||
)
|
)
|
||||||
ipcMain.handle(
|
safeHandle(
|
||||||
IPC.playbackStartVod,
|
IPC.playbackStartVod,
|
||||||
(_e, vodId: string, channelLogin: string, title: string, durationSeconds: number, startSeconds?: number, quality?: string) =>
|
(vodId: string, channelLogin: string, title: string, durationSeconds: number, startSeconds?: number, quality?: string) =>
|
||||||
playback.startVod(vodId, channelLogin, title, durationSeconds, startSeconds, quality)
|
playback.startVod(vodId, channelLogin, title, durationSeconds, startSeconds, quality)
|
||||||
)
|
)
|
||||||
ipcMain.handle(
|
safeHandle(
|
||||||
IPC.playbackGetQualities,
|
IPC.playbackGetQualities,
|
||||||
(_e, twitchUrl: string) => playback.getAvailableQualities(twitchUrl)
|
(twitchUrl: string) => playback.getAvailableQualities(twitchUrl)
|
||||||
)
|
)
|
||||||
ipcMain.handle(IPC.playbackStop, () => playback.stop())
|
safeHandle(IPC.playbackStop, () => playback.stop())
|
||||||
ipcMain.handle(IPC.playbackPause, () => {
|
safeHandle(IPC.playbackPause, () => {
|
||||||
// Bring Electron window back into focus (Windows: Gamepad API requires window focus)
|
// Bring Electron window back into focus (Windows: Gamepad API requires window focus)
|
||||||
for (const win of BrowserWindow.getAllWindows()) win.focus()
|
for (const win of BrowserWindow.getAllWindows()) win.focus()
|
||||||
})
|
})
|
||||||
ipcMain.handle(
|
safeHandle(
|
||||||
IPC.playbackReportPosition,
|
IPC.playbackReportPosition,
|
||||||
(_e, vodId: string, positionSeconds: number, durationSeconds: number) => {
|
(vodId: string, positionSeconds: number, durationSeconds: number) => {
|
||||||
playback.updateVodPosition(vodId, positionSeconds, durationSeconds)
|
playback.updateVodPosition(vodId, positionSeconds, durationSeconds)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export function getStreamUrl(url: string, quality: string): Promise<string> {
|
|||||||
if (code === 0 && hlsUrl) {
|
if (code === 0 && hlsUrl) {
|
||||||
resolve(hlsUrl)
|
resolve(hlsUrl)
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(`streamlink --stream-url exit ${code}, stdout: ${out.trim().slice(0, 200)}`))
|
reject(new Error(`streamlink --stream-url ${url} ${quality} exit ${code}, stdout: ${out.trim().slice(0, 200)}`))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
VodChapter,
|
VodChapter,
|
||||||
VodInfo
|
VodInfo
|
||||||
} from './types'
|
} from './types'
|
||||||
|
import log from 'electron-log/main'
|
||||||
|
|
||||||
function parseDuration(d: string): number {
|
function parseDuration(d: string): number {
|
||||||
let s = 0
|
let s = 0
|
||||||
@@ -103,7 +104,7 @@ export class HelixClient {
|
|||||||
for (const id of chunk) url.searchParams.append('user_id', id)
|
for (const id of chunk) url.searchParams.append('user_id', id)
|
||||||
url.searchParams.set('first', '100')
|
url.searchParams.set('first', '100')
|
||||||
const res = await fetch(url.toString(), { headers: await this.authHeaders() })
|
const res = await fetch(url.toString(), { headers: await this.authHeaders() })
|
||||||
if (!res.ok) continue
|
if (!res.ok) { log.warn('[helixClient] getLiveStreams batch failed, partial result:', res.status); continue }
|
||||||
const data = (await res.json()) as { data: HelixStream[] }
|
const data = (await res.json()) as { data: HelixStream[] }
|
||||||
for (const stream of data.data) map.set(stream.user_id, stream)
|
for (const stream of data.data) map.set(stream.user_id, stream)
|
||||||
}
|
}
|
||||||
@@ -117,7 +118,7 @@ export class HelixClient {
|
|||||||
const url = new URL(`${BASE}/users`)
|
const url = new URL(`${BASE}/users`)
|
||||||
for (const id of chunk) url.searchParams.append('id', id)
|
for (const id of chunk) url.searchParams.append('id', id)
|
||||||
const res = await fetch(url.toString(), { headers: await this.authHeaders() })
|
const res = await fetch(url.toString(), { headers: await this.authHeaders() })
|
||||||
if (!res.ok) continue
|
if (!res.ok) { log.warn('[helixClient] getProfileImages batch failed, partial result:', res.status); continue }
|
||||||
const data = (await res.json()) as { data: HelixUser[] }
|
const data = (await res.json()) as { data: HelixUser[] }
|
||||||
for (const user of data.data) map.set(user.id, user.profile_image_url)
|
for (const user of data.data) map.set(user.id, user.profile_image_url)
|
||||||
}
|
}
|
||||||
@@ -165,10 +166,11 @@ export class HelixClient {
|
|||||||
url.searchParams.set('game_id', g.id)
|
url.searchParams.set('game_id', g.id)
|
||||||
url.searchParams.set('first', '100')
|
url.searchParams.set('first', '100')
|
||||||
const res = await fetch(url.toString(), { headers })
|
const res = await fetch(url.toString(), { headers })
|
||||||
if (!res.ok) return undefined
|
if (!res.ok) { log.warn('[helixClient] viewer-count fetch failed for game', g.id, res.status); return undefined }
|
||||||
const d = (await res.json()) as { data: HelixStream[] }
|
const d = (await res.json()) as { data: HelixStream[] }
|
||||||
return d.data.reduce((sum, s) => sum + s.viewer_count, 0)
|
return d.data.reduce((sum, s) => sum + s.viewer_count, 0)
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
log.warn('[helixClient] viewer-count fetch failed for game', g.id, err)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -252,7 +254,7 @@ export class HelixClient {
|
|||||||
query: HelixClient.CHAPTERS_QUERY
|
query: HelixClient.CHAPTERS_QUERY
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
if (!res.ok) return []
|
if (!res.ok) { log.warn('[helixClient] getVodChapters non-ok status:', res.status); return [] }
|
||||||
const json = await res.json() as Array<{
|
const json = await res.json() as Array<{
|
||||||
data?: { video?: { moments?: { edges: Array<{ node: {
|
data?: { video?: { moments?: { edges: Array<{ node: {
|
||||||
positionMilliseconds: number
|
positionMilliseconds: number
|
||||||
@@ -270,7 +272,8 @@ export class HelixClient {
|
|||||||
gameName: node.details?.game?.name ?? node.description,
|
gameName: node.details?.game?.name ?? node.description,
|
||||||
gameId: node.details?.game?.id ?? null
|
gameId: node.details?.game?.id ?? null
|
||||||
}))
|
}))
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
log.warn('[helixClient] getVodChapters failed, returning [] (chapters optional):', err)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'electron-log/preload'
|
||||||
import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron'
|
import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron'
|
||||||
import { IPC } from '../main/ipc/channels'
|
import { IPC } from '../main/ipc/channels'
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Component, type ErrorInfo, type ReactNode } from 'react'
|
||||||
|
import log from 'electron-log/renderer'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
error: Error | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends Component<Props, State> {
|
||||||
|
state: State = { error: null }
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { error }
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||||
|
log.error('[error-boundary]', error, errorInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
if (this.state.error) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100vh',
|
||||||
|
background: '#0e0e10',
|
||||||
|
color: '#efeff1',
|
||||||
|
gap: 16,
|
||||||
|
padding: 32,
|
||||||
|
fontFamily: 'sans-serif'
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: 24, color: '#ff6b6b' }}>Ein Fehler ist aufgetreten</div>
|
||||||
|
<pre style={{ fontSize: 12, color: '#adadb8', maxWidth: 640, whiteSpace: 'pre-wrap', textAlign: 'left' }}>
|
||||||
|
{this.state.error.message}
|
||||||
|
</pre>
|
||||||
|
<button
|
||||||
|
onClick={() => location.reload()}
|
||||||
|
style={{
|
||||||
|
padding: '10px 24px',
|
||||||
|
background: '#9147ff',
|
||||||
|
color: '#fff',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 16,
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Neu laden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return this.props.children
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import Hls from 'hls.js'
|
import Hls from 'hls.js'
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
|
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
|
||||||
import { POSITION_REPORT_INTERVAL_MS } from '../constants/playback'
|
import { POSITION_REPORT_INTERVAL_MS } from '../constants/playback'
|
||||||
|
import log from 'electron-log/renderer'
|
||||||
|
|
||||||
export interface VideoPlayerHandle {
|
export interface VideoPlayerHandle {
|
||||||
seek(delta: number): void
|
seek(delta: number): void
|
||||||
@@ -81,7 +82,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, Props>(
|
|||||||
hlsRef.current = null
|
hlsRef.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[VideoPlayer] Hls.isSupported:', Hls.isSupported(), '| URL:', hlsUrl.slice(0, 80))
|
log.info('[VideoPlayer] Hls.isSupported:', Hls.isSupported(), '| URL:', hlsUrl.slice(0, 80))
|
||||||
|
|
||||||
if (Hls.isSupported()) {
|
if (Hls.isSupported()) {
|
||||||
const hls = new Hls({ enableWorker: false })
|
const hls = new Hls({ enableWorker: false })
|
||||||
@@ -89,30 +90,30 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, Props>(
|
|||||||
|
|
||||||
// Attach media first, then load the source — more reliable in Electron/Chromium
|
// Attach media first, then load the source — more reliable in Electron/Chromium
|
||||||
hls.attachMedia(video)
|
hls.attachMedia(video)
|
||||||
console.log('[VideoPlayer] attachMedia aufgerufen')
|
log.info('[VideoPlayer] attachMedia aufgerufen')
|
||||||
|
|
||||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||||
console.log('[VideoPlayer] MEDIA_ATTACHED — lade Source')
|
log.info('[VideoPlayer] MEDIA_ATTACHED — lade Source')
|
||||||
hls.loadSource(hlsUrl)
|
hls.loadSource(hlsUrl)
|
||||||
})
|
})
|
||||||
|
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
console.log('[VideoPlayer] MANIFEST_PARSED — readyState:', video.readyState, 'networkState:', video.networkState)
|
log.info('[VideoPlayer] MANIFEST_PARSED — readyState:', video.readyState, 'networkState:', video.networkState)
|
||||||
if (startPosition > 0 && !isLive) {
|
if (startPosition > 0 && !isLive) {
|
||||||
video.currentTime = startPosition
|
video.currentTime = startPosition
|
||||||
}
|
}
|
||||||
// With the correct order (attachMedia → MEDIA_ATTACHED → loadSource)
|
// With the correct order (attachMedia → MEDIA_ATTACHED → loadSource)
|
||||||
// the MediaSource is already attached at MANIFEST_PARSED — play() is safe to call.
|
// the MediaSource is already attached at MANIFEST_PARSED — play() is safe to call.
|
||||||
video.play().then(() => {
|
video.play().then(() => {
|
||||||
console.log('[VideoPlayer] play() erfolgreich')
|
log.info('[VideoPlayer] play() erfolgreich')
|
||||||
}).catch((err: unknown) => {
|
}).catch((err: unknown) => {
|
||||||
console.error('[VideoPlayer] play() fehlgeschlagen:', err)
|
log.error('[VideoPlayer] play() fehlgeschlagen:', err)
|
||||||
onError?.(`Wiedergabe konnte nicht gestartet werden: ${err}`)
|
onError?.(`Wiedergabe konnte nicht gestartet werden: ${err}`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
hls.on(Hls.Events.ERROR, (_evt, data) => {
|
hls.on(Hls.Events.ERROR, (_evt, data) => {
|
||||||
console.warn('[VideoPlayer] hls.js Fehler:', data.type, data.details, data.fatal)
|
log.warn('[VideoPlayer] hls.js Fehler:', data.type, data.details, data.fatal)
|
||||||
if (data.fatal) {
|
if (data.fatal) {
|
||||||
onError?.(`HLS-Fehler: ${data.type} – ${data.details}`)
|
onError?.(`HLS-Fehler: ${data.type} – ${data.details}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { AXIS_THRESHOLD, REPEAT_INITIAL_MS, REPEAT_INTERVAL_MS } from '../constants/input'
|
import { AXIS_THRESHOLD, REPEAT_INITIAL_MS, REPEAT_INTERVAL_MS } from '../constants/input'
|
||||||
|
import log from 'electron-log/renderer'
|
||||||
|
|
||||||
const BUTTON_MAP: Record<number, string> = {
|
const BUTTON_MAP: Record<number, string> = {
|
||||||
0: 'Enter', // A
|
0: 'Enter', // A
|
||||||
@@ -70,11 +71,11 @@ export class GamepadService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onConnected = (e: GamepadEvent): void => {
|
private onConnected = (e: GamepadEvent): void => {
|
||||||
console.log(`[gamepad] connected: ${e.gamepad.id} (index ${e.gamepad.index})`)
|
log.info(`[gamepad] connected: ${e.gamepad.id} (index ${e.gamepad.index})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onDisconnected = (e: GamepadEvent): void => {
|
private onDisconnected = (e: GamepadEvent): void => {
|
||||||
console.log(`[gamepad] disconnected: ${e.gamepad.id} (index ${e.gamepad.index})`)
|
log.info(`[gamepad] disconnected: ${e.gamepad.id} (index ${e.gamepad.index})`)
|
||||||
this.padStates.delete(e.gamepad.index)
|
this.padStates.delete(e.gamepad.index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ import React from 'react'
|
|||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import { SettingsProvider } from './context/SettingsContext'
|
import { SettingsProvider } from './context/SettingsContext'
|
||||||
|
import { ErrorBoundary } from './components/ErrorBoundary'
|
||||||
import './styles/global.css'
|
import './styles/global.css'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
|
<ErrorBoundary>
|
||||||
<App />
|
<App />
|
||||||
|
</ErrorBoundary>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import LanguageBadge from '../components/LanguageBadge'
|
|||||||
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
|
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
|
||||||
import { formatViewers, formatViewersFull } from '../lib/formatting'
|
import { formatViewers, formatViewersFull } from '../lib/formatting'
|
||||||
import { STREAM_GAP, STREAM_W } from '../constants/ui'
|
import { STREAM_GAP, STREAM_W } from '../constants/ui'
|
||||||
|
import log from 'electron-log/renderer'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
hasFocus: boolean
|
hasFocus: boolean
|
||||||
@@ -50,7 +51,7 @@ export default function BrowseScreen({
|
|||||||
setFocusRegion('shelf')
|
setFocusRegion('shelf')
|
||||||
setLoadState('ok')
|
setLoadState('ok')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[BrowseScreen] Laden fehlgeschlagen:', err)
|
log.error('[BrowseScreen] Laden fehlgeschlagen:', err)
|
||||||
setLoadState('error')
|
setLoadState('error')
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
@@ -63,7 +64,7 @@ export default function BrowseScreen({
|
|||||||
setTopGames((prev) => [...prev, ...result.games])
|
setTopGames((prev) => [...prev, ...result.games])
|
||||||
setNextCursor(result.cursor)
|
setNextCursor(result.cursor)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[BrowseScreen] Nachladen fehlgeschlagen:', err)
|
log.error('[BrowseScreen] Nachladen fehlgeschlagen:', err)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingMore(false)
|
setIsLoadingMore(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
|||||||
import { GamepadHintItem } from '../components/GamepadPrompt'
|
import { GamepadHintItem } from '../components/GamepadPrompt'
|
||||||
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
|
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
|
||||||
import FocusableCard from '../components/FocusableCard'
|
import FocusableCard from '../components/FocusableCard'
|
||||||
|
import log from 'electron-log/renderer'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
game: GameInfo
|
game: GameInfo
|
||||||
@@ -26,7 +27,7 @@ export default function CategoryScreen({ game, onSelectChannel, onStartLive, onB
|
|||||||
setFocusedIndex(0)
|
setFocusedIndex(0)
|
||||||
setLoadState('ok')
|
setLoadState('ok')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[CategoryScreen] Laden fehlgeschlagen:', err)
|
log.error('[CategoryScreen] Laden fehlgeschlagen:', err)
|
||||||
setLoadState('error')
|
setLoadState('error')
|
||||||
}
|
}
|
||||||
}, [game.id])
|
}, [game.id])
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
|||||||
import FocusableCard from '../components/FocusableCard'
|
import FocusableCard from '../components/FocusableCard'
|
||||||
import { GamepadHintItem } from '../components/GamepadPrompt'
|
import { GamepadHintItem } from '../components/GamepadPrompt'
|
||||||
import type { FollowedChannelInfo } from '../types/t4sd'
|
import type { FollowedChannelInfo } from '../types/t4sd'
|
||||||
|
import log from 'electron-log/renderer'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
hasFocus: boolean
|
hasFocus: boolean
|
||||||
@@ -29,7 +30,7 @@ export default function FollowingScreen({
|
|||||||
setFocusedIndex(0)
|
setFocusedIndex(0)
|
||||||
setLoadState('ok')
|
setLoadState('ok')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[FollowingScreen] getFollowed fehlgeschlagen:', err)
|
log.error('[FollowingScreen] getFollowed fehlgeschlagen:', err)
|
||||||
setLoadState('error')
|
setLoadState('error')
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
|||||||
import FocusableCard from '../components/FocusableCard'
|
import FocusableCard from '../components/FocusableCard'
|
||||||
import { GamepadHintItem } from '../components/GamepadPrompt'
|
import { GamepadHintItem } from '../components/GamepadPrompt'
|
||||||
import type { FollowedChannelInfo } from '../types/t4sd'
|
import type { FollowedChannelInfo } from '../types/t4sd'
|
||||||
|
import log from 'electron-log/renderer'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
hasFocus: boolean
|
hasFocus: boolean
|
||||||
@@ -39,7 +40,7 @@ export default function StreamListScreen({
|
|||||||
setFocusedIndex(0)
|
setFocusedIndex(0)
|
||||||
setLoadState('ok')
|
setLoadState('ok')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[StreamListScreen] Laden fehlgeschlagen:', err)
|
log.error('[StreamListScreen] Laden fehlgeschlagen:', err)
|
||||||
setLoadState('error')
|
setLoadState('error')
|
||||||
}
|
}
|
||||||
}, [language])
|
}, [language])
|
||||||
@@ -52,7 +53,7 @@ export default function StreamListScreen({
|
|||||||
setStreams((prev) => [...prev, ...result.streams])
|
setStreams((prev) => [...prev, ...result.streams])
|
||||||
setNextCursor(result.cursor)
|
setNextCursor(result.cursor)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[StreamListScreen] Nachladen fehlgeschlagen:', err)
|
log.error('[StreamListScreen] Nachladen fehlgeschlagen:', err)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingMore(false)
|
setIsLoadingMore(false)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user