Procházet zdrojové kódy

fix: compare mode duration gating + dual playhead sync + per-video annotations

Duration gating:
- If 2 videos differ by >5 frames, enter "mismatch mode": banner with Cancel
  button shown, second video NOT rendered, user must cancel or pick another
- compareAsset shown in header during mismatch so user knows what was picked

Dual playhead sync:
- Added onTimelineSeek prop to VideoPlayer
- handleSeek calls parent via onTimelineSeek + seeks self immediately
- Main player in compare mode: onTimelineSeek={handleTimeUpdate}
- Compare player: receives updated currentTime via externalCurrentTime
- Both players seek to same time on timeline drag/click

Per-video annotations:
- Main player in compare mode gets allComments + visibleAnnotations (its own)
- Compare player gets empty arrays (has no annotations from this asset)
- Both players show their own speech bubbles if they have timestamped comments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude Dev před 1 měsícem
rodič
revize
778956402a

+ 27 - 14
src/app/review/[assetId]/page.tsx

@@ -75,8 +75,12 @@ export default function ReviewPage() {
     const diffFrames = Math.abs(dur1 - dur2) * fps;
     if (diffFrames > 5) {
       setCompareMismatch(
-        `Videos differ by ${Math.round(diffFrames)} frames. Playback may be out of sync.`
+        `Videos differ by ${Math.round(diffFrames)} frames. Cannot compare — timing mismatch.`
       );
+      // Show mismatch banner but don't enter compare mode
+      setCompareAsset(compareAssetArg);
+      setCompareMode(true);
+      return;
     }
     setCompareAsset(compareAssetArg);
     setCompareMode(true);
@@ -580,25 +584,27 @@ export default function ReviewPage() {
                   src={videoUrl}
                   mimeType={asset.mimeType}
                   fps={fps}
-                  comments={[]}
-                  visibleAnnotations={[]}
-                  drawMode={false}
+                  comments={allComments}
+                  visibleAnnotations={visibleAnnotations}
+                  drawMode={drawMode}
                   drawTool={drawTool}
                   drawColor={drawColor}
-                  onDrawModeChange={() => {}}
-                  onDrawToolChange={() => {}}
-                  onDrawColorChange={() => {}}
-                  pendingStrokes={[]}
-                  onStrokeComplete={() => {}}
+                  onDrawModeChange={setDrawMode}
+                  onDrawToolChange={setDrawTool}
+                  onDrawColorChange={setDrawColor}
+                  pendingStrokes={pendingStrokes}
+                  onStrokeComplete={handleStrokeComplete}
                   onTimeUpdate={handleTimeUpdate}
-                  onCommentClick={() => {}}
+                  onCommentClick={handleCommentSeek}
+                  onPlayingChange={setPlaying}
+                  onTimelineSeek={handleTimeUpdate}
                   externalCurrentTime={currentTime}
                   externalPlaying={playing}
                 />
               </div>
 
-              {/* Compare video */}
-              {compareAsset && (
+              {/* Compare video — only show when durations match */}
+              {compareAsset && !compareMismatch && (
                 <div className="flex-1 min-w-0">
                   <div className="text-xs mb-1 px-1 truncate" style={{ color: 'rgba(255,255,255,0.5)' }}>
                     {compareAsset.title}
@@ -650,12 +656,19 @@ export default function ReviewPage() {
 
           {/* ── Compare mismatch warning ─────────────────────────── */}
           {compareMode && compareMismatch && (
-            <div className="rounded-xl px-4 py-3 text-xs flex items-center gap-2"
+            <div className="rounded-xl px-4 py-3 text-xs flex items-center gap-3"
                  style={{ background: 'rgba(251,191,36,0.10)', border: '1px solid rgba(251,191,36,0.25)', color: '#FCD34D' }}>
               <svg className="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
                 <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
               </svg>
-              {compareMismatch}
+              <span className="flex-1">{compareMismatch}</span>
+              <button
+                onClick={handleExitCompare}
+                className="shrink-0 px-2 py-1 rounded-md transition-colors"
+                style={{ background: 'rgba(251,191,36,0.15)', color: '#FCD34D' }}
+              >
+                Cancel
+              </button>
             </div>
           )}
 

+ 13 - 3
src/components/video-player/VideoPlayer.tsx

@@ -30,6 +30,8 @@ interface Props {
   onTimeUpdate: (time: number) => void;
   onCommentClick: (comment: Comment) => void;
   onPlayingChange?: (playing: boolean) => void;
+  /** When provided, the parent intercepts timeline seeks in compare mode */
+  onTimelineSeek?: (time: number) => void;
   // ── Compare mode ───────────────────────────────────────────────────────
   /** When provided, this player is slave to the main player in compare mode */
   isComparePlayer?: boolean;
@@ -56,6 +58,7 @@ export function VideoPlayer({
   onTimeUpdate,
   onCommentClick,
   onPlayingChange,
+  onTimelineSeek,
   isComparePlayer = false,
   externalCurrentTime,
   externalPlaying,
@@ -346,9 +349,16 @@ export function VideoPlayer({
   const handleSeek = (time: number) => {
     const video = videoRef.current;
     if (!video) return;
-    video.currentTime = time;
-    setCurrentTime(time);
-    onTimeUpdate(time);
+    // In compare mode, parent intercepts timeline seeks to sync both players
+    if (onTimelineSeek) {
+      // Seek main player immediately; parent updates compare player via currentTime
+      video.currentTime = time;
+      onTimelineSeek(time);
+    } else {
+      video.currentTime = time;
+      setCurrentTime(time);
+      onTimeUpdate(time);
+    }
   };
 
   // Volume icon: 0 bars = muted, 1 bar = low, 2 = med, 3 = high