mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 09:33:09 +01:00
Update auf mpv 0.41.0 mit lua support
This commit is contained in:
@@ -125,14 +125,14 @@ echo ""
|
|||||||
echo "=== [5/6] SHA256-Prüfsummen für Manifest-Sources ==="
|
echo "=== [5/6] SHA256-Prüfsummen für Manifest-Sources ==="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
MPV_URL="https://github.com/mpv-player/mpv/archive/refs/tags/v0.38.0.tar.gz"
|
MPV_URL="https://github.com/mpv-player/mpv/archive/refs/tags/v0.41.0.tar.gz"
|
||||||
SL_URL="https://github.com/streamlink/streamlink/archive/refs/tags/6.11.0.tar.gz"
|
SL_URL="https://github.com/streamlink/streamlink/archive/refs/tags/6.11.0.tar.gz"
|
||||||
MANIFEST="flatpak/tv.twitch4steamdeck.App.yml"
|
MANIFEST="flatpak/tv.twitch4steamdeck.App.yml"
|
||||||
|
|
||||||
MPV_ARCHIVE="/tmp/mpv-0.38.0.tar.gz"
|
MPV_ARCHIVE="/tmp/mpv-0.41.0.tar.gz"
|
||||||
SL_ARCHIVE="/tmp/streamlink-6.11.0.tar.gz"
|
SL_ARCHIVE="/tmp/streamlink-6.11.0.tar.gz"
|
||||||
|
|
||||||
echo "Lade mpv 0.38.0 ..."
|
echo "Lade mpv 0.41.0 ..."
|
||||||
curl -L --progress-bar -o "$MPV_ARCHIVE" "$MPV_URL"
|
curl -L --progress-bar -o "$MPV_ARCHIVE" "$MPV_URL"
|
||||||
MPV_SHA=$(sha256sum "$MPV_ARCHIVE" | awk '{print $1}')
|
MPV_SHA=$(sha256sum "$MPV_ARCHIVE" | awk '{print $1}')
|
||||||
echo " mpv sha256: $MPV_SHA"
|
echo " mpv sha256: $MPV_SHA"
|
||||||
@@ -143,7 +143,7 @@ SL_SHA=$(sha256sum "$SL_ARCHIVE" | awk '{print $1}')
|
|||||||
echo " streamlink sha256: $SL_SHA"
|
echo " streamlink sha256: $SL_SHA"
|
||||||
|
|
||||||
# Platzhalter im Manifest ersetzen
|
# Platzhalter im Manifest ersetzen
|
||||||
sed -i "s/FILL_IN_sha256sum_of_mpv_0\.38\.0_tarball/$MPV_SHA/" "$MANIFEST"
|
sed -i "s/FILL_IN_sha256sum_of_mpv_0\.41\.0_tarball/$MPV_SHA/" "$MANIFEST"
|
||||||
sed -i "s/FILL_IN_sha256sum_of_streamlink_tarball/$SL_SHA/" "$MANIFEST"
|
sed -i "s/FILL_IN_sha256sum_of_streamlink_tarball/$SL_SHA/" "$MANIFEST"
|
||||||
echo " ✓ Manifest aktualisiert"
|
echo " ✓ Manifest aktualisiert"
|
||||||
|
|
||||||
@@ -171,3 +171,7 @@ echo " twitch4steamdeck.flatpak tv.twitch4steamdeck.App"
|
|||||||
echo " # Dann twitch4steamdeck.flatpak per USB/SSH auf Deck kopieren"
|
echo " # Dann twitch4steamdeck.flatpak per USB/SSH auf Deck kopieren"
|
||||||
echo " # Dort: flatpak install --user twitch4steamdeck.flatpak"
|
echo " # Dort: flatpak install --user twitch4steamdeck.flatpak"
|
||||||
echo "======================================================"
|
echo "======================================================"
|
||||||
|
echo "=== flatpak build-bundle ==="
|
||||||
|
flatpak build-bundle ~/.local/share/flatpak/repo twitch4steamdeck.flatpak tv.twitch4steamdeck.App
|
||||||
|
echo "=== Übertrage auf Steamdeck ==="
|
||||||
|
scp twitch4steamdeck.flatpak steamdeck:/home/deck/
|
||||||
@@ -74,8 +74,39 @@ modules:
|
|||||||
sha256: b964df9e5a066daa5e08d2dc82692c57ca27d00b8cc257e8e960c9f1cf26231b
|
sha256: b964df9e5a066daa5e08d2dc82692c57ca27d00b8cc257e8e960c9f1cf26231b
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# 1d) mpv — Media-Player für Live-Streams und VODs
|
# 1d) Lua 5.2 — Laufzeit + Dev-Dateien für mpv-Scripts/OSC/ytdl-Hooks
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
- name: lua
|
||||||
|
buildsystem: simple
|
||||||
|
build-commands:
|
||||||
|
- make clean
|
||||||
|
- make linux MYCFLAGS="$CFLAGS -fPIC" MYLDFLAGS="$LDFLAGS"
|
||||||
|
- make INSTALL_TOP=/app INSTALL_INC=/app/include/lua5.2 INSTALL_LIB=/app/lib INSTALL_MAN=/app/share/man install
|
||||||
|
- mkdir -p /app/lib/pkgconfig
|
||||||
|
- |
|
||||||
|
cat > /app/lib/pkgconfig/lua52.pc <<'EOF'
|
||||||
|
prefix=/app
|
||||||
|
exec_prefix=${prefix}
|
||||||
|
libdir=${exec_prefix}/lib
|
||||||
|
includedir=${prefix}/include/lua5.2
|
||||||
|
|
||||||
|
Name: Lua
|
||||||
|
Description: Embedded scripting language
|
||||||
|
Version: 5.2.4
|
||||||
|
Libs: -L${libdir} -llua -lm -ldl
|
||||||
|
Cflags: -I${includedir}
|
||||||
|
EOF
|
||||||
|
- cp /app/lib/pkgconfig/lua52.pc /app/lib/pkgconfig/lua5.2.pc
|
||||||
|
- cp /app/lib/pkgconfig/lua52.pc /app/lib/pkgconfig/lua-5.2.pc
|
||||||
|
sources:
|
||||||
|
- type: archive
|
||||||
|
url: https://www.lua.org/ftp/lua-5.2.4.tar.gz
|
||||||
|
sha256: b9e2e4aad6789b3b63a056d442f7b39f0ecfca3ae0f1fc0ae4e9614401b69f4b
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 1e) mpv — Media-Player für Live-Streams und VODs
|
||||||
# ffmpeg, libass, libplacebo, libva (VAAPI), X11 verfügbar.
|
# ffmpeg, libass, libplacebo, libva (VAAPI), X11 verfügbar.
|
||||||
|
# Lua aktiviert, damit das Bundle näher am Desktop-mpv liegt.
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
- name: mpv
|
- name: mpv
|
||||||
buildsystem: meson
|
buildsystem: meson
|
||||||
@@ -85,15 +116,15 @@ modules:
|
|||||||
- -Dx11=enabled
|
- -Dx11=enabled
|
||||||
- -Degl-x11=enabled
|
- -Degl-x11=enabled
|
||||||
- -Dalsa=disabled
|
- -Dalsa=disabled
|
||||||
- -Dlua=disabled
|
- -Dlua=enabled
|
||||||
- -Djavascript=disabled
|
- -Djavascript=disabled
|
||||||
- -Drubberband=disabled
|
- -Drubberband=disabled
|
||||||
- -Dvapoursynth=disabled
|
- -Dvapoursynth=disabled
|
||||||
- -Dbuild-date=false
|
- -Dbuild-date=false
|
||||||
sources:
|
sources:
|
||||||
- type: archive
|
- type: archive
|
||||||
url: https://github.com/mpv-player/mpv/archive/refs/tags/v0.38.0.tar.gz
|
url: https://github.com/mpv-player/mpv/archive/refs/tags/v0.41.0.tar.gz
|
||||||
sha256: 86d9ef40b6058732f67b46d0bbda24a074fae860b3eaae05bab3145041303066
|
sha256: ee21092a5ee427353392360929dc64645c54479aefdb5babc5cfbb5fad626209
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# 2) streamlink (Python) — liefert HLS-URLs aus Twitch-Streams
|
# 2) streamlink (Python) — liefert HLS-URLs aus Twitch-Streams
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import { randomUUID } from 'node:crypto'
|
||||||
|
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
|
||||||
|
|
||||||
|
interface ProxySession {
|
||||||
|
sourceUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAbsoluteUrl(value: string): boolean {
|
||||||
|
return /^https?:\/\//i.test(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveUrl(baseUrl: string, value: string): string {
|
||||||
|
if (isAbsoluteUrl(value)) return value
|
||||||
|
return new URL(value, baseUrl).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteTagUris(line: string, baseUrl: string, sessionId: string): string {
|
||||||
|
return line.replace(/URI="([^"]+)"/g, (_match, uri: string) => {
|
||||||
|
const absoluteUrl = resolveUrl(baseUrl, uri)
|
||||||
|
const proxied = `/segment/${sessionId}?url=${encodeURIComponent(absoluteUrl)}`
|
||||||
|
return `URI="${proxied}"`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readResponseText(res: Response): Promise<string> {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
||||||
|
}
|
||||||
|
return await res.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HlsProxyServer {
|
||||||
|
private server = createServer((req, res) => {
|
||||||
|
void this.handleRequest(req, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
private readonly sessions = new Map<string, ProxySession>()
|
||||||
|
private port: number | null = null
|
||||||
|
private listenPromise: Promise<void> | null = null
|
||||||
|
|
||||||
|
private async ensureListening(): Promise<void> {
|
||||||
|
if (this.port !== null) return
|
||||||
|
if (!this.listenPromise) {
|
||||||
|
this.listenPromise = new Promise((resolve, reject) => {
|
||||||
|
this.server.once('error', reject)
|
||||||
|
this.server.listen(0, '127.0.0.1', () => {
|
||||||
|
this.server.off('error', reject)
|
||||||
|
const address = this.server.address()
|
||||||
|
if (!address || typeof address === 'string') {
|
||||||
|
reject(new Error('HLS-Proxy konnte keinen Port binden'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.port = address.port
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await this.listenPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSession(sourceUrl: string): Promise<string> {
|
||||||
|
await this.ensureListening()
|
||||||
|
const sessionId = randomUUID()
|
||||||
|
this.sessions.set(sessionId, { sourceUrl })
|
||||||
|
return `http://127.0.0.1:${this.port}/playlist/${sessionId}.m3u8`
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSession(playlistUrl: string | null | undefined): void {
|
||||||
|
if (!playlistUrl) return
|
||||||
|
const match = /\/playlist\/([^/.]+)\.m3u8$/i.exec(playlistUrl)
|
||||||
|
if (!match) return
|
||||||
|
this.sessions.delete(match[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.sessions.clear()
|
||||||
|
if (this.port !== null) {
|
||||||
|
this.server.close()
|
||||||
|
}
|
||||||
|
this.port = null
|
||||||
|
this.listenPromise = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
||||||
|
try {
|
||||||
|
const requestUrl = new URL(req.url ?? '/', 'http://127.0.0.1')
|
||||||
|
if (requestUrl.pathname.startsWith('/playlist/')) {
|
||||||
|
await this.handlePlaylistRequest(requestUrl, res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (requestUrl.pathname.startsWith('/segment/')) {
|
||||||
|
await this.handlePassthroughRequest(requestUrl, req, res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.writeHead(404).end('Not found')
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
res.writeHead(500).end(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handlePlaylistRequest(requestUrl: URL, res: ServerResponse): Promise<void> {
|
||||||
|
const match = /\/playlist\/([^/.]+)\.m3u8$/i.exec(requestUrl.pathname)
|
||||||
|
if (!match) {
|
||||||
|
res.writeHead(404).end('Unknown playlist')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = this.sessions.get(match[1])
|
||||||
|
if (!session) {
|
||||||
|
res.writeHead(404).end('Session expired')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstream = await fetch(session.sourceUrl, {
|
||||||
|
headers: { Accept: 'application/vnd.apple.mpegurl, application/x-mpegURL, text/plain' }
|
||||||
|
})
|
||||||
|
const body = await readResponseText(upstream)
|
||||||
|
const rewritten = this.rewritePlaylist(body, session.sourceUrl, match[1])
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/vnd.apple.mpegurl',
|
||||||
|
'Cache-Control': 'no-store'
|
||||||
|
})
|
||||||
|
res.end(rewritten)
|
||||||
|
}
|
||||||
|
|
||||||
|
private rewritePlaylist(body: string, sourceUrl: string, sessionId: string): string {
|
||||||
|
const lines = body.split(/\r?\n/)
|
||||||
|
const output: string[] = []
|
||||||
|
let insertedPlaylistType = false
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
output.push('')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith('#EXT-X-PLAYLIST-TYPE:')) {
|
||||||
|
insertedPlaylistType = true
|
||||||
|
output.push(line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith('#')) {
|
||||||
|
output.push(rewriteTagUris(line, sourceUrl, sessionId))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const absoluteUrl = resolveUrl(sourceUrl, trimmed)
|
||||||
|
output.push(`/segment/${sessionId}?url=${encodeURIComponent(absoluteUrl)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!insertedPlaylistType) {
|
||||||
|
const headerIndex = output.findIndex((line) => line.startsWith('#EXTM3U'))
|
||||||
|
if (headerIndex >= 0) {
|
||||||
|
output.splice(headerIndex + 1, 0, '#EXT-X-PLAYLIST-TYPE:VOD')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handlePassthroughRequest(
|
||||||
|
requestUrl: URL,
|
||||||
|
req: IncomingMessage,
|
||||||
|
res: ServerResponse
|
||||||
|
): Promise<void> {
|
||||||
|
const upstreamUrl = requestUrl.searchParams.get('url')
|
||||||
|
if (!upstreamUrl || !isAbsoluteUrl(upstreamUrl)) {
|
||||||
|
res.writeHead(400).end('Missing upstream URL')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = new Headers()
|
||||||
|
const range = req.headers.range
|
||||||
|
if (typeof range === 'string' && range.length > 0) {
|
||||||
|
headers.set('Range', range)
|
||||||
|
}
|
||||||
|
const userAgent = req.headers['user-agent']
|
||||||
|
if (typeof userAgent === 'string' && userAgent.length > 0) {
|
||||||
|
headers.set('User-Agent', userAgent)
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstream = await fetch(upstreamUrl, { headers })
|
||||||
|
const responseHeaders: Record<string, string> = {
|
||||||
|
'Cache-Control': 'no-store'
|
||||||
|
}
|
||||||
|
for (const [key, value] of upstream.headers.entries()) {
|
||||||
|
if (key.toLowerCase() === 'transfer-encoding') continue
|
||||||
|
responseHeaders[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(upstream.status, responseHeaders)
|
||||||
|
if (!upstream.body) {
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const chunk of upstream.body as AsyncIterable<Uint8Array>) {
|
||||||
|
res.write(chunk)
|
||||||
|
}
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,7 +103,7 @@ export class PlaybackService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const effectiveStart = startSeconds !== undefined ? startSeconds : resumePos
|
const effectiveStart = startSeconds !== undefined ? startSeconds : resumePos
|
||||||
const proc = spawnMpv(hlsUrl, { ipcPath: this.ipcPath, isVod: true })
|
const proc = spawnMpv(hlsUrl, { ipcPath: this.ipcPath })
|
||||||
|
|
||||||
let mpvStderrBuf = ''
|
let mpvStderrBuf = ''
|
||||||
proc.stdout?.on('data', (d: Buffer) => console.log('[mpv]', d.toString().trim()))
|
proc.stdout?.on('data', (d: Buffer) => console.log('[mpv]', d.toString().trim()))
|
||||||
|
|||||||
@@ -70,30 +70,14 @@ export function getStreamUrl(url: string, quality: string): Promise<string> {
|
|||||||
|
|
||||||
export interface MpvOptions {
|
export interface MpvOptions {
|
||||||
ipcPath: string
|
ipcPath: string
|
||||||
isVod?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Startet mpv direkt mit einer URL (HLS oder lokal). IPC-Socket wird gesetzt. */
|
/** Startet mpv direkt mit einer URL (HLS oder lokal). IPC-Socket wird gesetzt. */
|
||||||
export function spawnMpv(url: string, { ipcPath, isVod = false }: MpvOptions): ChildProcess {
|
export function spawnMpv(url: string, { ipcPath }: MpvOptions): ChildProcess {
|
||||||
const args = [
|
const args = [
|
||||||
url,
|
url,
|
||||||
`--input-ipc-server=${ipcPath}`,
|
`--input-ipc-server=${ipcPath}`,
|
||||||
'--fullscreen',
|
'--fullscreen'
|
||||||
'--vo=gpu',
|
|
||||||
'--hwdec=auto',
|
|
||||||
'--force-window=yes'
|
|
||||||
]
|
]
|
||||||
if (isVod) {
|
|
||||||
args.push(
|
|
||||||
'--cache=yes',
|
|
||||||
'--demuxer-seekable-cache=yes',
|
|
||||||
'--force-seekable=yes',
|
|
||||||
'--cache-pause-initial=yes',
|
|
||||||
'--cache-pause-wait=2',
|
|
||||||
'--demuxer-max-bytes=150M',
|
|
||||||
'--demuxer-max-back-bytes=75M',
|
|
||||||
'--hr-seek=no'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return spawn(MPV_BIN, args, { stdio: ['ignore', 'pipe', 'pipe'] })
|
return spawn(MPV_BIN, args, { stdio: ['ignore', 'pipe', 'pipe'] })
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user