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,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; }
|
||||
}
|
||||
Reference in New Issue
Block a user