mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 15:43:08 +01:00
feat: add quality selection, improve chapter panel UX, and show VOD view counts
Quality selection during playback: - Add getAvailableQualities() in streamlink via --json, sorted by fixed order - Expose playback:get-qualities IPC channel (handlers, preload, type declarations) - Add quality panel in PlaybackOverlay: - Button + dropdown - Keyboard navigation (X: open, arrows: navigate, Enter: confirm, Escape: close) - Quality change triggers streamlink restart; VOD resumes at current position - Available in ChannelScreen (Live + VOD) and AppShell global overlay (Live) - Add QualityIcon Chapter panel UX improvements: - Video no longer pauses when chapter panel opens - PlaybackOverlay is hidden via conditional render instead - Remove wasPlayingBeforeChapters state (no longer needed) - X button seeks to video start when no chapters are found, with hint in panel UI VOD cards and defaults: - Show view count with eye icon on VOD cards (matching playback overlay style) - Change default streamBadgeMode from 'language' to 'both'
This commit is contained in:
@@ -31,6 +31,7 @@ export const IPC = {
|
||||
playbackStop: 'playback:stop',
|
||||
playbackPause: 'playback:pause',
|
||||
playbackReportPosition: 'playback:report-position',
|
||||
playbackGetQualities: 'playback:get-qualities',
|
||||
/** main → renderer */
|
||||
playbackEvent: 'playback:event',
|
||||
/** main → renderer: HLS-URL + Metadaten für den Renderer-seitigen Video-Player */
|
||||
@@ -95,8 +96,12 @@ export function registerIpcHandlers(
|
||||
)
|
||||
ipcMain.handle(
|
||||
IPC.playbackStartVod,
|
||||
(_e, vodId: string, channelLogin: string, title: string, durationSeconds: number, startSeconds?: number) =>
|
||||
playback.startVod(vodId, channelLogin, title, durationSeconds, startSeconds)
|
||||
(_e, vodId: string, channelLogin: string, title: string, durationSeconds: number, startSeconds?: number, quality?: string) =>
|
||||
playback.startVod(vodId, channelLogin, title, durationSeconds, startSeconds, quality)
|
||||
)
|
||||
ipcMain.handle(
|
||||
IPC.playbackGetQualities,
|
||||
(_e, twitchUrl: string) => playback.getAvailableQualities(twitchUrl)
|
||||
)
|
||||
ipcMain.handle(IPC.playbackStop, () => playback.stop())
|
||||
ipcMain.handle(IPC.playbackPause, () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { getStreamUrl } from './streamlink'
|
||||
import { getAvailableQualities, getStreamUrl } from './streamlink'
|
||||
import type { PlaybackEvent } from './types'
|
||||
import * as history from '../store/historyRepo'
|
||||
|
||||
@@ -32,12 +32,17 @@ export class PlaybackService extends EventEmitter {
|
||||
this.emit('playback-event', { kind: 'started', isLive: true } satisfies PlaybackEvent)
|
||||
}
|
||||
|
||||
getAvailableQualities(twitchUrl: string): Promise<string[]> {
|
||||
return getAvailableQualities(twitchUrl)
|
||||
}
|
||||
|
||||
async startVod(
|
||||
vodId: string,
|
||||
channelLogin: string,
|
||||
title: string,
|
||||
durationSeconds: number,
|
||||
startSeconds?: number
|
||||
startSeconds?: number,
|
||||
quality = 'best'
|
||||
): Promise<void> {
|
||||
const resumePos = history.getPosition(vodId)
|
||||
history.upsertVod({
|
||||
@@ -50,7 +55,7 @@ export class PlaybackService extends EventEmitter {
|
||||
|
||||
let hlsUrl: string
|
||||
try {
|
||||
hlsUrl = await getStreamUrl(`https://www.twitch.tv/videos/${vodId}`, 'best')
|
||||
hlsUrl = await getStreamUrl(`https://www.twitch.tv/videos/${vodId}`, quality)
|
||||
} catch (e) {
|
||||
this.emit('playback-event', {
|
||||
kind: 'error',
|
||||
|
||||
@@ -18,6 +18,43 @@ function resolveStreamlinkBin(): string {
|
||||
|
||||
const STREAMLINK_BIN = resolveStreamlinkBin()
|
||||
|
||||
const QUALITY_ORDER = ['best', '1080p60', '1080p', '720p60', '720p', '480p', '360p', '160p', 'audio_only', 'worst']
|
||||
|
||||
function sortQualities(qualities: string[]): string[] {
|
||||
return [...qualities].sort((a, b) => {
|
||||
const ai = QUALITY_ORDER.indexOf(a)
|
||||
const bi = QUALITY_ORDER.indexOf(b)
|
||||
if (ai === -1 && bi === -1) return a.localeCompare(b)
|
||||
if (ai === -1) return 1
|
||||
if (bi === -1) return -1
|
||||
return ai - bi
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fragt streamlink nach allen verfügbaren Qualitätsstufen für eine URL (--json).
|
||||
* Gibt eine sortierte Liste zurück; bei Fehler leeres Array.
|
||||
*/
|
||||
export function getAvailableQualities(url: string): Promise<string[]> {
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn(STREAMLINK_BIN, ['--json', url], {
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
})
|
||||
let out = ''
|
||||
proc.stdout?.on('data', (d: Buffer) => { out += d.toString() })
|
||||
proc.on('error', () => resolve([]))
|
||||
proc.on('exit', () => {
|
||||
try {
|
||||
const json = JSON.parse(out) as { streams?: Record<string, unknown> }
|
||||
const keys = Object.keys(json.streams ?? {})
|
||||
resolve(sortQualities(keys))
|
||||
} catch {
|
||||
resolve([])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fragt streamlink nach der direkten HLS-URL für einen Stream oder VOD.
|
||||
* Wird für Live-Streams (twitch.tv/<login>) und VODs (twitch.tv/videos/<id>) verwendet.
|
||||
|
||||
Reference in New Issue
Block a user