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:
@@ -42,6 +42,10 @@ interface PlaylistVersion {
|
||||
thumbnailUrl: string | null;
|
||||
posterUrl: string | null;
|
||||
fps: number;
|
||||
frameCount?: number | null;
|
||||
handleHead?: number | null;
|
||||
handleTail?: number | null;
|
||||
pictureStartFrame?: number | null;
|
||||
approvalStatus: string;
|
||||
taskTitle: string;
|
||||
}
|
||||
@@ -276,6 +280,7 @@ export function PlaylistClient({ projects, userRole }: PlaylistClientProps) {
|
||||
fps={activeShot.latestVersion.fps}
|
||||
comments={[]}
|
||||
annotations={[]}
|
||||
handles={activeShot.latestVersion}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-3 text-zinc-600">
|
||||
|
||||
@@ -50,6 +50,10 @@ export async function GET(req: NextRequest) {
|
||||
thumbnailUrl: true,
|
||||
posterUrl: true,
|
||||
fps: true,
|
||||
frameCount: true,
|
||||
handleHead: true,
|
||||
handleTail: true,
|
||||
pictureStartFrame: true,
|
||||
approvalStatus: true,
|
||||
mimeType: true,
|
||||
createdAt: true,
|
||||
@@ -85,6 +89,10 @@ export async function GET(req: NextRequest) {
|
||||
thumbnailUrl: latest.thumbnailUrl,
|
||||
posterUrl: latest.posterUrl,
|
||||
fps: latest.fps,
|
||||
frameCount: latest.frameCount,
|
||||
handleHead: latest.handleHead,
|
||||
handleTail: latest.handleTail,
|
||||
pictureStartFrame: latest.pictureStartFrame,
|
||||
approvalStatus: latest.approvalStatus,
|
||||
taskTitle: latest.taskTitle,
|
||||
},
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useReviewStore } from "@/hooks/use-review-player";
|
||||
import { ReviewPasswordGate } from "@/components/clients/ReviewPasswordGate";
|
||||
import { HandleBadge } from "@/components/player/HandleBadge";
|
||||
|
||||
interface Comment {
|
||||
id: string;
|
||||
@@ -58,6 +59,10 @@ interface Version {
|
||||
fileUrl: string;
|
||||
fps: number;
|
||||
duration: number | null;
|
||||
frameCount?: number | null;
|
||||
handleHead?: number | null;
|
||||
handleTail?: number | null;
|
||||
pictureStartFrame?: number | null;
|
||||
approvalStatus: string;
|
||||
notes: string | null;
|
||||
// Top-level high-res flags (normalised from either shot or task.shot)
|
||||
@@ -513,6 +518,10 @@ export default function ClientReviewPage({
|
||||
comments={comments as any}
|
||||
versionId={version.id}
|
||||
onAddComment={handlePlayerAddComment}
|
||||
handles={version}
|
||||
// The director watches the cut as cut; handles are still one
|
||||
// toggle away, and flagged whenever the playhead is in one.
|
||||
defaultEditRangeOnly
|
||||
saveAnnotationFn={async (data) => {
|
||||
const res = await fetch(`/api/client/${token}/annotation`, {
|
||||
method: "POST",
|
||||
@@ -644,6 +653,7 @@ export default function ClientReviewPage({
|
||||
<span className="ml-auto font-mono text-xs text-amber-400/80">
|
||||
{frameToTimecode(comment.frameNumber, version.fps)}
|
||||
</span>
|
||||
<HandleBadge frame={comment.frameNumber} />
|
||||
</div>
|
||||
<p className="text-sm text-zinc-200 leading-relaxed">{comment.text}</p>
|
||||
{comment.canEdit && (
|
||||
@@ -692,8 +702,9 @@ export default function ClientReviewPage({
|
||||
|
||||
{commentsOpen && commentFrame !== null ? (
|
||||
<div className="border-t border-zinc-800 p-3 space-y-2">
|
||||
<p className="text-xs text-zinc-500 font-mono">
|
||||
<p className="flex items-center gap-2 text-xs text-zinc-500 font-mono">
|
||||
Frame {commentFrame} · {frameToTimecode(commentFrame, version.fps)}
|
||||
<HandleBadge frame={commentFrame} />
|
||||
</p>
|
||||
<Textarea
|
||||
placeholder="Add your note..."
|
||||
|
||||
@@ -28,6 +28,7 @@ import { submitApproval } from "@/actions/approvals";
|
||||
import { internallyApproveShot } from "@/actions/shots";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { ShareWithClientButton } from "@/components/versions/ShareWithClientButton";
|
||||
import { VersionHandlesButton } from "@/components/versions/VersionHandlesButton";
|
||||
import {
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
@@ -50,6 +51,9 @@ interface ReviewPageClientProps {
|
||||
fps: number;
|
||||
duration: number | null;
|
||||
frameCount: number | null;
|
||||
handleHead: number | null;
|
||||
handleTail: number | null;
|
||||
pictureStartFrame: number | null;
|
||||
approvalStatus: string;
|
||||
notes: string | null;
|
||||
isClientVisible: boolean;
|
||||
@@ -283,6 +287,20 @@ export function ReviewPageClient({
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* Handles — outside the breadcrumb so it never gets squeezed */}
|
||||
{userRole !== "CLIENT" && (
|
||||
<div className="shrink-0">
|
||||
<VersionHandlesButton
|
||||
key={version.id}
|
||||
versionId={version.id}
|
||||
handleHead={version.handleHead}
|
||||
handleTail={version.handleTail}
|
||||
pictureStartFrame={version.pictureStartFrame}
|
||||
onSaved={() => router.refresh()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Approval status */}
|
||||
<span
|
||||
className={cn(
|
||||
@@ -361,6 +379,7 @@ export function ReviewPageClient({
|
||||
versionId={version.id}
|
||||
onAddComment={handleAddComment}
|
||||
onAnnotationSaved={handleAnnotationSaved}
|
||||
handles={version}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user