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:
2026-06-03 18:09:43 +08:00
co-authored by Claude Opus 4.8
parent b2aaef093d
commit a5c2cc3202
12 changed files with 1305 additions and 6 deletions
@@ -0,0 +1,22 @@
using LiveRecorder.Application.Models.RecordTasks;
namespace LiveRecorder.Application.Abstractions.Recording;
/// <summary>
/// Service for reading and parsing danmaku XML files produced by the recording system.
/// </summary>
public interface IDanmakuService
{
/// <summary>
/// Returns parsed danmaku events for a single recording task (one segment).
/// Returns null if the task does not exist or has no danmaku file.
/// </summary>
Task<DanmakuResponseDto?> GetTaskDanmakuAsync(Guid recordTaskId, CancellationToken cancellationToken = default);
/// <summary>
/// Returns aggregated danmaku events for all tasks in a recording session.
/// Returns null if the session does not exist or has no tasks with danmaku.
/// Offsets for segments beyond the first are adjusted so they are relative to the session start.
/// </summary>
Task<SessionDanmakuResponseDto?> GetSessionDanmakuAsync(Guid recordSessionId, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,96 @@
namespace LiveRecorder.Application.Models.RecordTasks;
/// <summary>
/// A single parsed danmaku event (chat message or non-chat event like gift/like/member/enter).
/// </summary>
public sealed class DanmakuEventDto
{
/// <summary>
/// Offset in seconds from the segment's video start time.
/// </summary>
public double OffsetSeconds { get; init; }
/// <summary>
/// Event type: "chat", "gift", "like", "member", "enter", "superchat", "live", "preparing", or platform-specific types.
/// </summary>
public required string Type { get; init; }
/// <summary>
/// For chat: the message text. For non-chat events: a descriptive label (e.g., "gift: rose x1").
/// </summary>
public required string Content { get; init; }
public string? User { get; init; }
public string? UserId { get; init; }
/// <summary>
/// Hex color string for chat messages (e.g., "FFFFFF"). Only meaningful for chat events.
/// </summary>
public string? Color { get; init; }
/// <summary>
/// Font size for chat messages (e.g., 25). Only meaningful for chat events.
/// </summary>
public int? FontSize { get; init; }
/// <summary>
/// Display mode for chat messages (1 = scroll right-to-left). Only meaningful for chat events.
/// </summary>
public int? Mode { get; init; }
/// <summary>
/// Unix timestamp in milliseconds when the event occurred (from the platform or recorded time).
/// </summary>
public long? TimestampMs { get; init; }
/// <summary>
/// For gift events: the gift name (e.g., "rose").
/// </summary>
public string? GiftName { get; init; }
/// <summary>
/// For gift/like events: the repeat count.
/// </summary>
public int? Count { get; init; }
/// <summary>
/// Truncated raw payload from the platform (for debugging).
/// </summary>
public string? Raw { get; init; }
}
/// <summary>
/// Danmaku response for a single recording task (one segment).
/// </summary>
public sealed class DanmakuResponseDto
{
public Guid RecordTaskId { get; init; }
public int SegmentIndex { get; init; }
public string? Platform { get; init; }
public string? RoomId { get; init; }
public string? LiveRoomId { get; init; }
public Guid RecordSessionId { get; init; }
/// <summary>
/// The UTC time when this segment's recording started (anchor for offset calculation).
/// </summary>
public DateTimeOffset? StartedAt { get; init; }
public required IReadOnlyList<DanmakuEventDto> Events { get; init; }
}
/// <summary>
/// Aggregated danmaku response for an entire recording session (all segments).
/// </summary>
public sealed class SessionDanmakuResponseDto
{
public Guid RecordSessionId { get; init; }
public required IReadOnlyList<DanmakuResponseDto> Tasks { get; init; }
}