'use client'; import { useState, useRef, useCallback, useEffect, useMemo } from 'react'; import Hls from 'hls.js'; import { usePathname, useRouter } from 'next/navigation'; import { cn } from '@/lib/utils'; import { type AnnotationStroke, type AnnotationCanvasHandle } from '@/components/annotation-canvas'; import { PlayerCore } from '@/components/video-page/player-core'; import { VideoPageHeader } from '@/components/video-page/video-page-header'; import { ImagePreviewDialog } from '@/components/video-page/image-preview-dialog'; import { CompareVersionsDialog } from '@/components/video-page/compare-versions-dialog'; import { VideoPageLoading } from '@/components/video-page/video-page-loading'; import { VideoPageError } from '@/components/video-page/video-page-error'; import { GuestNameGate } from '@/components/video-page/guest-name-gate'; import { useCommentMedia } from '@/components/video-page/hooks/use-comment-media'; import { validateAnnotationStrokes } from '@/lib/validation'; import { resolveR2PlaybackUrl } from '@/lib/video-upload-validation'; import { useVersionActions } from '@/components/video-page/hooks/use-version-actions'; import { useWatchProgress } from '@/components/video-page/hooks/use-watch-progress'; import { useVideoPlayer } from '@/components/video-page/hooks/use-video-player'; import { useCommentActions } from '@/components/video-page/hooks/use-comment-actions'; import { useVideoPageData } from '@/components/video-page/hooks/use-video-page-data'; import { useCommentExport } from '@/components/video-page/hooks/use-comment-export'; import { useDownloadActions } from '@/components/video-page/hooks/use-download-actions'; import { useVersionDurationSync } from '@/components/video-page/hooks/use-version-duration-sync'; import { CommentComposer } from '@/components/video-page/comment-composer'; import { CommentsPane } from '@/components/video-page/comments-pane'; import { AssetsPane } from '@/components/video-page/assets-pane'; import { ApprovalRequestDialog } from '@/components/video-page/approval-request-dialog'; import { ApprovalRequestsPanel } from '@/components/video-page/approval-requests-panel'; import type { CommentMarker, PlayerAdapter, VideoPageCommentsActions, VideoPageCompareActions, VideoPageComposerActions, VideoPageHeaderActions, } from '@/components/video-page/types'; import { useApprovals } from '@/components/video-page/hooks/use-approvals'; import { useVideoAssets } from '@/components/video-page/hooks/use-video-assets'; import { useSubtitles } from '@/components/video-page/hooks/use-subtitles'; import { useYoutubeCaptions } from '@/components/video-page/hooks/use-youtube-captions'; import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn'; import { getSpeedOptionsForProvider } from '@/components/video-page/hooks/video-player-utils'; function formatTime(seconds: number): string { const totalSeconds = Math.floor(seconds); const hrs = Math.floor(totalSeconds / 3600); const mins = Math.floor((totalSeconds % 3600) / 60); const secs = totalSeconds % 60; if (hrs > 0) { return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } return `${mins}:${secs.toString().padStart(2, '0')}`; } function formatBunnyQualityLabel( level: { height?: number; bitrate?: number }, index: number ): string { if (typeof level.height === 'number' && level.height > 0) { return `${level.height}p`; } if (typeof level.bitrate === 'number' && level.bitrate > 0) { return `${Math.round(level.bitrate / 1000)} kbps`; } return `Level ${index + 1}`; } export type VideoPageMode = 'dashboard' | 'watch'; interface VideoPageContentProps { mode: VideoPageMode; videoId: string; projectId?: string; directUploadsEnabled?: boolean; directUploadProvider?: import('@/components/video-page/types').DirectUploadProvider; } export function VideoPageContent({ mode, videoId, projectId: propProjectId, directUploadsEnabled = false, directUploadProvider = 'bunny', }: VideoPageContentProps) { const iframeRef = useRef(null); const videoRef = useRef(null); const bunnyViewportRef = useRef(null); const hlsRef = useRef(null); const playerRef = useRef(null); const timelineRef = useRef(null); const progressRef = useRef(null); const playheadRef = useRef(null); const scrubReadoutRef = useRef(null); const videoContainerRef = useRef(null); const pathname = usePathname(); const scheduleWatchProgressSaveRef = useRef< (input: { progress: number; duration?: number; immediate?: boolean; force?: boolean }) => void >(() => {}); const { playingVoiceId, voiceProgress, voiceCurrentTime, voicePlaybackRate, playVoice, toggleVoiceSpeed, } = useCommentMedia(); const [showResolved, setShowResolved] = useState(false); const [activeSidePane, setActiveSidePane] = useState<'comments' | 'assets'>('comments'); const [highlightedAssetId, setHighlightedAssetId] = useState(null); const editAnnotationCanvasRef = useRef(null); // Annotation state const [isAnnotating, setIsAnnotating] = useState(false); const [annotationStrokes, setAnnotationStrokes] = useState(null); const [viewingAnnotation, setViewingAnnotation] = useState(null); const annotationCanvasRef = useRef(null); const [guestName, setGuestName] = useState(() => { if (typeof window === 'undefined') return ''; return localStorage.getItem('openframe_guest_name') || ''; }); const [guestNameConfirmed, setGuestNameConfirmed] = useState(() => { if (mode === 'dashboard') return true; if (typeof window === 'undefined') return false; return !!localStorage.getItem('openframe_guest_name'); }); // Compare dialog state const [showCompareDialog, setShowCompareDialog] = useState(false); const [selectedCompareVersions, setSelectedCompareVersions] = useState>(new Set()); const [showApprovalRequestDialog, setShowApprovalRequestDialog] = useState(false); const [showApprovalsPanel, setShowApprovalsPanel] = useState(false); const router = useRouter(); const { video, setVideo, loading, error, activeVersionId, setActiveVersionId, availableTags, selectedTagId, setSelectedTagId, projectId, fetchVersionComments, } = useVideoPageData({ mode, videoId, propProjectId, }); const isGuest = video ? !video.isAuthenticated : false; const canInitializePlayer = mode !== 'watch' || !isGuest || guestNameConfirmed; const normalizedGuestName = guestName.trim(); const canUploadAssets = !!video?.canUploadAssets; const canDownloadAssets = !!video?.canDownloadAssets; const { assets, isLoadingAssets, isCreatingAsset, deletingAssetIds, activeDownloadAssetId, hasMoreAssets, isLoadingMoreAssets, fetchAssets, loadMoreAssets, createAsset, deleteAsset, downloadAsset, getGuestUploadToken, } = useVideoAssets({ videoId, isAuthenticated: !!video?.isAuthenticated, canUploadAssets, canDownloadAssets, guestName: normalizedGuestName, }); const { showVersionDialog, setShowVersionDialog, newVersionUrl, newVersionLabel, setNewVersionLabel, newVersionSource, newVersionUrlError, isCreatingVersion, newVersionMode, setNewVersionMode, newVersionFile, setNewVersionFile, newVersionUploadProgress, newVersionUploadStatus, handleNewVersionUrlChange, handleCreateVersion, showDeleteVersionDialog, setShowDeleteVersionDialog, setVersionToDelete, isDeletingVersion, handleDeleteVersion, } = useVersionActions({ projectId: propProjectId, videoId, directUploadsEnabled, directUploadProvider, setVideo, activeVersionId, setActiveVersionId, }); // Memoize version selection handler to prevent recreating on each render const handleVersionSelect = useCallback( (versionId: string) => { setActiveVersionId(versionId); }, [setActiveVersionId] ); // Memoize toggle show resolved handler const handleToggleShowResolved = useCallback(() => { setShowResolved((prev) => !prev); }, []); const handleAssetMentionClick = useCallback((assetId: string) => { setActiveSidePane('assets'); setHighlightedAssetId(assetId); }, []); const { isExportingCsv, isExportingPdf, exportComments } = useCommentExport({ activeVersionId, showResolved, }); // Determine current user info for permission checks and comment display const currentUserId = video?.currentUserId || null; const currentUserName = video?.currentUserName || null; const canResolveComments = !!video?.canResolveComments; const canRequestApproval = !!video?.canRequestApproval; const canShareVideo = !!video?.canShareVideo; const { requests: approvalRequests, candidates: approvalCandidates, isLoadingRequests: isLoadingApprovals, isLoadingCandidates: isLoadingApprovalCandidates, isSubmittingRequest: isSubmittingApprovalRequest, isSubmittingDecision: isSubmittingApprovalDecision, isCancelingRequest: isCancelingApprovalRequest, activePendingRequest, error: approvalError, setError: setApprovalError, fetchRequests: fetchApprovalRequests, fetchCandidates: fetchApprovalCandidates, createRequest: createApprovalRequest, submitDecision: submitApprovalDecision, cancelRequest: cancelApprovalRequest, } = useApprovals({ projectId, activeVersionId, currentUserId, }); // Memoize active version lookup to avoid recalculating on every render const activeVersion = useMemo(() => { return ( video?.versions?.find((v) => v.id === activeVersionId) || video?.versions?.find((v) => v.isActive) || video?.versions?.[0] ); }, [video?.versions, activeVersionId]); const activeProviderId = activeVersion?.providerId; const speedOptions = getSpeedOptionsForProvider(activeProviderId); // Only the providers that play through our own