feat(player): zmiana jakosci w trakcie odtwarzania
Some checks are pending
Backend tests / test (push) Waiting to run

User-request 89845707 ('quality customisation button in player'). Do tej pory
jakosc dalo sie wybrac TYLKO przed startem (PlaybackQualityModal w SceneDetail);
zeby ja zmienic, trzeba bylo wyjsc z playera i puscic scene od nowa.

SceneDetail przekazuje teraz cala liste jakosci w params.qualityLinks (label+url+
headers), player pokazuje ikone kolka gdy jest >1 wariant. Przelaczenie robi
replace() i przywraca pozycje sprzed podmiany (replace resetuje czas, wiec
zapisujemy currentTime i ustawiamy go po zaladowaniu), czyli user nie traci
miejsca w filmie. Lista jako overlay, nie Alert - Androidowy AlertDialog renderuje
max 3 przyciski, a jakosci bywa wiecej.
This commit is contained in:
goon-foss 2026-07-28 11:56:59 +02:00
parent 8841327b58
commit c1ba536607
4 changed files with 84 additions and 2 deletions

View file

@ -16,6 +16,13 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
{
id: '2026-07-28b',
date: 'July 2026',
items: [
'Change video quality while watching: tap the gear icon in the player. It keeps your position, so you do not lose your place.',
],
},
{
id: '2026-07-28',
date: 'July 2026',

View file

@ -82,6 +82,10 @@ export type RootStackParamList = {
refererHost?: string;
title?: string;
mode?: 'video' | 'webview';
// Lista jakości tego samego wideo (z resolve). Pozwala przełączyć jakość BEZ
// wychodzenia z playera (user-request 89845707) — player robi replace() i wraca
// na zapamiętaną pozycję. Undefined = brak wyboru (jedno źródło).
qualityLinks?: { label: string; url: string; headers?: Record<string, string> }[];
// Headers do bezpośredniego fetchu CDN URL'a (Referer + User-Agent). Mobile
// próbuje direct PIERWSZY — bandwidth idzie device→CDN, nie przez VPS proxy.
// Backend zwraca te headers w StreamLink.headers przy resolve.

View file

@ -30,6 +30,8 @@ import { theme } from '../theme';
import type { StreamLink } from '../types';
interface RouteParams {
/** Lista jakości tego samego wideo → przełącznik w playerze (user-request 89845707). */
qualityLinks?: { label: string; url: string; headers?: Record<string, string> }[];
url: string;
sceneId: string;
// 'tube:<sitetag>' źródła — do telemetrii odtwarzania (ranking źródeł). Opcjonalne;
@ -215,6 +217,35 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
}
}, [url]);
// Zmiana jakości BEZ wychodzenia z playera (user-request 89845707). Podmieniamy
// źródło i wracamy na tę samą pozycję — replace() resetuje czas, więc zapisujemy go
// przed podmianą i przywracamy po załadowaniu.
const qualityLinks = params.qualityLinks;
const [qualityOpen, setQualityOpen] = React.useState(false);
const [activeQuality, setActiveQuality] = React.useState<string | null>(null);
const switchQuality = React.useCallback(
(q: { label: string; url: string; headers?: Record<string, string> }) => {
setQualityOpen(false);
let at = 0;
try { at = player.currentTime || 0; } catch { at = 0; }
try {
player.replace(q.headers ? { uri: q.url, headers: q.headers } : q.url);
setActiveQuality(q.label);
setTimeout(() => {
try {
if (at > 1) player.currentTime = at;
player.play();
} catch {
// player zdążył się rozmontować — nic nie robimy
}
}, 600);
} catch {
// ignore
}
},
[player],
);
const statusEvent = useEvent(player, 'statusChange', { status: player.status });
const status = statusEvent?.status ?? player.status;
const playerError = statusEvent?.error;
@ -916,6 +947,11 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
) : (
<View style={{ flex: 1 }} />
)}
{qualityLinks && qualityLinks.length > 1 ? (
<Pressable onPress={() => setQualityOpen(true)} hitSlop={16} style={styles.iconBtn}>
<Text style={styles.iconText}></Text>
</Pressable>
) : null}
<Pressable onPress={copyLink} hitSlop={16} style={styles.iconBtn}>
<Text style={styles.iconText}>🔗</Text>
</Pressable>
@ -958,6 +994,30 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
</View>
</Animated.View>
{qualityOpen && qualityLinks ? (
// quality-picker-overlay: lista jakości nad wideo. Bez Modala — Androidowy
// AlertDialog renderuje max 3 przyciski, a jakości bywa więcej.
<View style={styles.overlay}>
<Text style={styles.overlayText}>Quality</Text>
{qualityLinks.map((q: { label: string; url: string; headers?: Record<string, string> }) => {
const on = (activeQuality ?? '') === q.label || (!activeQuality && q.url === url);
return (
<Pressable
key={q.label + q.url}
onPress={() => switchQuality(q)}
style={{ paddingVertical: 10, paddingHorizontal: 22 }}
>
<Text style={[styles.overlayText, on && { color: theme.accentGlow, fontWeight: '700' }]}>
{on ? '✓ ' : ''}{q.label}
</Text>
</Pressable>
);
})}
<Pressable onPress={() => setQualityOpen(false)} style={{ paddingVertical: 10 }}>
<Text style={styles.overlayText}>Cancel</Text>
</Pressable>
</View>
) : null}
{status === 'loading' && (
<View style={styles.overlay} pointerEvents="none">
<ActivityIndicator color={theme.fg} size="large" />

View file

@ -597,6 +597,7 @@ function PlaybackButton({
const openAsVideo = async (
link: StreamLink,
fallbackEmbedUrl?: string,
allLinks?: StreamLink[],
) => {
let refererHost: string | undefined;
try {
@ -638,6 +639,16 @@ function PlaybackButton({
headers: isDirect && link.headers ? link.headers : undefined,
fallbackProxyUrl: isDirect ? link.stream_url || undefined : undefined,
fallbackEmbedUrl,
// Pozostałe jakości → przełącznik w playerze (bez powrotu do listy źródeł).
qualityLinks: (allLinks && allLinks.length > 1)
? allLinks
.map((l) => ({
label: l.quality || 'auto',
url: l.direct_url || l.stream_url || '',
headers: l.headers || undefined,
}))
.filter((l) => !!l.url)
: undefined,
});
};
@ -761,7 +772,7 @@ function PlaybackButton({
const autoPick = pickAuto(directLinks);
if (autoPick) {
markStarted();
await openAsVideo(autoPick, fallbackEmbedUrl);
await openAsVideo(autoPick, fallbackEmbedUrl, directLinks);
return;
}
setQualityLinks(directLinks);
@ -824,7 +835,7 @@ function PlaybackButton({
// direct link dał 403. `qualityLinks` zawiera tylko stream_url-e, więc
// szukamy embed'a w `links` na resolve response wcześniej; tu już go nie
// mamy, ale to kompromis (rzadki przypadek multi-quality + dead direct).
await openAsVideo(link);
await openAsVideo(link, undefined, qualityLinks || undefined);
} else if (link.embed_url) {
let refererHost: string | undefined;
try {