feat(player): start muted, unmute via button (autoplay-friendly)

Scenes/movies now start with sound OFF; user enables audio via a control
(UX request). NativeVideoPlayer: useVideoPlayer starts muted=true + speaker
toggle in top controls + always-visible "Tap for sound" pill while muted.
WebView path: injected autoplay sets muted=true (also makes muted autoplay
reliable per browser policy → faster CDN extraction); host player controls
handle unmute when the WebView is the actual surface.

Verified on emulator against the live runtime-1.1 OTA bundle: video starts
muted (pill shown), tap unmutes (pill clears).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-06-07 14:03:52 +02:00
parent 9d0cb7f26e
commit 4d14f3946b

View file

@ -161,8 +161,19 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
const player = useVideoPlayer(source, (p) => { const player = useVideoPlayer(source, (p) => {
p.loop = false; p.loop = false;
// Start wyciszony — dźwięk dopiero po tapnięciu przycisku (UX request 2026-06-07).
// Dodatkowo: muted autoplay nie wymaga audio-focus, więc nie ucisza muzyki usera
// przy podglądzie i nie zaskakuje głośnym startem.
p.muted = true;
p.play(); p.play();
}); });
const [muted, setMuted] = React.useState(true);
const toggleMute = React.useCallback(() => {
const next = !player.muted;
player.muted = next;
setMuted(next);
setControlsVisible(true);
}, [player]);
const statusEvent = useEvent(player, 'statusChange', { status: player.status }); const statusEvent = useEvent(player, 'statusChange', { status: player.status });
const status = statusEvent?.status ?? player.status; const status = statusEvent?.status ?? player.status;
@ -637,6 +648,14 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
</View> </View>
)} )}
{/* Unmute pill zawsze widoczny gdy wyciszone (poza fade controls), bo start jest
muted i user musi wiedzieć jak włączyć dźwięk. Tap unmute. */}
{muted && (
<Pressable onPress={toggleMute} style={styles.unmutePill} hitSlop={12}>
<Text style={styles.unmutePillText}>🔇 Tap for sound</Text>
</Pressable>
)}
{/* Controls overlay pointerEvents box-none żeby gesture overlay pod spodem {/* Controls overlay pointerEvents box-none żeby gesture overlay pod spodem
dalej dostawał taps poza interactive elements (back, play, scrubber). */} dalej dostawał taps poza interactive elements (back, play, scrubber). */}
<Animated.View <Animated.View
@ -654,6 +673,9 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
) : ( ) : (
<View style={{ flex: 1 }} /> <View style={{ flex: 1 }} />
)} )}
<Pressable onPress={toggleMute} hitSlop={16} style={styles.iconBtn}>
<Text style={styles.iconText}>{muted ? '🔇' : '🔊'}</Text>
</Pressable>
<Pressable onPress={toggleFullscreen} hitSlop={16} style={styles.iconBtn}> <Pressable onPress={toggleFullscreen} hitSlop={16} style={styles.iconBtn}>
<Text style={styles.iconText}>{isLandscape ? '' : '⛶'}</Text> <Text style={styles.iconText}>{isLandscape ? '' : '⛶'}</Text>
</Pressable> </Pressable>
@ -1053,7 +1075,12 @@ const INJECTED_JS = `
// i ma valid src, próbujemy odpalić. // i ma valid src, próbujemy odpalić.
if (el.tagName === 'VIDEO' && el.paused && (el.src || el.currentSrc)) { if (el.tagName === 'VIDEO' && el.paused && (el.src || el.currentSrc)) {
try { try {
el.muted = false; // Start muted (UX request 2026-06-07: dźwięk dopiero po geście usera).
// Bonus: muted autoplay jest dozwolony przez politykę przeglądarki, więc
// video faktycznie rusza bez gestu → szybsza ekstrakcja CDN URL (potem
// nav.replace na NativeVideoPlayer). W odsłoniętym WebView dźwięk włącza
// user przez własne kontrolki playera hostera.
el.muted = true;
const p = el.play(); const p = el.play();
if (p && p.catch) p.catch(function(){}); if (p && p.catch) p.catch(function(){});
} catch (e) {} } catch (e) {}
@ -1614,4 +1641,18 @@ const styles = StyleSheet.create({
borderRadius: 14, borderRadius: 14,
overflow: 'hidden', overflow: 'hidden',
}, },
unmutePill: {
position: 'absolute',
bottom: 96,
alignSelf: 'center',
backgroundColor: 'rgba(0,0,0,0.78)',
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 18,
},
unmutePillText: {
color: theme.fg,
fontSize: 14,
fontWeight: '700',
},
}); });