mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-07 23:18:38 +08:00
1
This commit is contained in:
96
hooks/useLiveDanmaku.ts
Normal file
96
hooks/useLiveDanmaku.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import type { DanmakuItem } from '../services/types';
|
||||
|
||||
function buildPacket(body: string, op: number): ArrayBuffer {
|
||||
const bodyBytes = new TextEncoder().encode(body);
|
||||
const total = 16 + bodyBytes.length;
|
||||
const buf = new ArrayBuffer(total);
|
||||
const view = new DataView(buf);
|
||||
view.setUint32(0, total, false); // total_len
|
||||
view.setUint16(4, 16, false); // header_len
|
||||
view.setUint16(6, 0, false); // ver=0 (no compression)
|
||||
view.setUint32(8, op, false); // op
|
||||
view.setUint32(12, 1, false); // seq
|
||||
new Uint8Array(buf).set(bodyBytes, 16);
|
||||
return buf;
|
||||
}
|
||||
|
||||
function parsePackets(buf: ArrayBuffer): { op: number; body: string }[] {
|
||||
const packets: { op: number; body: string }[] = [];
|
||||
let offset = 0;
|
||||
const view = new DataView(buf);
|
||||
while (offset + 16 <= buf.byteLength) {
|
||||
const totalLen = view.getUint32(offset, false);
|
||||
const headerLen = view.getUint16(offset + 4, false);
|
||||
const op = view.getUint32(offset + 8, false);
|
||||
if (totalLen < headerLen || offset + totalLen > buf.byteLength) break;
|
||||
const bodyLen = totalLen - headerLen;
|
||||
const bodyBytes = new Uint8Array(buf, offset + headerLen, bodyLen);
|
||||
const body = new TextDecoder('utf-8').decode(bodyBytes);
|
||||
packets.push({ op, body });
|
||||
offset += totalLen;
|
||||
}
|
||||
return packets;
|
||||
}
|
||||
|
||||
export function useLiveDanmaku(roomId: number): DanmakuItem[] {
|
||||
const [danmakus, setDanmakus] = useState<DanmakuItem[]>([]);
|
||||
const heartbeatRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!roomId) return;
|
||||
setDanmakus([]);
|
||||
|
||||
const ws = new WebSocket('wss://broadcastlv.chat.bilibili.com/sub');
|
||||
ws.binaryType = 'arraybuffer';
|
||||
|
||||
ws.onopen = () => {
|
||||
const authBody = JSON.stringify({
|
||||
roomid: roomId,
|
||||
platform: 'web',
|
||||
type: 2,
|
||||
uid: 0,
|
||||
protover: 0,
|
||||
});
|
||||
ws.send(buildPacket(authBody, 7));
|
||||
|
||||
heartbeatRef.current = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(buildPacket('', 2));
|
||||
}
|
||||
}, 30000);
|
||||
};
|
||||
|
||||
ws.onmessage = (e: MessageEvent) => {
|
||||
try {
|
||||
const packets = parsePackets(e.data as ArrayBuffer);
|
||||
for (const pkt of packets) {
|
||||
if (pkt.op === 5 && pkt.body) {
|
||||
try {
|
||||
const msg = JSON.parse(pkt.body);
|
||||
if (msg.cmd === 'DANMU_MSG') {
|
||||
const info = msg.info;
|
||||
const text = info[1] as string;
|
||||
const color = (info[3]?.[3] as number) ?? 0xffffff;
|
||||
const item: DanmakuItem = { time: 0, mode: 1, fontSize: 25, color, text };
|
||||
setDanmakus(prev => [...prev, item]);
|
||||
}
|
||||
} catch { /* ignore single message parse error */ }
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
ws.onerror = () => {};
|
||||
ws.onclose = () => {
|
||||
if (heartbeatRef.current) clearInterval(heartbeatRef.current);
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (heartbeatRef.current) clearInterval(heartbeatRef.current);
|
||||
ws.close();
|
||||
};
|
||||
}, [roomId]);
|
||||
|
||||
return danmakus;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { getLiveRoomDetail, getLiveAnchorInfo, getLiveStreamUrl } from '../services/bilibili';
|
||||
import type { LiveRoomDetail, LiveAnchorInfo, LiveStreamInfo } from '../services/types';
|
||||
|
||||
@@ -33,7 +33,7 @@ export function useLiveDetail(roomId: number) {
|
||||
]);
|
||||
if (cancelled) return;
|
||||
|
||||
let stream: LiveStreamInfo = { hlsUrl: '', flvUrl: '', qn: 0 };
|
||||
let stream: LiveStreamInfo = { hlsUrl: '', flvUrl: '', qn: 0, qualities: [] };
|
||||
if (room.live_status === 1) {
|
||||
stream = await getLiveStreamUrl(roomId);
|
||||
}
|
||||
@@ -50,5 +50,12 @@ export function useLiveDetail(roomId: number) {
|
||||
return () => { cancelled = true; };
|
||||
}, [roomId]);
|
||||
|
||||
return state;
|
||||
const changeQuality = useCallback(async (qn: number) => {
|
||||
try {
|
||||
const stream = await getLiveStreamUrl(roomId, qn);
|
||||
setState(prev => ({ ...prev, stream }));
|
||||
} catch { /* ignore */ }
|
||||
}, [roomId]);
|
||||
|
||||
return { ...state, changeQuality };
|
||||
}
|
||||
|
||||
42
hooks/useSearch.ts
Normal file
42
hooks/useSearch.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { searchVideos } from '../services/bilibili';
|
||||
import type { VideoItem } from '../services/types';
|
||||
|
||||
export function useSearch() {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [results, setResults] = useState<VideoItem[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const loadingRef = useRef(false);
|
||||
|
||||
const search = useCallback(async (kw: string, reset = false) => {
|
||||
if (!kw.trim() || loadingRef.current) return;
|
||||
loadingRef.current = true;
|
||||
setLoading(true);
|
||||
const currentPage = reset ? 1 : page;
|
||||
try {
|
||||
const items = await searchVideos(kw, currentPage);
|
||||
if (reset) {
|
||||
setResults(items);
|
||||
setPage(2);
|
||||
} else {
|
||||
setResults(prev => [...prev, ...items]);
|
||||
setPage(p => p + 1);
|
||||
}
|
||||
setHasMore(items.length >= 20);
|
||||
} catch {
|
||||
setHasMore(false);
|
||||
} finally {
|
||||
loadingRef.current = false;
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page]);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (!keyword.trim() || loadingRef.current || !hasMore) return;
|
||||
search(keyword, false);
|
||||
}, [keyword, hasMore, search]);
|
||||
|
||||
return { keyword, setKeyword, results, loading, hasMore, search, loadMore };
|
||||
}
|
||||
@@ -13,14 +13,17 @@ export function useVideoList() {
|
||||
const freshIdxRef = useRef(0);
|
||||
|
||||
const load = useCallback(async (reset = false) => {
|
||||
if (loadingRef.current) return;
|
||||
if (loadingRef.current) {
|
||||
if (reset) setRefreshing(false);
|
||||
return;
|
||||
}
|
||||
loadingRef.current = true;
|
||||
const idx = reset ? 0 : freshIdxRef.current;
|
||||
const idx = freshIdxRef.current;
|
||||
setLoading(true);
|
||||
try {
|
||||
const promises: [Promise<VideoItem[]>, Promise<LiveRoom[]>] = [
|
||||
getRecommendFeed(idx),
|
||||
reset || idx === 0
|
||||
(reset || idx === 0)
|
||||
? getLiveList(1, 0).catch(() => [] as LiveRoom[])
|
||||
: Promise.resolve([] as LiveRoom[]),
|
||||
];
|
||||
@@ -42,11 +45,11 @@ export function useVideoList() {
|
||||
}, []); // stable — no stale closure risk
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
console.log('Refreshing video list');
|
||||
setRefreshing(true);
|
||||
load(true);
|
||||
}, [load]);
|
||||
|
||||
const videos = useMemo(() => pages.flat(), [pages]);
|
||||
|
||||
return { videos, pages, liveRooms, loading, refreshing, load, refresh };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user