1"use client"
2
3import { forwardRef, useImperativeHandle, useRef, useState } from "react"
4import type { Group as KonvaGroup } from "konva/lib/Group"
5import type { Stage as KonvaStage } from "konva/lib/Stage"
6import { Stage } from "react-konva"
7
8import { StripStage } from "@/features/editor/StripStage"
9import type { EffectParams, FramePreset } from "@/features/editor/presets"
10import { CLIP_FRAME_COUNT } from "@/features/capture/clipRecorder"
11import { getLayout, type LayoutId } from "@/lib/layouts"
12import type { Shot, StickerInstance, StripText } from "@/stores/booth"
13
14import { recordStripVideo, type RecordedLiveVideo } from "./recordStripVideo"
15
16/* SH-03: panggung rekam di luar layar untuk versi hidup. Lebarnya 480 px
17 (bingkai klip), jadi video yang dihasilkan ringan. Panggung ini tidak pernah
18 menerima pilihan, dan lencana "Klip" tidak ikut terekam. */
19
20const RECORD_WIDTH = 480
21/* Dua putaran penuh: sekitar 3 detik (SH-03). */
22const RECORD_LOOPS = 2
23
24const IGNORE = () => {}
25
26export interface LiveStageHandle {
27 record(loops?: number): Promise<RecordedLiveVideo | null>
28}
29
30interface LiveStageProps {
31 layoutId: LayoutId
32 shots: (Shot | null)[]
33 frame: FramePreset
34 effect: EffectParams
35 stickers: StickerInstance[]
36 texts: StripText[]
37 showDate: boolean
38 qr?: { imageUrl: string | null }
39}
40
41export const LiveStage = forwardRef<LiveStageHandle, LiveStageProps>(
42 function LiveStage({ layoutId, shots, frame, effect, stickers, texts, showDate, qr }, ref) {
43 const stageRef = useRef<KonvaStage>(null)
44 const groupRef = useRef<KonvaGroup>(null)
45 const [index, setIndex] = useState(0)
46
47 const layout = getLayout(layoutId)
48 const scale = RECORD_WIDTH / layout.canvas.width
49 const frames = shots.map((shot) => shot?.clip ?? null)
50
51 useImperativeHandle(
52 ref,
53 () => ({
54 async record(loops = RECORD_LOOPS) {
55 const canvas =
56 stageRef.current?.getLayers()[0]?.getNativeCanvasElement() ?? null
57 const hasFrames = frames.some(
58 (item) => item !== null && item.length > 0
59 )
60 if (canvas === null || !hasFrames) {
61 return null
62 }
63 return recordStripVideo(canvas, setIndex, CLIP_FRAME_COUNT, loops)
64 },
65 }),
66 [frames]
67 )
68
69 return (
70 <div className="absolute top-0 left-[-9999px]" aria-hidden>
71 <Stage
72 ref={stageRef}
73 width={RECORD_WIDTH}
74 height={layout.canvas.height * scale}
75 listening={false}
76 >
77 <StripStage
78 layoutId={layoutId}
79 shots={shots}
80 frame={frame}
81 effect={effect}
82 stickers={stickers}
83 texts={texts}
84 showDate={showDate}
85 qr={qr}
86 live={{ frames, index, chrome: false }}
87 scale={scale}
88 groupRef={groupRef}
89 onSelect={IGNORE}
90 onChangeSticker={IGNORE}
91 onChangeText={IGNORE}
92 />
93 </Stage>
94 </div>
95 )
96 }
97)
98