@hyperframes/studio 0.7.55 → 0.7.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/hyperframes-player--Z69cEkE.js +459 -0
- package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/index-DfmYkU44.js +423 -0
- package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +172 -144
- package/dist/index.html +2 -2
- package/dist/index.js +43613 -40346
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +126 -125
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +53 -2
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +171 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +260 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +343 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +56 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +23 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +125 -1
- package/src/hooks/timelineEditingHelpers.ts +53 -292
- package/src/hooks/timelineMoveAdapter.test.ts +81 -0
- package/src/hooks/timelineMoveAdapter.ts +39 -0
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +245 -1
- package/src/hooks/useDomEditCommits.ts +162 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +423 -0
- package/src/hooks/useElementLifecycleOps.ts +53 -71
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +148 -18
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +398 -241
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +167 -132
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +55 -5
- package/src/player/components/Timeline.test.ts +183 -23
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +218 -523
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/TimelineRuler.tsx +58 -48
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +1026 -0
- package/src/player/components/timelineClipDragCommit.ts +498 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +42 -2
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +207 -75
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +60 -0
- package/src/player/components/timelineStackingSync.ts +27 -2
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
- package/src/player/components/useTimelineStackingSync.ts +94 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +52 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +5 -2
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +21 -81
- package/src/player/lib/timelineElementHelpers.ts +73 -5
- package/src/player/lib/timelineIframeHelpers.ts +4 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +99 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +67 -29
- package/src/utils/timelineDiscovery.ts +0 -17
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-BRwkMj0w.js +0 -423
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useCallback, type RefObject } from "react";
|
|
2
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
3
|
+
import { useDomEditActionsContextOptional } from "../../contexts/DomEditContext";
|
|
4
|
+
import { useStudioShellContextOptional } from "../../contexts/StudioContext";
|
|
5
|
+
import { findElementForSelection } from "../../components/editor/domEditingElement";
|
|
6
|
+
import { readEffectiveZIndex } from "../../components/editor/canvasContextMenuZOrder";
|
|
7
|
+
import type { StackingPatch } from "./timelineStackingSync";
|
|
8
|
+
|
|
9
|
+
interface UseTimelineStackingSyncInput {
|
|
10
|
+
expandedElementsRef: RefObject<TimelineElement[]>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Lane ↔ stacking unification (research/STAGE3-NEEDED-WIRING.md). Provision the
|
|
14
|
+
// two deps commitDraggedClipMove accepts so a lane-change drag also patches the
|
|
15
|
+
// edited clip's z-index. Both read the SAME preview iframe + z-order persist path
|
|
16
|
+
// the canvas right-click menu / LayersPanel use, so a timeline lane move and a
|
|
17
|
+
// menu z-edit produce one shared inline-style commit shape. Optional contexts:
|
|
18
|
+
// outside the NLE (standalone <Timeline>) these are null ⇒ deps undefined ⇒ the
|
|
19
|
+
// commit's z-sync is a no-op (backward compatible).
|
|
20
|
+
export function useTimelineStackingSync({ expandedElementsRef }: UseTimelineStackingSyncInput) {
|
|
21
|
+
const domEditActions = useDomEditActionsContextOptional();
|
|
22
|
+
const shell = useStudioShellContextOptional();
|
|
23
|
+
const zSyncPreviewIframeRef = domEditActions?.previewIframeRef ?? null;
|
|
24
|
+
const handleDomZIndexReorderCommit = domEditActions?.handleDomZIndexReorderCommit;
|
|
25
|
+
const zSyncActiveCompPath = shell?.activeCompPath ?? null;
|
|
26
|
+
|
|
27
|
+
// Resolve a TimelineElement to its live iframe HTMLElement via the same
|
|
28
|
+
// hfId ?? id ?? selector[selectorIndex] resolver the timeline's DOM patches use.
|
|
29
|
+
const resolveIframeElement = useCallback(
|
|
30
|
+
(el: TimelineElement): HTMLElement | null => {
|
|
31
|
+
const doc = zSyncPreviewIframeRef?.current?.contentDocument ?? null;
|
|
32
|
+
if (!doc) return null;
|
|
33
|
+
return findElementForSelection(
|
|
34
|
+
doc,
|
|
35
|
+
{
|
|
36
|
+
hfId: el.hfId,
|
|
37
|
+
id: el.domId ?? el.id,
|
|
38
|
+
selector: el.selector,
|
|
39
|
+
selectorIndex: el.selectorIndex,
|
|
40
|
+
sourceFile: el.sourceFile,
|
|
41
|
+
},
|
|
42
|
+
zSyncActiveCompPath,
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
[zSyncPreviewIframeRef, zSyncActiveCompPath],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// NaN (NOT 0) when the element can't be resolved in the preview iframe — a
|
|
49
|
+
// nested / unmounted sub-comp node, or one outside the active file. Fabricating
|
|
50
|
+
// z=0 would enter computeStackingPatches as a real overlapping neighbour at the
|
|
51
|
+
// z-floor and skew the boundary math; a non-finite value tells it to EXCLUDE this
|
|
52
|
+
// clip instead. NaN (rather than null) keeps the return assignable to the
|
|
53
|
+
// `(el) => number` reader contract the drag hook / commit deps declare.
|
|
54
|
+
const readClipZIndex = useCallback(
|
|
55
|
+
(el: TimelineElement): number => {
|
|
56
|
+
const node = resolveIframeElement(el);
|
|
57
|
+
return node ? readEffectiveZIndex(node) : Number.NaN;
|
|
58
|
+
},
|
|
59
|
+
[resolveIframeElement],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const applyStackingPatches = useCallback(
|
|
63
|
+
(patches: StackingPatch[], coalesceKey?: string) => {
|
|
64
|
+
if (!handleDomZIndexReorderCommit) return;
|
|
65
|
+
const entries = patches.flatMap((p) => {
|
|
66
|
+
const el = expandedElementsRef.current.find((e) => (e.key ?? e.id) === p.key);
|
|
67
|
+
const node = el && resolveIframeElement(el);
|
|
68
|
+
if (!el || !node) return [];
|
|
69
|
+
return [
|
|
70
|
+
{
|
|
71
|
+
element: node,
|
|
72
|
+
zIndex: p.zIndex,
|
|
73
|
+
id: el.domId ?? el.id,
|
|
74
|
+
selector: el.selector,
|
|
75
|
+
selectorIndex: el.selectorIndex,
|
|
76
|
+
sourceFile: el.sourceFile ?? zSyncActiveCompPath ?? "index.html",
|
|
77
|
+
// The store key: lets the commit update the store's zIndex
|
|
78
|
+
// synchronously (and roll it back on failure).
|
|
79
|
+
key: p.key,
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
});
|
|
83
|
+
// Forward the drag-commit's shared coalesce key so the z-reorder history
|
|
84
|
+
// entry merges with the lane change's move entry into one undo step.
|
|
85
|
+
if (entries.length) handleDomZIndexReorderCommit(entries, coalesceKey);
|
|
86
|
+
},
|
|
87
|
+
[handleDomZIndexReorderCommit, resolveIframeElement, zSyncActiveCompPath, expandedElementsRef],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Engage the z-sync only when the persist path is present (inside the NLE).
|
|
91
|
+
const zSyncEnabled = Boolean(handleDomZIndexReorderCommit && zSyncPreviewIframeRef);
|
|
92
|
+
|
|
93
|
+
return { readClipZIndex, applyStackingPatches, zSyncEnabled };
|
|
94
|
+
}
|
|
@@ -163,7 +163,14 @@ function buildChildElements(
|
|
|
163
163
|
key,
|
|
164
164
|
start: clamped.start,
|
|
165
165
|
duration: clamped.duration,
|
|
166
|
+
// `track` becomes a synthetic display row under the expanded host, but the
|
|
167
|
+
// factory-set `authoredTrack` (the child's data-track-index in ITS OWN
|
|
168
|
+
// file's coordinate space) and the runtime-computed `stackingContextId`
|
|
169
|
+
// must survive verbatim — lane persists and z-sync read them, they are
|
|
170
|
+
// never reconstructed from display lanes.
|
|
166
171
|
track: display.track + result.length,
|
|
172
|
+
authoredTrack: base.authoredTrack,
|
|
173
|
+
stackingContextId: base.stackingContextId,
|
|
167
174
|
expandedParentStart: editBasis.start,
|
|
168
175
|
domId,
|
|
169
176
|
selector,
|
|
@@ -192,12 +199,9 @@ function domSiblingClips(
|
|
|
192
199
|
start: host.start,
|
|
193
200
|
duration: host.duration,
|
|
194
201
|
track: host.track,
|
|
195
|
-
zIndex: 0,
|
|
196
|
-
stackingContextId: host.stackingContextId ?? host.domId ?? host.id ?? null,
|
|
197
202
|
kind: "element",
|
|
198
203
|
tagName: null,
|
|
199
204
|
compositionId: null,
|
|
200
|
-
compositionAncestors: host.compositionAncestors ?? [],
|
|
201
205
|
parentCompositionId: host.id ?? null,
|
|
202
206
|
compositionSrc: host.compositionSrc ?? null,
|
|
203
207
|
assetUrl: null,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { useRef, useCallback } from "react";
|
|
10
10
|
import { useCaptionStore } from "../../captions/store";
|
|
11
11
|
import { shouldIgnorePlaybackShortcutEvent, SHUTTLE_SPEEDS } from "../lib/playbackShortcuts";
|
|
12
|
+
import { canvasNudgeKeysClaimed } from "../../utils/canvasNudgeGate";
|
|
12
13
|
import { usePlayerStore } from "../store/playerStore";
|
|
13
14
|
import { stepFrameTime, STUDIO_PREVIEW_FPS } from "../lib/time";
|
|
14
15
|
import type { PlaybackAdapter } from "../lib/playbackTypes";
|
|
@@ -97,6 +98,10 @@ export function usePlaybackKeyboard({
|
|
|
97
98
|
togglePlay();
|
|
98
99
|
return;
|
|
99
100
|
}
|
|
101
|
+
// A nudgeable canvas selection owns the arrow keys (DomEditOverlay moves
|
|
102
|
+
// the element); frame-stepping would double-handle the same keystroke.
|
|
103
|
+
const arrowStep = e.code === "ArrowLeft" || e.code === "ArrowRight";
|
|
104
|
+
if (arrowStep && canvasNudgeKeysClaimed()) return;
|
|
100
105
|
if (e.code === "ArrowLeft") {
|
|
101
106
|
e.preventDefault();
|
|
102
107
|
stepFrames(e.shiftKey ? -10 : -1);
|
|
@@ -153,6 +153,61 @@ describe("useTimelinePlayer seek hydration", () => {
|
|
|
153
153
|
unmountWithAct(root);
|
|
154
154
|
unsubscribe();
|
|
155
155
|
});
|
|
156
|
+
|
|
157
|
+
it("does not settle from an unsupported runtime protocol message", () => {
|
|
158
|
+
const { api, root } = renderTimelinePlayerHarness();
|
|
159
|
+
const iframe = document.createElement("iframe");
|
|
160
|
+
const iframeWindow = {
|
|
161
|
+
postMessage: vi.fn(),
|
|
162
|
+
scrollTo: vi.fn(),
|
|
163
|
+
addEventListener: vi.fn(),
|
|
164
|
+
removeEventListener: vi.fn(),
|
|
165
|
+
} as Record<string, unknown>;
|
|
166
|
+
Object.defineProperty(iframe, "contentWindow", {
|
|
167
|
+
value: iframeWindow,
|
|
168
|
+
configurable: true,
|
|
169
|
+
});
|
|
170
|
+
Object.defineProperty(iframe, "contentDocument", {
|
|
171
|
+
value: document.implementation.createHTMLDocument("preview"),
|
|
172
|
+
configurable: true,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
act(() => {
|
|
176
|
+
api.iframeRef.current = iframe;
|
|
177
|
+
api.onIframeLoad();
|
|
178
|
+
});
|
|
179
|
+
expect(usePlayerStore.getState().timelineReady).toBe(false);
|
|
180
|
+
|
|
181
|
+
iframeWindow.__player = {
|
|
182
|
+
play: vi.fn(),
|
|
183
|
+
pause: vi.fn(),
|
|
184
|
+
seek: vi.fn(),
|
|
185
|
+
getTime: () => 0,
|
|
186
|
+
getDuration: () => 30,
|
|
187
|
+
isPlaying: () => false,
|
|
188
|
+
};
|
|
189
|
+
act(() => {
|
|
190
|
+
window.dispatchEvent(
|
|
191
|
+
new MessageEvent("message", {
|
|
192
|
+
source: iframeWindow as unknown as Window,
|
|
193
|
+
data: { source: "hf-preview", type: "state", protocolVersion: 999 },
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
expect(usePlayerStore.getState().timelineReady).toBe(false);
|
|
198
|
+
|
|
199
|
+
act(() => {
|
|
200
|
+
window.dispatchEvent(
|
|
201
|
+
new MessageEvent("message", {
|
|
202
|
+
source: iframeWindow as unknown as Window,
|
|
203
|
+
data: { source: "hf-preview", type: "state" },
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
expect(usePlayerStore.getState().timelineReady).toBe(true);
|
|
208
|
+
|
|
209
|
+
unmountWithAct(root);
|
|
210
|
+
});
|
|
156
211
|
});
|
|
157
212
|
|
|
158
213
|
describe("useTimelinePlayer audio controls (#835)", () => {
|
|
@@ -437,12 +437,19 @@ describe("anonymous timeline identity", () => {
|
|
|
437
437
|
});
|
|
438
438
|
|
|
439
439
|
describe("mergeTimelineElementsPreservingDowngrades", () => {
|
|
440
|
-
it("preserves missing
|
|
440
|
+
it("preserves missing sub-composition elements when a shorter manifest arrives", () => {
|
|
441
441
|
expect(
|
|
442
442
|
mergeTimelineElementsPreservingDowngrades(
|
|
443
443
|
[
|
|
444
444
|
{ id: "hero", tag: "div", start: 0, duration: 4, track: 0 },
|
|
445
|
-
{
|
|
445
|
+
{
|
|
446
|
+
id: "cta",
|
|
447
|
+
tag: "div",
|
|
448
|
+
start: 4,
|
|
449
|
+
duration: 2,
|
|
450
|
+
track: 1,
|
|
451
|
+
compositionSrc: "scenes/cta.html",
|
|
452
|
+
},
|
|
446
453
|
],
|
|
447
454
|
[{ id: "hero", tag: "div", start: 0, duration: 4, track: 0 }],
|
|
448
455
|
8,
|
|
@@ -450,10 +457,31 @@ describe("mergeTimelineElementsPreservingDowngrades", () => {
|
|
|
450
457
|
),
|
|
451
458
|
).toEqual([
|
|
452
459
|
{ id: "hero", tag: "div", start: 0, duration: 4, track: 0 },
|
|
453
|
-
{
|
|
460
|
+
{
|
|
461
|
+
id: "cta",
|
|
462
|
+
tag: "div",
|
|
463
|
+
start: 4,
|
|
464
|
+
duration: 2,
|
|
465
|
+
track: 1,
|
|
466
|
+
compositionSrc: "scenes/cta.html",
|
|
467
|
+
},
|
|
454
468
|
]);
|
|
455
469
|
});
|
|
456
470
|
|
|
471
|
+
it("drops missing top-level elements so undo does not leave ghost clips", () => {
|
|
472
|
+
expect(
|
|
473
|
+
mergeTimelineElementsPreservingDowngrades(
|
|
474
|
+
[
|
|
475
|
+
{ id: "hero", tag: "div", start: 0, duration: 4, track: 0 },
|
|
476
|
+
{ id: "split-clone", tag: "div", start: 4, duration: 2, track: 1 },
|
|
477
|
+
],
|
|
478
|
+
[{ id: "hero", tag: "div", start: 0, duration: 4, track: 0 }],
|
|
479
|
+
8,
|
|
480
|
+
8,
|
|
481
|
+
),
|
|
482
|
+
).toEqual([{ id: "hero", tag: "div", start: 0, duration: 4, track: 0 }]);
|
|
483
|
+
});
|
|
484
|
+
|
|
457
485
|
it("accepts longer-duration or same-size updates as authoritative", () => {
|
|
458
486
|
expect(
|
|
459
487
|
mergeTimelineElementsPreservingDowngrades(
|
|
@@ -477,6 +505,7 @@ describe("mergeTimelineElementsPreservingDowngrades", () => {
|
|
|
477
505
|
start: 0,
|
|
478
506
|
duration: 3,
|
|
479
507
|
track: 0,
|
|
508
|
+
compositionSrc: "scenes/cards.html",
|
|
480
509
|
},
|
|
481
510
|
{
|
|
482
511
|
id: "Card",
|
|
@@ -486,6 +515,7 @@ describe("mergeTimelineElementsPreservingDowngrades", () => {
|
|
|
486
515
|
start: 3,
|
|
487
516
|
duration: 3,
|
|
488
517
|
track: 1,
|
|
518
|
+
compositionSrc: "scenes/cards.html",
|
|
489
519
|
},
|
|
490
520
|
],
|
|
491
521
|
[
|
|
@@ -497,6 +527,7 @@ describe("mergeTimelineElementsPreservingDowngrades", () => {
|
|
|
497
527
|
start: 0,
|
|
498
528
|
duration: 3,
|
|
499
529
|
track: 0,
|
|
530
|
+
compositionSrc: "scenes/cards.html",
|
|
500
531
|
},
|
|
501
532
|
],
|
|
502
533
|
8,
|
|
@@ -511,6 +542,7 @@ describe("mergeTimelineElementsPreservingDowngrades", () => {
|
|
|
511
542
|
start: 0,
|
|
512
543
|
duration: 3,
|
|
513
544
|
track: 0,
|
|
545
|
+
compositionSrc: "scenes/cards.html",
|
|
514
546
|
},
|
|
515
547
|
{
|
|
516
548
|
id: "Card",
|
|
@@ -520,6 +552,7 @@ describe("mergeTimelineElementsPreservingDowngrades", () => {
|
|
|
520
552
|
start: 3,
|
|
521
553
|
duration: 3,
|
|
522
554
|
track: 1,
|
|
555
|
+
compositionSrc: "scenes/cards.html",
|
|
523
556
|
},
|
|
524
557
|
]);
|
|
525
558
|
});
|
|
@@ -3,6 +3,7 @@ import { usePlayerStore, liveTime, type TimelineElement } from "../store/playerS
|
|
|
3
3
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
4
4
|
import { usePlaybackKeyboard } from "./usePlaybackKeyboard";
|
|
5
5
|
import { useTimelineSyncCallbacks } from "./useTimelineSyncCallbacks";
|
|
6
|
+
import { useTimelinePlayerLoop } from "./useTimelinePlayerLoop";
|
|
6
7
|
|
|
7
8
|
export type { ClipManifestClip } from "../lib/playbackTypes";
|
|
8
9
|
export { createStaticSeekPlaybackAdapter } from "../lib/playbackAdapter";
|
|
@@ -15,7 +16,6 @@ export {
|
|
|
15
16
|
parseTimelineFromDOM,
|
|
16
17
|
readTimelineDurationFromDocument,
|
|
17
18
|
resolveStandaloneRootCompositionSrc,
|
|
18
|
-
resolveIframe,
|
|
19
19
|
} from "../lib/timelineDOM";
|
|
20
20
|
export {
|
|
21
21
|
shouldIgnorePlaybackShortcutEvent,
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
mergeTimelineElementsPreservingDowngrades,
|
|
37
37
|
parseTimelineFromDOM,
|
|
38
38
|
} from "../lib/timelineDOM";
|
|
39
|
+
import { normalizeToZones } from "../components/timelineZones";
|
|
39
40
|
import {
|
|
40
41
|
setPreviewMediaMuted,
|
|
41
42
|
setPreviewPlaybackRate,
|
|
@@ -45,6 +46,7 @@ import { scrubMusicAtSeek, stopScrubPreviewAudio } from "../lib/playbackScrub";
|
|
|
45
46
|
import { applyCachedSourceDurations, probeMissingSourceDurations } from "../lib/mediaProbe";
|
|
46
47
|
import { shouldResumeForwardPlaybackAfterSeek, shouldStopAfterSeek } from "../lib/playbackSeek";
|
|
47
48
|
import { applyPreviewVariablesToUrl } from "../../hooks/previewVariablesStore";
|
|
49
|
+
import { acceptStudioRuntimeMessage } from "../lib/runtimeProtocol";
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
52
|
* Whether the derived elements differ from the current ones in any field that
|
|
@@ -90,12 +92,17 @@ export function useTimelinePlayer() {
|
|
|
90
92
|
// applyCachedSourceDurations re-applies the cached probe duration: re-derived
|
|
91
93
|
// elements (e.g. after a clip move) can arrive without sourceDuration, which
|
|
92
94
|
// otherwise makes trimmed waveforms lose their window.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
// Enforced CapCut zoning (overlay → main → audio): normalize track indices
|
|
96
|
+
// on every discovery. Idempotent — already-zoned input is returned as-is, so
|
|
97
|
+
// drops persist zoned indices and reloads re-zone to the same (no drift).
|
|
98
|
+
const mergedElements = normalizeToZones(
|
|
99
|
+
applyCachedSourceDurations(
|
|
100
|
+
mergeTimelineElementsPreservingDowngrades(
|
|
101
|
+
state.elements,
|
|
102
|
+
elements,
|
|
103
|
+
state.duration,
|
|
104
|
+
resolvedDuration,
|
|
105
|
+
),
|
|
99
106
|
),
|
|
100
107
|
);
|
|
101
108
|
|
|
@@ -207,49 +214,14 @@ export function useTimelinePlayer() {
|
|
|
207
214
|
}
|
|
208
215
|
}, []);
|
|
209
216
|
|
|
210
|
-
const stopReverseLoop =
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const adapter = getAdapter();
|
|
218
|
-
if (adapter) {
|
|
219
|
-
const rawTime = adapter.getTime();
|
|
220
|
-
const dur = adapter.getDuration();
|
|
221
|
-
const time = dur > 0 ? Math.min(rawTime, dur) : rawTime;
|
|
222
|
-
liveTime.notify(time); // direct DOM updates, no React re-render
|
|
223
|
-
const { inPoint, outPoint } = usePlayerStore.getState();
|
|
224
|
-
const rawLoopEnd = outPoint !== null ? Math.min(outPoint, dur) : dur;
|
|
225
|
-
const rawLoopStart = inPoint !== null ? inPoint : 0;
|
|
226
|
-
const loopEnd = rawLoopStart < rawLoopEnd ? rawLoopEnd : dur;
|
|
227
|
-
const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0;
|
|
228
|
-
if (time >= loopEnd) {
|
|
229
|
-
if (usePlayerStore.getState().loopEnabled && dur > 0) {
|
|
230
|
-
// keepPlaying skips the adapter's implicit pause; play() below is then a no-op.
|
|
231
|
-
adapter.seek(loopStart, { keepPlaying: true });
|
|
232
|
-
liveTime.notify(loopStart);
|
|
233
|
-
adapter.play();
|
|
234
|
-
setIsPlaying(true);
|
|
235
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (adapter.isPlaying()) adapter.pause();
|
|
239
|
-
setCurrentTime(time); // sync Zustand once at end
|
|
240
|
-
setIsPlaying(false);
|
|
241
|
-
cancelAnimationFrame(rafRef.current);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
246
|
-
};
|
|
247
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
248
|
-
}, [getAdapter, setCurrentTime, setIsPlaying]);
|
|
217
|
+
const { startRAFLoop, stopRAFLoop, stopReverseLoop } = useTimelinePlayerLoop({
|
|
218
|
+
rafRef,
|
|
219
|
+
reverseRafRef,
|
|
220
|
+
getAdapter,
|
|
221
|
+
setCurrentTime,
|
|
222
|
+
setIsPlaying,
|
|
223
|
+
});
|
|
249
224
|
|
|
250
|
-
const stopRAFLoop = useCallback(() => {
|
|
251
|
-
cancelAnimationFrame(rafRef.current);
|
|
252
|
-
}, []);
|
|
253
225
|
const applyPlaybackRate = useCallback((rate: number) => {
|
|
254
226
|
const iframe = iframeRef.current;
|
|
255
227
|
if (!iframe) return;
|
|
@@ -463,10 +435,24 @@ export function useTimelinePlayer() {
|
|
|
463
435
|
applyPreviewAudioState,
|
|
464
436
|
});
|
|
465
437
|
const saveSeekPosition = useCallback(() => {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
438
|
+
// Never DEGRADE the saved position. Overlapping reloads (e.g. an external
|
|
439
|
+
// file drop = upload reload + insert reload back-to-back) call this while
|
|
440
|
+
// the iframe from the FIRST reload is mid-teardown: getAdapter() can still
|
|
441
|
+
// return that dying document's adapter, whose getTime() reads 0 — and the
|
|
442
|
+
// store's currentTime can lag the visual playhead. Overwriting the
|
|
443
|
+
// still-unconsumed pendingSeek with either value is exactly how the
|
|
444
|
+
// playhead used to end up at 0 after a Finder drop (verified live via a
|
|
445
|
+
// currentTime write-trace). So: while a refresh is already in flight and a
|
|
446
|
+
// save exists, keep it; otherwise trust the live adapter, then the store.
|
|
447
|
+
const refreshInFlight = isRefreshingRef.current && pendingSeekRef.current != null;
|
|
448
|
+
if (!refreshInFlight) {
|
|
449
|
+
const adapter = getAdapter();
|
|
450
|
+
if (adapter) {
|
|
451
|
+
pendingSeekRef.current = adapter.getTime();
|
|
452
|
+
} else if (pendingSeekRef.current == null) {
|
|
453
|
+
pendingSeekRef.current = usePlayerStore.getState().currentTime ?? 0;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
470
456
|
isRefreshingRef.current = true;
|
|
471
457
|
stopRAFLoop();
|
|
472
458
|
stopReverseLoop();
|
|
@@ -476,6 +462,16 @@ export function useTimelinePlayer() {
|
|
|
476
462
|
const iframe = iframeRef.current;
|
|
477
463
|
if (!iframe) return;
|
|
478
464
|
saveSeekPosition();
|
|
465
|
+
// Hide the iframe across the full reload so the user never sees the reloading
|
|
466
|
+
// document's RAW DOM (every clip stacked and visible) in the window between the
|
|
467
|
+
// new document parsing and the runtime initializing + seeking. initializeAdapter
|
|
468
|
+
// reveals it again right after its restore seek renders the correct frame.
|
|
469
|
+
// Tradeoff: this shows the parent stage background (a brief "freeze"/blank, on
|
|
470
|
+
// the order of the reload time ~100-300ms) INSTEAD of the all-clips flash. A
|
|
471
|
+
// blank is far less jarring than a burst of every asset appearing at once.
|
|
472
|
+
// Only the FULL-reload edits (drops/inserts) hit this — timing edits now take
|
|
473
|
+
// the soft-reload path and never touch refreshPlayer.
|
|
474
|
+
iframe.style.visibility = "hidden";
|
|
479
475
|
const src = iframe.src;
|
|
480
476
|
const url = new URL(src, window.location.origin);
|
|
481
477
|
url.searchParams.set("_t", String(Date.now()));
|
|
@@ -497,6 +493,9 @@ export function useTimelinePlayer() {
|
|
|
497
493
|
if (e.source && ourIframe && e.source !== ourIframe.contentWindow) {
|
|
498
494
|
return;
|
|
499
495
|
}
|
|
496
|
+
if (data?.source === "hf-preview") {
|
|
497
|
+
if (!acceptStudioRuntimeMessage(data)) return;
|
|
498
|
+
}
|
|
500
499
|
if (data?.source === "hf-preview" && data?.type === "state") {
|
|
501
500
|
try {
|
|
502
501
|
if (usePlayerStore.getState().elements.length === 0) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The forward playback loop for the timeline player.
|
|
3
|
+
*
|
|
4
|
+
* Owns the three requestAnimationFrame lifecycle callbacks that drive (and stop)
|
|
5
|
+
* playback:
|
|
6
|
+
* - startRAFLoop — the forward tick: advance liveTime, honour in/out loop
|
|
7
|
+
* points + loopEnabled, and pause + sync the store at the end.
|
|
8
|
+
* - stopRAFLoop — cancel the forward tick.
|
|
9
|
+
* - stopReverseLoop — cancel the reverse-shuttle tick (owned by the parent hook).
|
|
10
|
+
*
|
|
11
|
+
* Called unconditionally at the top level of useTimelinePlayer so its useCallback
|
|
12
|
+
* hooks run in a stable order; every dependency is passed in as an argument.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useCallback } from "react";
|
|
16
|
+
import { liveTime, usePlayerStore } from "../store/playerStore";
|
|
17
|
+
import type { PlaybackAdapter } from "../lib/playbackTypes";
|
|
18
|
+
|
|
19
|
+
interface UseTimelinePlayerLoopParams {
|
|
20
|
+
rafRef: React.MutableRefObject<number>;
|
|
21
|
+
reverseRafRef: React.MutableRefObject<number>;
|
|
22
|
+
getAdapter: () => PlaybackAdapter | null;
|
|
23
|
+
setCurrentTime: (v: number) => void;
|
|
24
|
+
setIsPlaying: (v: boolean) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface UseTimelinePlayerLoopResult {
|
|
28
|
+
startRAFLoop: () => void;
|
|
29
|
+
stopRAFLoop: () => void;
|
|
30
|
+
stopReverseLoop: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useTimelinePlayerLoop({
|
|
34
|
+
rafRef,
|
|
35
|
+
reverseRafRef,
|
|
36
|
+
getAdapter,
|
|
37
|
+
setCurrentTime,
|
|
38
|
+
setIsPlaying,
|
|
39
|
+
}: UseTimelinePlayerLoopParams): UseTimelinePlayerLoopResult {
|
|
40
|
+
const stopReverseLoop = useCallback(() => {
|
|
41
|
+
cancelAnimationFrame(reverseRafRef.current);
|
|
42
|
+
}, [reverseRafRef]);
|
|
43
|
+
|
|
44
|
+
const startRAFLoop = useCallback(() => {
|
|
45
|
+
// fallow-ignore-next-line complexity
|
|
46
|
+
const tick = () => {
|
|
47
|
+
const adapter = getAdapter();
|
|
48
|
+
if (adapter) {
|
|
49
|
+
const rawTime = adapter.getTime();
|
|
50
|
+
const dur = adapter.getDuration();
|
|
51
|
+
const time = dur > 0 ? Math.min(rawTime, dur) : rawTime;
|
|
52
|
+
liveTime.notify(time); // direct DOM updates, no React re-render
|
|
53
|
+
const { inPoint, outPoint } = usePlayerStore.getState();
|
|
54
|
+
const rawLoopEnd = outPoint !== null ? Math.min(outPoint, dur) : dur;
|
|
55
|
+
const rawLoopStart = inPoint !== null ? inPoint : 0;
|
|
56
|
+
const loopEnd = rawLoopStart < rawLoopEnd ? rawLoopEnd : dur;
|
|
57
|
+
const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0;
|
|
58
|
+
if (time >= loopEnd) {
|
|
59
|
+
if (usePlayerStore.getState().loopEnabled && dur > 0) {
|
|
60
|
+
// keepPlaying skips the adapter's implicit pause; play() below is then a no-op.
|
|
61
|
+
adapter.seek(loopStart, { keepPlaying: true });
|
|
62
|
+
liveTime.notify(loopStart);
|
|
63
|
+
adapter.play();
|
|
64
|
+
setIsPlaying(true);
|
|
65
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (adapter.isPlaying()) adapter.pause();
|
|
69
|
+
setCurrentTime(time); // sync Zustand once at end
|
|
70
|
+
setIsPlaying(false);
|
|
71
|
+
cancelAnimationFrame(rafRef.current);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
76
|
+
};
|
|
77
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
78
|
+
}, [rafRef, getAdapter, setCurrentTime, setIsPlaying]);
|
|
79
|
+
|
|
80
|
+
const stopRAFLoop = useCallback(() => {
|
|
81
|
+
cancelAnimationFrame(rafRef.current);
|
|
82
|
+
}, [rafRef]);
|
|
83
|
+
|
|
84
|
+
return { startRAFLoop, stopRAFLoop, stopReverseLoop };
|
|
85
|
+
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
autoHealMissingCompositionIds,
|
|
27
27
|
buildMissingCompositionElements,
|
|
28
28
|
} from "../lib/timelineIframeHelpers";
|
|
29
|
+
import { acceptedRuntimeMessageFps, inspectStudioRuntimeMessage } from "../lib/runtimeProtocol";
|
|
29
30
|
|
|
30
31
|
interface UseTimelineSyncCallbacksParams {
|
|
31
32
|
iframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
@@ -132,6 +133,9 @@ export function useTimelineSyncCallbacks({
|
|
|
132
133
|
clips: ClipManifestClip[];
|
|
133
134
|
durationInFrames: number;
|
|
134
135
|
scenes?: Array<{ id: string; label: string; start: number; duration: number }>;
|
|
136
|
+
protocolVersion?: unknown;
|
|
137
|
+
capabilities?: unknown;
|
|
138
|
+
fps?: unknown;
|
|
135
139
|
}) => {
|
|
136
140
|
if (!data.clips || data.clips.length === 0) {
|
|
137
141
|
return;
|
|
@@ -222,7 +226,7 @@ export function useTimelineSyncCallbacks({
|
|
|
222
226
|
hostEl,
|
|
223
227
|
});
|
|
224
228
|
});
|
|
225
|
-
const rawDuration = data.durationInFrames /
|
|
229
|
+
const rawDuration = data.durationInFrames / acceptedRuntimeMessageFps(data);
|
|
226
230
|
// Clamp non-finite or absurdly large durations — the runtime can emit
|
|
227
231
|
// Infinity when it detects a loop-inflated GSAP timeline without an
|
|
228
232
|
// explicit data-duration on the root composition. Floor the manifest total
|
|
@@ -418,6 +422,10 @@ export function useTimelineSyncCallbacks({
|
|
|
418
422
|
if (e.source && iframe && e.source !== iframe.contentWindow) return;
|
|
419
423
|
const data = e.data;
|
|
420
424
|
if (data?.source === "hf-preview" && (data?.type === "state" || data?.type === "timeline")) {
|
|
425
|
+
// The main message handler owns protocol-error diagnostics. This readiness-only
|
|
426
|
+
// listener mirrors its acceptance gate without dispatching a duplicate event:
|
|
427
|
+
// an unsupported runtime must not make the iframe appear successfully settled.
|
|
428
|
+
if (inspectStudioRuntimeMessage(data).status === "unsupported") return;
|
|
421
429
|
trySettle();
|
|
422
430
|
}
|
|
423
431
|
};
|
package/src/player/index.ts
CHANGED
|
@@ -6,11 +6,14 @@ export { VideoThumbnail } from "./components/VideoThumbnail";
|
|
|
6
6
|
export { CompositionThumbnail } from "./components/CompositionThumbnail";
|
|
7
7
|
|
|
8
8
|
// Hooks
|
|
9
|
-
export { useTimelinePlayer
|
|
9
|
+
export { useTimelinePlayer } from "./hooks/useTimelinePlayer";
|
|
10
|
+
export { resolveIframe } from "./lib/timelineDOM";
|
|
10
11
|
|
|
11
12
|
// Store
|
|
12
13
|
export { usePlayerStore, liveTime } from "./store/playerStore";
|
|
13
|
-
|
|
14
|
+
// Public library surface; external consumers are invisible to the workspace analyzer.
|
|
15
|
+
// fallow-ignore-next-line unused-exports
|
|
16
|
+
export type { SelectElementOptions, TimelineElement, ZoomMode } from "./store/playerStore";
|
|
14
17
|
|
|
15
18
|
// Utils
|
|
16
19
|
export { formatTime } from "./lib/time";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
acceptStudioRuntimeMessage,
|
|
4
|
+
acceptedRuntimeMessageFps,
|
|
5
|
+
createRuntimeControlMessage,
|
|
6
|
+
inspectStudioRuntimeMessage,
|
|
7
|
+
postRuntimeControlMessage,
|
|
8
|
+
} from "./runtimeProtocol";
|
|
9
|
+
|
|
10
|
+
describe("Studio runtime protocol", () => {
|
|
11
|
+
it("versions every control message and declares rational fps", () => {
|
|
12
|
+
expect(createRuntimeControlMessage("seek", { timeSeconds: 1.25 }, 60)).toEqual({
|
|
13
|
+
source: "hf-parent",
|
|
14
|
+
type: "control",
|
|
15
|
+
action: "seek",
|
|
16
|
+
protocolVersion: 1,
|
|
17
|
+
capabilities: ["seconds-time", "rational-fps", "seek-keep-playing"],
|
|
18
|
+
fps: { numerator: 60, denominator: 1 },
|
|
19
|
+
timeSeconds: 1.25,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("posts the typed message to the target window", () => {
|
|
24
|
+
const target = { postMessage: vi.fn() };
|
|
25
|
+
postRuntimeControlMessage(target as unknown as Window, "pause");
|
|
26
|
+
expect(target.postMessage).toHaveBeenCalledWith(
|
|
27
|
+
expect.objectContaining({ action: "pause", protocolVersion: 1 }),
|
|
28
|
+
"*",
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("preserves legacy 30fps messages and rejects unknown majors", () => {
|
|
33
|
+
expect(inspectStudioRuntimeMessage({ source: "hf-preview" })).toEqual({
|
|
34
|
+
status: "legacy",
|
|
35
|
+
fps: 30,
|
|
36
|
+
});
|
|
37
|
+
expect(inspectStudioRuntimeMessage({ protocolVersion: 2 })).toMatchObject({
|
|
38
|
+
status: "unsupported",
|
|
39
|
+
code: "unsupported_protocol_version",
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("reads explicit fps for accepted timeline messages", () => {
|
|
44
|
+
const message = createRuntimeControlMessage("pause", {}, 60);
|
|
45
|
+
expect(acceptedRuntimeMessageFps(message)).toBe(60);
|
|
46
|
+
expect(acceptStudioRuntimeMessage(message)).toMatchObject({ status: "supported", fps: 60 });
|
|
47
|
+
});
|
|
48
|
+
});
|