fix: 修复小窗闭包过期、详情页 loading 卡死、视频播放器性能问题

- 小窗 PanResponder 用 storeRef 替代闭包捕获,修复 roomId/bvid 始终为初始值
- useLiveDetail 用 ref 比对替代 cancelled 标志,防止 fetch 被意外取消
- 详情页 useLayoutEffect 同步清除小窗,BigVideoCard 小窗活跃时跳过播放
- 视频播放器竖屏/全屏互斥渲染,减半解码器占用
- onProgress 节流 + 退出全屏强制恢复播放
This commit is contained in:
Developer
2026-03-25 15:03:49 +08:00
parent 68b8b7d665
commit e3def7d01b
11 changed files with 147 additions and 118 deletions

View File

@@ -8,7 +8,6 @@ import {
Image,
Modal,
StatusBar,
useWindowDimensions,
Alert,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
@@ -36,14 +35,10 @@ export default function DownloadsScreen() {
const [playingUri, setPlayingUri] = useState<string | null>(null);
const [playingTitle, setPlayingTitle] = useState('');
const [shareTask, setShareTask] = useState<(DownloadTask & { key: string }) | null>(null);
const [showControls, setShowControls] = useState(true);
const { width, height } = useWindowDimensions();
const isLandscape = width > height;
async function openPlayer(uri: string, title: string) {
setPlayingTitle(title);
setPlayingUri(uri);
setShowControls(true);
await ScreenOrientation?.unlockAsync();
}
@@ -133,29 +128,23 @@ export default function DownloadsScreen() {
onRequestClose={closePlayer}
>
<StatusBar hidden />
<TouchableOpacity
activeOpacity={1}
style={styles.playerBg}
onPress={() => setShowControls(v => !v)}
>
<View style={styles.playerBg}>
{playingUri && (
<Video
source={{ uri: playingUri }}
style={isLandscape ? { width, height } : { width, height: width * 0.5625 }}
style={StyleSheet.absoluteFillObject}
resizeMode="contain"
controls={false}
controls
paused={false}
/>
)}
{showControls && (
<View style={[styles.playerBar, isLandscape && { top: 16 }]}>
<TouchableOpacity onPress={closePlayer} style={styles.closeBtn} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
<Ionicons name="chevron-back" size={24} color="#fff" />
</TouchableOpacity>
<Text style={styles.playerTitle} numberOfLines={1}>{playingTitle}</Text>
</View>
)}
</TouchableOpacity>
<View style={styles.playerBar}>
<TouchableOpacity onPress={closePlayer} style={styles.closeBtn} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
<Ionicons name="chevron-back" size={24} color="#fff" />
</TouchableOpacity>
<Text style={styles.playerTitle} numberOfLines={1}>{playingTitle}</Text>
</View>
</View>
</Modal>
</SafeAreaView>
);

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useLayoutEffect, useRef } from "react";
import {
View,
Text,
@@ -24,9 +24,16 @@ type Tab = "intro" | "danmaku";
export default function LiveDetailScreen() {
const { roomId } = useLocalSearchParams<{ roomId: string }>();
console.log("LiveDetailScreen params:", { roomId });
const router = useRouter();
const theme = useTheme();
const id = parseInt(roomId ?? "0", 10);
// 进入详情页时立即清除小窗useLayoutEffect 在绘制前同步执行)
useLayoutEffect(() => {
useLiveStore.getState().clearLive();
}, []);
const { room, anchor, stream, loading, error, changeQuality } =
useLiveDetail(id);
const [tab, setTab] = useState<Tab>("intro");
@@ -37,15 +44,7 @@ export default function LiveDetailScreen() {
const qualities = stream?.qualities ?? [];
const currentQn = stream?.qn ?? 0;
const { setLive, clearLive } = useLiveStore();
// 进入该直播间时,若小窗正在播放同一房间,清除小窗避免双播
// 仅在挂载时运行一次(用 getState 读值,不创建响应式依赖)
useEffect(() => {
if (useLiveStore.getState().roomId === id) {
clearLive();
}
}, []);
const setLive = useLiveStore(s => s.setLive);
const actualRoomId = room?.roomid ?? id;
const { danmakus, giftCounts } = useLiveDanmaku(isLive ? actualRoomId : 0);

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from "react";
import React, { useState, useEffect, useLayoutEffect, useRef } from "react";
import {
View,
Text,
@@ -23,6 +23,7 @@ import { formatCount, formatDuration } from "../../utils/format";
import { proxyImageUrl } from "../../utils/imageUrl";
import { DownloadSheet } from "../../components/DownloadSheet";
import { useTheme } from "../../utils/theme";
import { useLiveStore } from "../../store/liveStore";
type Tab = "intro" | "comments" | "danmaku";
@@ -30,6 +31,11 @@ export default function VideoDetailScreen() {
const { bvid } = useLocalSearchParams<{ bvid: string }>();
const router = useRouter();
const theme = useTheme();
// 进入视频详情页时立即清除直播小窗
useLayoutEffect(() => {
useLiveStore.getState().clearLive();
}, []);
const {
video,
playData,