feat(review): flag handles in the review player (metadata only)
Deploy / deploy (push) Successful in 3m31s
Deploy / deploy (push) Successful in 3m31s
Renders run 8 frames past the cut at each end. Directors were flagging glitches in unfixed handles as shot problems, so the player now marks them — without trimming or burning anything into the EXRs/MOV/MP4. - Version.handleHead/handleTail/pictureStartFrame, Export.handleHead/ handleTail; migration backfills pipeline exports and their preview versions from the manifest's handleFrames (else 8) - AE panel: "Includes handles [8] frames each end" -> manifest.review; older manifests fall back to source.handleFrames - finalize copies handles onto the Version; worker 1.6.4 reports the head slate length (pictureStartSeconds), else the player infers it as MP4 frames - EXR frameCount - Player: hatched HANDLE / SLATE regions and cut marks on the timeline, "HEAD/TAIL HANDLE · NOT IN EDIT" overlay on handle frames, HANDLE badge on notes, "Edit range only" toggle (default on in the client portal) - Staff Handles editor on the review page for manual uploads/corrections Also fixes two player bugs found while testing: timeToFrame floored float error (every seek landed a frame early), and J/reverse never ran (the keyboard effect's cleanup stopped it on the next render). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
RectangleHorizontal,
|
||||
Scissors,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -74,8 +75,20 @@ export function PlaybackControls({
|
||||
setShowAnnotations,
|
||||
setAspectPresetId,
|
||||
setBlankingMode,
|
||||
editRange,
|
||||
editRangeOnly,
|
||||
setEditRangeOnly,
|
||||
} = useReviewStore();
|
||||
|
||||
// Keeping to the edit, the ends of the transport are the cut points.
|
||||
const keepToEdit = editRangeOnly && !!editRange;
|
||||
const goToFrame = (frame: number) => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
video.pause();
|
||||
video.currentTime = frame / fps;
|
||||
};
|
||||
|
||||
const handleRateChange = (val: string) => {
|
||||
const rate = parseFloat(val);
|
||||
if (videoRef.current) videoRef.current.playbackRate = rate;
|
||||
@@ -115,13 +128,14 @@ export function PlaybackControls({
|
||||
size="icon-sm"
|
||||
className="text-zinc-400 hover:text-white"
|
||||
onClick={() => {
|
||||
if (videoRef.current) videoRef.current.currentTime = 0;
|
||||
if (keepToEdit && editRange) goToFrame(editRange.cutIn);
|
||||
else if (videoRef.current) videoRef.current.currentTime = 0;
|
||||
}}
|
||||
>
|
||||
<ChevronFirst className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Go to start</TooltipContent>
|
||||
<TooltipContent>{keepToEdit ? "Go to cut in" : "Go to start"}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
@@ -198,14 +212,15 @@ export function PlaybackControls({
|
||||
size="icon-sm"
|
||||
className="text-zinc-400 hover:text-white"
|
||||
onClick={() => {
|
||||
if (videoRef.current)
|
||||
if (keepToEdit && editRange) goToFrame(editRange.tailStart - 1);
|
||||
else if (videoRef.current)
|
||||
videoRef.current.currentTime = videoRef.current.duration;
|
||||
}}
|
||||
>
|
||||
<ChevronLast className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Go to end</TooltipContent>
|
||||
<TooltipContent>{keepToEdit ? "Go to cut out" : "Go to end"}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,6 +297,32 @@ export function PlaybackControls({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Edit range only — only for versions with handles marked */}
|
||||
{editRange && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className={cn(
|
||||
"text-zinc-400 hover:text-white",
|
||||
editRangeOnly && "text-amber-400 bg-amber-400/10"
|
||||
)}
|
||||
onClick={() => setEditRangeOnly(!editRangeOnly)}
|
||||
aria-pressed={editRangeOnly}
|
||||
aria-label="Edit range only"
|
||||
>
|
||||
<Scissors className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{editRangeOnly
|
||||
? "Playing the edit only — click to include handles"
|
||||
: "Play the edit only (skip handles)"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Annotation toggle */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
Reference in New Issue
Block a user