2026-03-16 21:13:26 +08:00
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
2026-03-17 19:52:26 +08:00
|
|
|
|
import { getLiveDanmakuHistory } from '../services/bilibili';
|
2026-03-16 21:13:26 +08:00
|
|
|
|
import type { DanmakuItem } from '../services/types';
|
|
|
|
|
|
|
2026-05-12 20:27:30 +08:00
|
|
|
|
const POLL_INTERVAL = 1500;
|
2026-03-16 21:13:26 +08:00
|
|
|
|
|
2026-03-17 19:52:26 +08:00
|
|
|
|
// 匹配 admin 消息中的礼物信息,如 "xxx 赠送了 辣条 x5" 或 "xxx 投喂 小心心 ×1"
|
|
|
|
|
|
const GIFT_PATTERN = /(?:赠送|投喂)\s*(?:了\s*)?(.+?)\s*[xX×]\s*(\d+)/;
|
2026-03-17 15:14:23 +08:00
|
|
|
|
|
2026-03-17 19:52:26 +08:00
|
|
|
|
// 常见礼物名列表,用于匹配
|
|
|
|
|
|
const KNOWN_GIFTS = new Set([
|
|
|
|
|
|
'辣条', '小心心', '打call', '干杯', '比心',
|
|
|
|
|
|
'吃瓜', '花式夸夸', '告白气球', '小电视飞船',
|
|
|
|
|
|
]);
|
2026-03-17 15:14:23 +08:00
|
|
|
|
|
2026-03-17 19:52:26 +08:00
|
|
|
|
export function useLiveDanmaku(roomId: number): {
|
|
|
|
|
|
danmakus: DanmakuItem[];
|
|
|
|
|
|
giftCounts: Record<string, number>;
|
|
|
|
|
|
} {
|
2026-03-16 21:13:26 +08:00
|
|
|
|
const [danmakus, setDanmakus] = useState<DanmakuItem[]>([]);
|
2026-03-17 19:52:26 +08:00
|
|
|
|
const [giftCounts, setGiftCounts] = useState<Record<string, number>>({});
|
|
|
|
|
|
const seenTextsRef = useRef<Set<string>>(new Set());
|
|
|
|
|
|
const seenAdminRef = useRef<Set<string>>(new Set());
|
|
|
|
|
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
2026-03-16 21:13:26 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!roomId) return;
|
|
|
|
|
|
setDanmakus([]);
|
2026-03-17 19:52:26 +08:00
|
|
|
|
setGiftCounts({});
|
|
|
|
|
|
seenTextsRef.current.clear();
|
|
|
|
|
|
seenAdminRef.current.clear();
|
2026-03-17 15:14:23 +08:00
|
|
|
|
let cancelled = false;
|
2026-03-16 21:13:26 +08:00
|
|
|
|
|
2026-03-17 19:52:26 +08:00
|
|
|
|
async function poll() {
|
2026-03-16 21:13:26 +08:00
|
|
|
|
try {
|
2026-03-17 19:52:26 +08:00
|
|
|
|
const { danmakus: items, adminMsgs } = await getLiveDanmakuHistory(roomId);
|
|
|
|
|
|
if (cancelled) return;
|
|
|
|
|
|
|
2026-05-12 20:27:30 +08:00
|
|
|
|
// 去重弹幕:以 uname + text + timeline 为 key
|
|
|
|
|
|
// 用 timeline 区分"同一人重复发同句"的合法重复(gethistory 多次轮询会返回相同尾部数据,靠 timeline 过滤掉)
|
2026-03-17 19:52:26 +08:00
|
|
|
|
const newItems = items.filter(item => {
|
2026-05-12 20:27:30 +08:00
|
|
|
|
const key = `${item.uname ?? ''}:${item.text}:${item.timeline ?? ''}`;
|
2026-03-17 19:52:26 +08:00
|
|
|
|
if (seenTextsRef.current.has(key)) return false;
|
|
|
|
|
|
seenTextsRef.current.add(key);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
});
|
|
|
|
|
|
if (newItems.length > 0) {
|
|
|
|
|
|
setDanmakus(prev => [...prev, ...newItems]);
|
|
|
|
|
|
}
|
2026-03-17 18:00:00 +08:00
|
|
|
|
|
2026-03-17 19:52:26 +08:00
|
|
|
|
// 解析 admin 消息中的礼物
|
|
|
|
|
|
const newGifts: Record<string, number> = {};
|
|
|
|
|
|
for (const msg of adminMsgs) {
|
|
|
|
|
|
// 去重 admin 消息
|
|
|
|
|
|
if (seenAdminRef.current.has(msg)) continue;
|
|
|
|
|
|
seenAdminRef.current.add(msg);
|
|
|
|
|
|
|
|
|
|
|
|
const match = msg.match(GIFT_PATTERN);
|
|
|
|
|
|
if (match) {
|
|
|
|
|
|
const giftName = match[1].trim();
|
|
|
|
|
|
const count = parseInt(match[2], 10);
|
|
|
|
|
|
if (KNOWN_GIFTS.has(giftName) && count > 0) {
|
|
|
|
|
|
newGifts[giftName] = (newGifts[giftName] ?? 0) + count;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Object.keys(newGifts).length > 0) {
|
|
|
|
|
|
setGiftCounts(prev => {
|
|
|
|
|
|
const next = { ...prev };
|
|
|
|
|
|
for (const [name, count] of Object.entries(newGifts)) {
|
|
|
|
|
|
next[name] = (next[name] ?? 0) + count;
|
|
|
|
|
|
}
|
|
|
|
|
|
return next;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-03-17 18:00:00 +08:00
|
|
|
|
|
2026-03-17 19:52:26 +08:00
|
|
|
|
// 防止 seen set 无限增长
|
|
|
|
|
|
if (seenTextsRef.current.size > 2000) {
|
|
|
|
|
|
const arr = Array.from(seenTextsRef.current);
|
|
|
|
|
|
seenTextsRef.current = new Set(arr.slice(-1000));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (seenAdminRef.current.size > 500) {
|
|
|
|
|
|
const arr = Array.from(seenAdminRef.current);
|
|
|
|
|
|
seenAdminRef.current = new Set(arr.slice(-250));
|
|
|
|
|
|
}
|
2026-03-17 15:14:23 +08:00
|
|
|
|
} catch (e) {
|
2026-03-17 19:52:26 +08:00
|
|
|
|
console.warn('[danmaku] poll failed:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
|
timerRef.current = setTimeout(poll, POLL_INTERVAL);
|
2026-03-17 15:14:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 19:52:26 +08:00
|
|
|
|
poll();
|
2026-03-16 21:13:26 +08:00
|
|
|
|
|
|
|
|
|
|
return () => {
|
2026-03-17 15:14:23 +08:00
|
|
|
|
cancelled = true;
|
2026-03-17 19:52:26 +08:00
|
|
|
|
if (timerRef.current) clearTimeout(timerRef.current);
|
2026-03-16 21:13:26 +08:00
|
|
|
|
};
|
|
|
|
|
|
}, [roomId]);
|
|
|
|
|
|
|
2026-03-17 19:52:26 +08:00
|
|
|
|
return { danmakus, giftCounts };
|
2026-03-16 21:13:26 +08:00
|
|
|
|
}
|