feat: 直播小窗播放(PiP)功能

- 新增 store/liveStore.ts:Zustand store,存储小窗直播状态(roomId/title/cover/hlsUrl)
- 新增 components/LiveMiniPlayer.tsx:可拖动悬浮迷你直播播放器
  - 原生端实际播放 HLS 流(react-native-video,有声)
  - Web 端降级展示封面图 + LIVE 徽标
  - LIVE 红点徽标 + 标题条底部
  - 关闭按钮(×)和点击进入直播间
  - HLS 出错时自动关闭(onError → clearLive)
  - 视频 MiniPlayer 激活时自动上移避免重叠
- _layout.tsx:全局挂载 <LiveMiniPlayer />
- live/[roomId].tsx:顶部栏添加小窗按钮(browsers-outline)
  - 仅直播中且有流地址时显示,离线时占位保持 title 居中
  - 点击后 setLive + router.back(),返回首页继续看直播
  - 进入同房间时自动 clearLive() 避免双播
This commit is contained in:
Developer
2026-03-25 13:16:16 +08:00
parent 13c73cdb25
commit 33a3148b38
4 changed files with 235 additions and 1 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import {
View,
Text,
@@ -18,6 +18,7 @@ import DanmakuList from "../../components/DanmakuList";
import { formatCount } from "../../utils/format";
import { proxyImageUrl } from "../../utils/imageUrl";
import { useTheme } from "../../utils/theme";
import { useLiveStore } from "../../store/liveStore";
type Tab = "intro" | "danmaku";
@@ -36,6 +37,15 @@ export default function LiveDetailScreen() {
const qualities = stream?.qualities ?? [];
const currentQn = stream?.qn ?? 0;
const { roomId: miniRoomId, setLive, clearLive } = useLiveStore();
// 进入该直播间时,若小窗正在播放同一房间,清除小窗避免双播
useEffect(() => {
if (miniRoomId === id) {
clearLive();
}
}, [id, miniRoomId]);
const actualRoomId = room?.roomid ?? id;
const { danmakus, giftCounts } = useLiveDanmaku(isLive ? actualRoomId : 0);
@@ -49,6 +59,19 @@ export default function LiveDetailScreen() {
<Text style={[styles.topTitle, { color: theme.text }]} numberOfLines={1}>
{room?.title ?? "直播间"}
</Text>
{isLive && hlsUrl ? (
<TouchableOpacity
style={styles.pipBtn}
onPress={() => {
setLive(id, room?.title ?? '', room?.keyframe ?? '', hlsUrl);
router.back();
}}
>
<Ionicons name="browsers-outline" size={22} color={theme.text} />
</TouchableOpacity>
) : (
<View style={styles.pipBtn} />
)}
</View>
{/* Player */}
@@ -177,6 +200,7 @@ const styles = StyleSheet.create({
borderBottomWidth: StyleSheet.hairlineWidth,
},
backBtn: { padding: 4 },
pipBtn: { padding: 4, width: 32, alignItems: 'center' },
topTitle: {
flex: 1,
fontSize: 15,