mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 09:33:09 +01:00
- Remove mpvController.ts and hlsTrimmer.ts entirely - Rewrite PlaybackService: streamlink --stream-url → HLS-URL → IPC event playback:hls-url - Add VideoPlayer.tsx (hls.js <video> wrapper, forwardRef with seek/seekTo/pause/play/stop/getCurrentTime) - Integrate VideoPlayer into ChannelScreen with direct videoRef calls (no IPC roundtrip for seek/pause) - Add global AppShell playback overlay for direct stream start (A-button in Browse/Category/StreamList) - Propagate onStartLive callback through BrowseScreen, CategoryScreen, StreamListScreen - Add CORS bypass for Twitch CDN (webRequest.onBeforeSendHeaders) and autoplay-policy switch - Update CSP: add media-src 'self' blob: for hls.js MediaSource API - Open chapter panel without stopping playback (pause + resume instead of stop/restart) - Drop IPC channels: seek, seekTo, resume, togglePause, getCurrentPosition, onTimeUpdate - Add IPC channel: playback:hls-url event with HlsUrlPayload
139 lines
4.6 KiB
JavaScript
139 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Testet die Backend-Playback-Pipeline: streamlink → HLS-URL → Manifest-Fetch → Segment-Fetch
|
|
*
|
|
* Usage:
|
|
* node scripts/test-playback-pipeline.mjs twitch.tv/<kanal>
|
|
* node scripts/test-playback-pipeline.mjs twitch.tv/videos/<vod-id>
|
|
*/
|
|
|
|
import { spawn } from 'node:child_process'
|
|
import { existsSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
|
|
const url = process.argv[2]
|
|
if (!url) {
|
|
console.error('Usage: node scripts/test-playback-pipeline.mjs twitch.tv/<kanal>')
|
|
process.exit(1)
|
|
}
|
|
|
|
function resolveStreamlinkBin() {
|
|
if (process.platform === 'win32') {
|
|
const candidates = [
|
|
join(process.env.LOCALAPPDATA ?? '', 'Programs', 'Streamlink', 'bin', 'streamlink.exe'),
|
|
join(process.env.ProgramFiles ?? '', 'Streamlink', 'bin', 'streamlink.exe'),
|
|
join(process.env['ProgramFiles(x86)'] ?? '', 'Streamlink', 'bin', 'streamlink.exe'),
|
|
]
|
|
for (const p of candidates) {
|
|
if (existsSync(p)) return p
|
|
}
|
|
}
|
|
return 'streamlink'
|
|
}
|
|
|
|
const STREAMLINK_BIN = resolveStreamlinkBin()
|
|
|
|
function pass(msg) { console.log(` ✓ PASS ${msg}`) }
|
|
function fail(msg) { console.log(` ✗ FAIL ${msg}`) }
|
|
function info(msg) { console.log(` → ${msg}`) }
|
|
|
|
async function run() {
|
|
console.log(`\n=== Playback Pipeline Test ===`)
|
|
console.log(`URL: ${url}`)
|
|
console.log(`Streamlink: ${STREAMLINK_BIN}`)
|
|
console.log(`Streamlink existiert: ${existsSync(STREAMLINK_BIN)}`)
|
|
console.log('')
|
|
|
|
// Schritt 1: Streamlink aufrufen
|
|
console.log('[1] Streamlink --stream-url aufrufen ...')
|
|
const { rawOut, rawErr, code, hlsUrl } = await new Promise((resolve) => {
|
|
const proc = spawn(STREAMLINK_BIN, ['--stream-url', url, 'best'], {
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
})
|
|
let out = ''
|
|
let err = ''
|
|
proc.stdout?.on('data', (d) => { out += d.toString() })
|
|
proc.stderr?.on('data', (d) => { err += d.toString() })
|
|
proc.on('error', (e) => resolve({ rawOut: '', rawErr: String(e), code: -1, hlsUrl: '' }))
|
|
proc.on('exit', (exitCode) => {
|
|
const lines = out.split('\n').map(l => l.trim())
|
|
const hlsUrl = lines.filter(l => l.startsWith('https://')).at(-1) ?? ''
|
|
resolve({ rawOut: out, rawErr: err, code: exitCode, hlsUrl })
|
|
})
|
|
})
|
|
|
|
info(`Exit-Code: ${code}`)
|
|
info(`stdout (roh):\n${rawOut.trim() || '(leer)'}`)
|
|
if (rawErr.trim()) info(`stderr: ${rawErr.trim().slice(0, 300)}`)
|
|
|
|
if (code !== 0) {
|
|
fail(`streamlink Exit-Code ${code}`)
|
|
process.exit(1)
|
|
}
|
|
if (!hlsUrl) {
|
|
fail('Keine https://-URL in stdout gefunden')
|
|
process.exit(1)
|
|
}
|
|
pass(`HLS-URL gefunden: ${hlsUrl.slice(0, 80)}...`)
|
|
|
|
// Schritt 2: Manifest fetchen
|
|
console.log('\n[2] HLS-Manifest fetchen ...')
|
|
let manifestText = ''
|
|
try {
|
|
const res = await fetch(hlsUrl, {
|
|
headers: {
|
|
'Origin': 'https://www.twitch.tv',
|
|
'Referer': 'https://www.twitch.tv/'
|
|
}
|
|
})
|
|
info(`HTTP Status: ${res.status}`)
|
|
manifestText = await res.text()
|
|
info(`Manifest (erste 200 Zeichen):\n${manifestText.slice(0, 200)}`)
|
|
|
|
if (!manifestText.startsWith('#EXTM3U')) {
|
|
fail('Antwort ist kein valides m3u8 (fehlt #EXTM3U)')
|
|
process.exit(1)
|
|
}
|
|
pass('#EXTM3U vorhanden — valides HLS-Manifest')
|
|
} catch (e) {
|
|
fail(`Manifest-Fetch fehlgeschlagen: ${e}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
// Schritt 3: Erstes Segment erreichbar?
|
|
console.log('\n[3] Erstes Segment erreichbar? ...')
|
|
const baseUrl = hlsUrl.substring(0, hlsUrl.lastIndexOf('/') + 1)
|
|
const segmentLine = manifestText.split('\n').find(l => l.trim() && !l.startsWith('#'))
|
|
if (!segmentLine) {
|
|
// Möglicherweise Master-Playlist → Sub-Playlist URL suchen
|
|
const subPlaylistLine = manifestText.split('\n').find(l => l.trim().startsWith('https://'))
|
|
if (subPlaylistLine) {
|
|
info(`Master-Playlist erkannt — Sub-Playlist: ${subPlaylistLine.trim().slice(0, 80)}`)
|
|
pass('Master-Playlist vorhanden (Sub-Playlists nicht weiter verfolgt)')
|
|
} else {
|
|
fail('Keine Segmente oder Sub-Playlists im Manifest gefunden')
|
|
}
|
|
} else {
|
|
const segmentUrl = segmentLine.startsWith('https://') ? segmentLine.trim() : `${baseUrl}${segmentLine.trim()}`
|
|
info(`Segment-URL: ${segmentUrl.slice(0, 80)}`)
|
|
try {
|
|
const segRes = await fetch(segmentUrl, { method: 'HEAD' })
|
|
info(`Segment HTTP Status: ${segRes.status}`)
|
|
if (segRes.ok) {
|
|
pass(`Erstes Segment erreichbar (HTTP ${segRes.status})`)
|
|
} else {
|
|
fail(`Erstes Segment HTTP ${segRes.status}`)
|
|
}
|
|
} catch (e) {
|
|
fail(`Segment-Fetch fehlgeschlagen: ${e}`)
|
|
}
|
|
}
|
|
|
|
console.log('\n=== Test abgeschlossen ===\n')
|
|
}
|
|
|
|
run().catch((e) => {
|
|
console.error('Unerwarteter Fehler:', e)
|
|
process.exit(1)
|
|
})
|