feat: add danmaku replay player integration
- Add IDanmakuService interface and DanmakuService implementation to parse danmaku XML files
- Add GET /api/record-tasks/{id}/danmaku and GET /api/record-sessions/{id}/danmaku endpoints
- Add DanmakuPlayer Vue component with native video + CSS overlay danmaku rendering
- Add danmakuEngine.ts pure-TypeScript animation loop with binary search, track management, and event notifications
- Add useDanmakuPlayer composable for reusable danmaku data loading
- Integrate danmaku toggle button into RecordTaskDetailView
- Integrate danmaku replay modal dialog into RecordSessionDetailView segment table
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { ref } from "vue";
|
||||
import apiClient, { getApiErrorMessage } from "@/api/client";
|
||||
import type { DanmakuEvent, DanmakuResponse, SessionDanmakuResponse } from "@/types";
|
||||
|
||||
export function useDanmakuPlayer() {
|
||||
const danmakuEvents = ref<DanmakuEvent[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
|
||||
async function loadTaskDanmaku(taskId: string): Promise<boolean> {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
const { data } = await apiClient.get<DanmakuResponse>(`/record-tasks/${taskId}/danmaku`);
|
||||
danmakuEvents.value = data.events;
|
||||
return data.events.length > 0;
|
||||
} catch (err) {
|
||||
if ((err as { response?: { status?: number } })?.response?.status === 404) {
|
||||
error.value = "该分片没有弹幕数据。";
|
||||
} else {
|
||||
error.value = getApiErrorMessage(err, "弹幕数据加载失败。");
|
||||
}
|
||||
danmakuEvents.value = [];
|
||||
return false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSessionDanmaku(sessionId: string): Promise<boolean> {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
const { data } = await apiClient.get<SessionDanmakuResponse>(`/record-sessions/${sessionId}/danmaku`);
|
||||
// Flatten all task events into a single list with session-level offsets
|
||||
const allEvents: DanmakuEvent[] = [];
|
||||
for (const task of data.tasks) {
|
||||
allEvents.push(...task.events);
|
||||
}
|
||||
// Sort by offset for proper playback order
|
||||
allEvents.sort((a, b) => a.offsetSeconds - b.offsetSeconds);
|
||||
danmakuEvents.value = allEvents;
|
||||
return allEvents.length > 0;
|
||||
} catch (err) {
|
||||
if ((err as { response?: { status?: number } })?.response?.status === 404) {
|
||||
error.value = "该场次没有弹幕数据。";
|
||||
} else {
|
||||
error.value = getApiErrorMessage(err, "弹幕数据加载失败。");
|
||||
}
|
||||
danmakuEvents.value = [];
|
||||
return false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clear(): void {
|
||||
danmakuEvents.value = [];
|
||||
error.value = "";
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
return { danmakuEvents, loading, error, loadTaskDanmaku, loadSessionDanmaku, clear };
|
||||
}
|
||||
Reference in New Issue
Block a user