export const PLAYBACK_COMMENT_VISIBLE_MS = 4_000; export function getPlaybackCommentVisibleMs(playbackRate = 1): number { const safePlaybackRate = Number.isFinite(playbackRate) && playbackRate > 0 ? playbackRate : 1; return PLAYBACK_COMMENT_VISIBLE_MS * safePlaybackRate; } export interface PlaybackComment { id: string; content: string; videoTimestampMs: number; authorEmail?: string | null; authorName?: string | null; parentId?: string | null; resolved?: boolean; } export function getActivePlaybackComments( comments: PlaybackComment[] | undefined, currentMs: number, playbackRate = 1, ): PlaybackComment[] { if (!comments?.length || !Number.isFinite(currentMs) || currentMs < 0) { return []; } const visibleMs = getPlaybackCommentVisibleMs(playbackRate); return comments .filter((comment) => { const timestamp = comment.videoTimestampMs; return ( comment.parentId == null && comment.resolved !== true && comment.content.trim().length > 0 && Number.isFinite(timestamp) && timestamp >= 0 && currentMs >= timestamp && currentMs < timestamp + visibleMs ); }) .sort( (a, b) => a.videoTimestampMs - b.videoTimestampMs || a.id.localeCompare(b.id), ); } export function PlaybackCommentOverlay({ comments, currentMs, playbackRate = 1, }: { comments: PlaybackComment[] | undefined; currentMs: number; playbackRate?: number; }) { const activeComments = getActivePlaybackComments( comments, currentMs, playbackRate, ); if (activeComments.length === 0) return null; return (
{activeComments.map((comment) => { const author = displayAuthor(comment); return (

{author}

{comment.content}

); })}
); } function displayAuthor(comment: PlaybackComment): string { const name = comment.authorName?.trim(); if (name) return name; const emailName = comment.authorEmail?.split("@")[0]?.trim(); return emailName || comment.authorEmail?.trim() || ""; } function initials(name: string): string { return ( name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((part) => part[0]?.toUpperCase() ?? "") .join("") || "?" ); }