feat(player): zmiana jakosci w trakcie odtwarzania
Some checks are pending
Backend tests / test (push) Waiting to run
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:
parent
8841327b58
commit
c1ba536607
4 changed files with 84 additions and 2 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue