@hyperframes/studio 0.8.5 → 0.8.7
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-DcM3fVcp.js → hyperframes-player-bDcU464i.js} +1 -1
- package/dist/assets/index-Ba9zbpT9.css +1 -0
- package/dist/assets/index-CUXnyoIa.js +428 -0
- package/dist/assets/{index-DgJrvYGf.js → index-FkAs3zRu.js} +1 -1
- package/dist/assets/{index-Del6XWZj.js → index-MSOSP1-J.js} +1 -1
- package/dist/index.d.ts +98 -17
- package/dist/index.html +2 -2
- package/dist/index.js +10337 -7200
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +22 -23
- package/src/captions/components/CaptionAnimationPanel.tsx +46 -13
- package/src/captions/components/CaptionOverlay.tsx +172 -29
- package/src/captions/components/CaptionOverlayUtils.ts +54 -0
- package/src/captions/components/CaptionPropertyPanel.tsx +49 -23
- package/src/captions/components/CaptionTimeline.tsx +36 -79
- package/src/captions/components/shared.tsx +76 -1
- package/src/captions/hooks/useCaptionSync.ts +72 -13
- package/src/captions/keyboard.test.ts +12 -0
- package/src/captions/keyboard.ts +12 -1
- package/src/captions/store.ts +120 -14
- package/src/components/EditorShell.selectionSync.test.tsx +1 -1
- package/src/components/EditorShell.tsx +13 -1
- package/src/components/StudioRightPanel.tsx +2 -0
- package/src/components/StudioRightPanel.types.ts +1 -0
- package/src/components/editor/AnimationCard.tsx +2 -1
- package/src/components/editor/AnimationCardParts.tsx +6 -3
- package/src/components/editor/ArcPathControls.tsx +10 -4
- package/src/components/editor/BlockParamsPanel.test.tsx +170 -0
- package/src/components/editor/BlockParamsPanel.tsx +153 -11
- package/src/components/editor/BorderRadiusEditor.tsx +3 -22
- package/src/components/editor/FileTree.tsx +41 -12
- package/src/components/editor/FileTreeNodes.tsx +90 -20
- package/src/components/editor/GsapAnimationList.tsx +5 -0
- package/src/components/editor/KeyframeDiamond.tsx +3 -1
- package/src/components/editor/KeyframeEaseList.tsx +3 -2
- package/src/components/editor/KeyframeNavigation.tsx +6 -2
- package/src/components/editor/LayersPanel.tsx +11 -2
- package/src/components/editor/MotionPathNode.tsx +1 -0
- package/src/components/editor/PropertyPanel.test.tsx +7 -6
- package/src/components/editor/PropertyPanel.tsx +13 -5
- package/src/components/editor/PropertyPanelFlat.tsx +2 -0
- package/src/components/editor/SnapToolbar.tsx +20 -4
- package/src/components/editor/TimelineFxPopover.test.tsx +113 -0
- package/src/components/editor/TimelineFxPopover.tsx +138 -0
- package/src/components/editor/Transform3DCube.tsx +22 -1
- package/src/components/editor/manualOffsetDrag.test.ts +49 -0
- package/src/components/editor/manualOffsetDrag.ts +13 -1
- package/src/components/editor/propertyPanel3dTransform.tsx +11 -3
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +139 -3
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +4 -0
- package/src/components/editor/propertyPanelColor.tsx +9 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +28 -7
- package/src/components/editor/propertyPanelCommitField.tsx +9 -1
- package/src/components/editor/propertyPanelFill.tsx +50 -2
- package/src/components/editor/propertyPanelFlatProps.ts +1 -0
- package/src/components/editor/propertyPanelFont.tsx +45 -9
- package/src/components/editor/propertyPanelFxPresetStyle.ts +18 -0
- package/src/components/editor/propertyPanelFxSection.test.tsx +3 -3
- package/src/components/editor/propertyPanelFxSection.tsx +8 -23
- package/src/components/editor/propertyPanelPrimitives.tsx +9 -12
- package/src/components/editor/propertyPanelStyleSections.tsx +21 -2
- package/src/components/editor/propertyPanelTransformCommit.ts +3 -3
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/components/editor/useApplyAudioFxPreset.ts +36 -0
- package/src/components/editor/useFxCarve.ts +0 -0
- package/src/components/editor/useFxCarveGrouping.ts +132 -0
- package/src/components/nle/PreviewOverlays.tsx +69 -2
- package/src/components/nle/PreviewPane.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +27 -0
- package/src/components/panels/SlideshowPanel.tsx +81 -12
- package/src/components/panels/SlideshowSubPanels.tsx +61 -7
- package/src/components/sidebar/AssetCard.test.tsx +2 -2
- package/src/components/sidebar/AssetCard.tsx +94 -46
- package/src/components/sidebar/AssetContextMenu.tsx +196 -42
- package/src/components/sidebar/AssetsTab.tsx +156 -96
- package/src/components/sidebar/AudioRow.tsx +95 -40
- package/src/components/sidebar/BlocksTab.tsx +40 -128
- package/src/components/sidebar/CompositionsTab.tsx +46 -34
- package/src/components/sidebar/LeftSidebar.tsx +87 -75
- package/src/components/sidebar/PromptPreviewModal.tsx +126 -0
- package/src/components/sidebar/assetHelpers.ts +8 -0
- package/src/contexts/TimelineEditContext.tsx +5 -0
- package/src/hooks/timelineAudioGroupVolume.ts +148 -0
- package/src/hooks/timelineEditingHelpers.ts +78 -1
- package/src/hooks/timelineElementFxAttribute.ts +150 -0
- package/src/hooks/timelineTrackVisibility.test.ts +222 -1
- package/src/hooks/timelineTrackVisibility.ts +187 -4
- package/src/hooks/useAppHotkeys.ts +22 -0
- package/src/hooks/useAudioSoloBridge.ts +61 -0
- package/src/hooks/useCaptionDetection.ts +22 -2
- package/src/hooks/useDomEditCommits.test.tsx +24 -0
- package/src/hooks/useEffectiveTimelineDuration.ts +20 -0
- package/src/hooks/useGroupLevel.ts +36 -0
- package/src/hooks/useHydrateActiveCompPathFromUrl.ts +36 -0
- package/src/hooks/useMusicBeatAnalysis.ts +13 -0
- package/src/hooks/useTimelineDeleteOps.ts +170 -0
- package/src/hooks/useTimelineEditing.ts +53 -129
- package/src/player/components/AudioWaveform.tsx +29 -26
- package/src/player/components/BeatStrip.test.tsx +5 -9
- package/src/player/components/BeatStrip.tsx +12 -5
- package/src/player/components/ClipContextMenu.tsx +8 -2
- package/src/player/components/EditModal.tsx +48 -7
- package/src/player/components/KeyframeDiamondContextMenu.tsx +90 -10
- package/src/player/components/LayerDisclosureRow.tsx +12 -13
- package/src/player/components/Player.tsx +43 -8
- package/src/player/components/PlayerControls.tsx +107 -63
- package/src/player/components/ShortcutsPanel.tsx +23 -4
- package/src/player/components/SpeedMenu.tsx +34 -42
- package/src/player/components/Timeline.test.ts +7 -7
- package/src/player/components/Timeline.tsx +7 -10
- package/src/player/components/TimelineFxButton.test.tsx +81 -0
- package/src/player/components/TimelineFxButton.tsx +143 -0
- package/src/player/components/TimelineGroupBusStrip.test.tsx +146 -0
- package/src/player/components/TimelineGroupBusStrip.tsx +103 -0
- package/src/player/components/TimelineGroupHeader.tsx +176 -0
- package/src/player/components/TimelineGroupRow.tsx +146 -0
- package/src/player/components/TimelineLanes.test.tsx +5 -0
- package/src/player/components/TimelineLanes.tsx +39 -33
- package/src/player/components/TimelineOverlays.tsx +34 -0
- package/src/player/components/TimelineSoloButton.tsx +32 -0
- package/src/player/components/TimelineTrackHeader.tsx +74 -98
- package/src/player/components/TimelineTrackPlainHeader.tsx +119 -0
- package/src/player/components/VideoThumbnail.tsx +6 -1
- package/src/player/components/menuKeyboardNav.ts +47 -0
- package/src/player/components/timelineCallbacks.ts +26 -0
- package/src/player/components/timelineKeyboardNavigation.test.ts +8 -0
- package/src/player/components/timelineKeyboardNavigation.ts +147 -30
- package/src/player/components/timelineLaneProps.ts +3 -0
- package/src/player/components/timelineLayout.ts +1 -1
- package/src/player/components/timelineNavigationIdentity.ts +4 -0
- package/src/player/components/useTimelineLaneRowIndexes.ts +38 -0
- package/src/player/components/useTimelineLogicalFocus.ts +9 -0
- package/src/player/components/useTimelineLogicalRows.test.tsx +8 -0
- package/src/player/components/useTimelineLogicalRows.ts +17 -12
- package/src/player/components/useTimelineMultiDragActorWindows.ts +30 -0
- package/src/player/components/useTimelineTrackDerivations.ts +150 -6
- package/src/player/components/useTimelineTrackLayout.ts +61 -4
- package/src/player/hooks/previewMessageRouter.ts +127 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +64 -17
- package/src/player/hooks/useExpandedTimelineElements.ts +8 -11
- package/src/player/hooks/useTimelinePlayer.ts +9 -47
- package/src/player/lib/timelineDOM.ts +51 -0
- package/src/player/store/audioSoloSlice.test.ts +113 -0
- package/src/player/store/audioSoloSlice.ts +43 -0
- package/src/player/store/editingModeSlice.ts +45 -0
- package/src/player/store/groupLevels.ts +33 -0
- package/src/player/store/keyframeSlice.ts +26 -0
- package/src/player/store/playerStore.ts +21 -22
- package/src/player/store/timelineElement.ts +10 -0
- package/src/telemetry/events.ts +12 -0
- package/dist/assets/index-Cx_UrhoW.js +0 -428
- package/dist/assets/index-DcF4s1PG.css +0 -1
|
@@ -7,12 +7,14 @@ import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
|
7
7
|
import type { DraggedClipState } from "./timelineClipDragTypes";
|
|
8
8
|
import { useTimelineTrackDerivations } from "./useTimelineTrackDerivations";
|
|
9
9
|
import {
|
|
10
|
+
STRIP_H,
|
|
10
11
|
TRACK_H,
|
|
11
12
|
createTimelineRowGeometry,
|
|
12
13
|
type TimelineRowGeometry,
|
|
13
14
|
trackHeights,
|
|
14
15
|
type TimelineTrackHeightClip,
|
|
15
16
|
} from "./timelineLayout";
|
|
17
|
+
import type { TimelineTrackGroupInfo } from "./useTimelineTrackDerivations";
|
|
16
18
|
|
|
17
19
|
export { getTrackStyle } from "./timelineIcons";
|
|
18
20
|
|
|
@@ -127,13 +129,35 @@ function computeLaneCounts(
|
|
|
127
129
|
return laneCounts;
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
/** Group anchor rows have no elements of their own (`groupTimelineTracks`
|
|
133
|
+
* pushes them as `[anchorKey, []]`), so `trackHeights` — which only ever
|
|
134
|
+
* looks at a row's clips — always gives them TRACK_H. Override those
|
|
135
|
+
* specific rows post-hoc: TRACK_H while collapsed, +STRIP_H once the
|
|
136
|
+
* group's own `∿` (bus strip) is open. */
|
|
137
|
+
function applyGroupStripHeights(
|
|
138
|
+
tracks: readonly (readonly [number, readonly TimelineElement[]])[],
|
|
139
|
+
rowHeights: number[],
|
|
140
|
+
groups: readonly TimelineTrackGroupInfo[],
|
|
141
|
+
expandedLaneOwnerIds: ReadonlySet<string>,
|
|
142
|
+
): number[] {
|
|
143
|
+
if (groups.length === 0) return rowHeights;
|
|
144
|
+
const groupByAnchor = new Map(groups.map((group) => [group.anchorKey, group]));
|
|
145
|
+
return tracks.map(([track], index) => {
|
|
146
|
+
const group = groupByAnchor.get(track);
|
|
147
|
+
if (!group || !expandedLaneOwnerIds.has(group.id)) return rowHeights[index] ?? TRACK_H;
|
|
148
|
+
return TRACK_H + STRIP_H;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
130
152
|
function useTimelineRowHeights(
|
|
131
153
|
tracks: [number, TimelineElement[]][],
|
|
132
154
|
gsapAnimations: Map<string, GsapAnimation[]>,
|
|
133
155
|
selectedElementId: string | null,
|
|
134
156
|
selectedElementIds: ReadonlySet<string>,
|
|
157
|
+
groups: readonly TimelineTrackGroupInfo[],
|
|
135
158
|
) {
|
|
136
159
|
const expandedClipIds = usePlayerStore((s) => s.expandedClipIds);
|
|
160
|
+
const expandedLaneOwnerIds = usePlayerStore((s) => s.expandedLaneOwnerIds);
|
|
137
161
|
const { laneCounts, rowGeometry } = useMemo(() => {
|
|
138
162
|
const laneCounts = computeLaneCounts(tracks, gsapAnimations);
|
|
139
163
|
// Keyframe lanes follow only the active clip, so a track with several
|
|
@@ -163,7 +187,12 @@ function useTimelineRowHeights(
|
|
|
163
187
|
},
|
|
164
188
|
];
|
|
165
189
|
});
|
|
166
|
-
const rowHeights =
|
|
190
|
+
const rowHeights = applyGroupStripHeights(
|
|
191
|
+
tracks,
|
|
192
|
+
trackHeights(heightTracks, expandedClipIds),
|
|
193
|
+
groups,
|
|
194
|
+
expandedLaneOwnerIds,
|
|
195
|
+
);
|
|
167
196
|
return {
|
|
168
197
|
laneCounts,
|
|
169
198
|
rowGeometry: createTimelineRowGeometry(
|
|
@@ -171,7 +200,15 @@ function useTimelineRowHeights(
|
|
|
171
200
|
rowHeights,
|
|
172
201
|
),
|
|
173
202
|
};
|
|
174
|
-
}, [
|
|
203
|
+
}, [
|
|
204
|
+
expandedClipIds,
|
|
205
|
+
expandedLaneOwnerIds,
|
|
206
|
+
gsapAnimations,
|
|
207
|
+
groups,
|
|
208
|
+
tracks,
|
|
209
|
+
selectedElementId,
|
|
210
|
+
selectedElementIds,
|
|
211
|
+
]);
|
|
175
212
|
const rowGeometryRef = useRef<TimelineRowGeometry>(rowGeometry);
|
|
176
213
|
rowGeometryRef.current = rowGeometry;
|
|
177
214
|
return {
|
|
@@ -188,7 +225,8 @@ export function useTimelineTrackLayout(
|
|
|
188
225
|
selectedElementId: string | null,
|
|
189
226
|
selectedElementIds: ReadonlySet<string>,
|
|
190
227
|
) {
|
|
191
|
-
const { tracks, trackStyles, trackOrder } =
|
|
228
|
+
const { tracks, trackStyles, trackOrder, groups, trackGroupOf } =
|
|
229
|
+
useTimelineTrackDerivations(expandedElements);
|
|
192
230
|
const trackOrderRef = useRef(trackOrder);
|
|
193
231
|
trackOrderRef.current = trackOrder;
|
|
194
232
|
const { laneCounts, rowGeometry, rowGeometryRef, rowHeights } = useTimelineRowHeights(
|
|
@@ -196,6 +234,7 @@ export function useTimelineTrackLayout(
|
|
|
196
234
|
gsapAnimations,
|
|
197
235
|
selectedElementId,
|
|
198
236
|
selectedElementIds,
|
|
237
|
+
groups,
|
|
199
238
|
);
|
|
200
239
|
|
|
201
240
|
return {
|
|
@@ -207,6 +246,8 @@ export function useTimelineTrackLayout(
|
|
|
207
246
|
rowGeometry,
|
|
208
247
|
rowGeometryRef,
|
|
209
248
|
rowHeights,
|
|
249
|
+
groups,
|
|
250
|
+
trackGroupOf,
|
|
210
251
|
};
|
|
211
252
|
}
|
|
212
253
|
|
|
@@ -227,7 +268,23 @@ function useDisplayRowHeights(
|
|
|
227
268
|
function useDisplayTrackOrder(draggedClip: DraggedClipState | null, trackOrder: number[]) {
|
|
228
269
|
return useMemo(() => {
|
|
229
270
|
if (!draggedClip?.started || trackOrder.includes(draggedClip.previewTrack)) return trackOrder;
|
|
230
|
-
|
|
271
|
+
// A group's members sit out of raw numeric order (pulled under their
|
|
272
|
+
// anchor row), so a plain numeric sort here would undo that grouping the
|
|
273
|
+
// moment a clip drags onto a brand-new track. Insert the new preview
|
|
274
|
+
// track only relative to other REAL (integer) tracks, leaving any
|
|
275
|
+
// fractional group-anchor keys exactly where grouping placed them.
|
|
276
|
+
const preview = draggedClip.previewTrack;
|
|
277
|
+
const result: number[] = [];
|
|
278
|
+
let inserted = false;
|
|
279
|
+
for (const key of trackOrder) {
|
|
280
|
+
if (!inserted && Number.isInteger(key) && key > preview) {
|
|
281
|
+
result.push(preview);
|
|
282
|
+
inserted = true;
|
|
283
|
+
}
|
|
284
|
+
result.push(key);
|
|
285
|
+
}
|
|
286
|
+
if (!inserted) result.push(preview);
|
|
287
|
+
return result;
|
|
231
288
|
}, [draggedClip, trackOrder]);
|
|
232
289
|
}
|
|
233
290
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `window.message` router for the preview iframe.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `useTimelinePlayer`, which had grown past the studio's 600-line
|
|
5
|
+
* file cap and carried a `fallow-ignore-next-line complexity` on this function
|
|
6
|
+
* admitting the same thing. Nothing here is new logic — it is the same four
|
|
7
|
+
* branches (accept-gate, group levels, state, timeline) against the same refs,
|
|
8
|
+
* with the suppression retired rather than moved.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
12
|
+
import type { ClipManifestClip, IframeWindow, PlaybackAdapter } from "../lib/playbackTypes";
|
|
13
|
+
import { hasTimelinePerformanceFixtureLease } from "../lib/timelinePerformanceFixture";
|
|
14
|
+
import { acceptStudioRuntimeMessage } from "../lib/runtimeProtocol";
|
|
15
|
+
import { groupLevels, parseGroupLevelsMessage } from "../store/groupLevels";
|
|
16
|
+
import { parseTimelineFromDOM } from "../lib/timelineDOM";
|
|
17
|
+
|
|
18
|
+
/** What `processTimelineMessage` accepts — the clip-manifest postMessage. */
|
|
19
|
+
export interface ClipManifestMessage {
|
|
20
|
+
clips: ClipManifestClip[];
|
|
21
|
+
durationInFrames: number;
|
|
22
|
+
scenes?: Array<{ id: string; label: string; start: number; duration: number }>;
|
|
23
|
+
protocolVersion?: unknown;
|
|
24
|
+
capabilities?: unknown;
|
|
25
|
+
fps?: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PreviewMessage {
|
|
29
|
+
source?: string;
|
|
30
|
+
type?: string;
|
|
31
|
+
clips?: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PreviewMessageRouterDeps {
|
|
35
|
+
iframeRef: { current: HTMLIFrameElement | null };
|
|
36
|
+
/** Kept as refs by the caller so the listener can be registered once. */
|
|
37
|
+
processTimelineMessageRef: { current: (data: ClipManifestMessage) => void };
|
|
38
|
+
enrichMissingCompositionsRef: { current: () => void };
|
|
39
|
+
lastTimelineMessageRef: { current: number };
|
|
40
|
+
getAdapter: () => PlaybackAdapter | null;
|
|
41
|
+
syncTimelineElements: (elements: TimelineElement[]) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** True when the message did not come from OUR preview iframe. */
|
|
45
|
+
function isForeignSource(e: MessageEvent, iframe: HTMLIFrameElement | null): boolean {
|
|
46
|
+
return Boolean(e.source && iframe && e.source !== iframe.contentWindow);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A preview message worth acting on, or null: the fixture lease, the sender
|
|
50
|
+
* check and the protocol accept-gate collapsed into one answer so the listener
|
|
51
|
+
* below stays a flat dispatch. */
|
|
52
|
+
function acceptedPreviewMessage(
|
|
53
|
+
e: MessageEvent,
|
|
54
|
+
iframe: HTMLIFrameElement | null,
|
|
55
|
+
): PreviewMessage | null {
|
|
56
|
+
if (hasTimelinePerformanceFixtureLease()) return null;
|
|
57
|
+
if (isForeignSource(e, iframe)) return null;
|
|
58
|
+
const data = e.data as PreviewMessage | null;
|
|
59
|
+
if (data?.source !== "hf-preview") return null;
|
|
60
|
+
return acceptStudioRuntimeMessage(data) ? data : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One meter reading per group with an active member. */
|
|
64
|
+
function handleGroupLevelsMessage(data: PreviewMessage): void {
|
|
65
|
+
const levels = parseGroupLevelsMessage(data);
|
|
66
|
+
if (levels) groupLevels.notify(levels);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A `state` tick doubles as a recovery hook: if the store still has no
|
|
71
|
+
* elements, read the manifest straight off the iframe, and if no `timeline`
|
|
72
|
+
* message has arrived recently, re-run enrichment.
|
|
73
|
+
*/
|
|
74
|
+
function handleStateMessage(
|
|
75
|
+
deps: PreviewMessageRouterDeps,
|
|
76
|
+
iframe: HTMLIFrameElement | null,
|
|
77
|
+
): void {
|
|
78
|
+
try {
|
|
79
|
+
if (usePlayerStore.getState().elements.length === 0) {
|
|
80
|
+
const manifest = (iframe?.contentWindow as IframeWindow | null)?.__clipManifest;
|
|
81
|
+
if (manifest && manifest.clips.length > 0) {
|
|
82
|
+
deps.processTimelineMessageRef.current(manifest);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (Date.now() - deps.lastTimelineMessageRef.current > 500) {
|
|
86
|
+
deps.enrichMissingCompositionsRef.current();
|
|
87
|
+
}
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The clip list. When it arrives empty-handed — the store still has nothing —
|
|
93
|
+
* fall back to parsing the live DOM, which is the only source left before the
|
|
94
|
+
* next tick.
|
|
95
|
+
*/
|
|
96
|
+
function handleTimelineMessage(
|
|
97
|
+
deps: PreviewMessageRouterDeps,
|
|
98
|
+
iframe: HTMLIFrameElement | null,
|
|
99
|
+
data: ClipManifestMessage,
|
|
100
|
+
): void {
|
|
101
|
+
deps.lastTimelineMessageRef.current = Date.now();
|
|
102
|
+
deps.processTimelineMessageRef.current(data);
|
|
103
|
+
deps.enrichMissingCompositionsRef.current();
|
|
104
|
+
if (usePlayerStore.getState().elements.length > 0) return;
|
|
105
|
+
try {
|
|
106
|
+
const doc = iframe?.contentDocument;
|
|
107
|
+
const adapter = deps.getAdapter();
|
|
108
|
+
if (!doc || !adapter) return;
|
|
109
|
+
const els = parseTimelineFromDOM(doc, adapter.getDuration());
|
|
110
|
+
if (els.length > 0) deps.syncTimelineElements(els);
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function createPreviewMessageHandler(
|
|
115
|
+
deps: PreviewMessageRouterDeps,
|
|
116
|
+
): (e: MessageEvent) => void {
|
|
117
|
+
return (e: MessageEvent) => {
|
|
118
|
+
const iframe = deps.iframeRef.current;
|
|
119
|
+
const data = acceptedPreviewMessage(e, iframe);
|
|
120
|
+
if (!data) return;
|
|
121
|
+
if (data.type === "group-levels") return handleGroupLevelsMessage(data);
|
|
122
|
+
if (data.type === "state") return handleStateMessage(deps, iframe);
|
|
123
|
+
if (data.type === "timeline" && Array.isArray(data.clips)) {
|
|
124
|
+
handleTimelineMessage(deps, iframe, data as unknown as ClipManifestMessage);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act, useEffect } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
6
|
import {
|
|
3
7
|
buildExpandedElements,
|
|
4
8
|
resolveTimelineExpansionRawId,
|
|
9
|
+
useExpandedTimelineElements,
|
|
5
10
|
} from "./useExpandedTimelineElements";
|
|
6
11
|
import { buildTimelineElementKey } from "../lib/timelineElementHelpers";
|
|
7
|
-
import type
|
|
12
|
+
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
8
13
|
import type { ClipManifestClip } from "../lib/playbackTypes";
|
|
9
14
|
|
|
15
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
16
|
+
|
|
10
17
|
const clip = (over: Partial<ClipManifestClip>): ClipManifestClip => ({
|
|
11
18
|
id: "x",
|
|
12
19
|
label: "x",
|
|
@@ -31,6 +38,19 @@ const el = (over: Partial<TimelineElement>): TimelineElement => ({
|
|
|
31
38
|
...over,
|
|
32
39
|
});
|
|
33
40
|
|
|
41
|
+
function TimelineExpansionHarness({ onValue }: { onValue: (value: TimelineElement[]) => void }) {
|
|
42
|
+
const value = useExpandedTimelineElements();
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
onValue(value);
|
|
45
|
+
}, [onValue, value]);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
document.body.innerHTML = "";
|
|
51
|
+
usePlayerStore.getState().reset();
|
|
52
|
+
});
|
|
53
|
+
|
|
34
54
|
describe("buildExpandedElements", () => {
|
|
35
55
|
it("rebases a 1-level child onto its sub-comp host (start + sourceFile)", () => {
|
|
36
56
|
// host s3 at absolute 16 → stats-panel.html; children live in that file.
|
|
@@ -357,7 +377,7 @@ describe("buildExpandedElements", () => {
|
|
|
357
377
|
it("keeps the host row present at every playhead position (keyframe lane repro)", () => {
|
|
358
378
|
// Live repro with no drag at all: seek 0 gave 3 diamonds, seek 7.68 gave 0,
|
|
359
379
|
// seek 0.2 gave 3. Diamonds render per row from keyframeCache.get(elementKey),
|
|
360
|
-
// so the whole lane went with the host row whenever the
|
|
380
|
+
// so the whole lane went with the host row whenever the store-time drill-in
|
|
361
381
|
// substituted it for its children.
|
|
362
382
|
const elements = [
|
|
363
383
|
el({ id: "scene", domId: "scene", key: "index.html#scene", start: 0, duration: 12 }),
|
|
@@ -371,7 +391,6 @@ describe("buildExpandedElements", () => {
|
|
|
371
391
|
for (const currentTime of [0, 7.68, 0.2]) {
|
|
372
392
|
const rawId = resolveTimelineExpansionRawId({
|
|
373
393
|
selectedElementId: null,
|
|
374
|
-
isPlaying: false,
|
|
375
394
|
currentTime,
|
|
376
395
|
manifest,
|
|
377
396
|
parentMap,
|
|
@@ -424,13 +443,12 @@ describe("buildExpandedElements", () => {
|
|
|
424
443
|
});
|
|
425
444
|
|
|
426
445
|
describe("resolveTimelineExpansionRawId", () => {
|
|
427
|
-
it("returns null
|
|
446
|
+
it("returns null inside a childless top-level clip", () => {
|
|
428
447
|
const manifest = [clip({ id: "title", start: 0, duration: 4 })];
|
|
429
448
|
|
|
430
449
|
expect(
|
|
431
450
|
resolveTimelineExpansionRawId({
|
|
432
451
|
selectedElementId: null,
|
|
433
|
-
isPlaying: false,
|
|
434
452
|
currentTime: 2,
|
|
435
453
|
manifest,
|
|
436
454
|
parentMap: new Map(),
|
|
@@ -438,7 +456,7 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
438
456
|
).toBeNull();
|
|
439
457
|
});
|
|
440
458
|
|
|
441
|
-
it("auto-expands an active composition with children when
|
|
459
|
+
it("auto-expands an active composition with children when nothing is selected", () => {
|
|
442
460
|
const manifest = [
|
|
443
461
|
clip({ id: "scene", start: 1, duration: 5 }),
|
|
444
462
|
clip({ id: "headline", start: 1.5, duration: 2 }),
|
|
@@ -448,7 +466,6 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
448
466
|
expect(
|
|
449
467
|
resolveTimelineExpansionRawId({
|
|
450
468
|
selectedElementId: null,
|
|
451
|
-
isPlaying: false,
|
|
452
469
|
currentTime: 2,
|
|
453
470
|
manifest,
|
|
454
471
|
parentMap,
|
|
@@ -468,7 +485,6 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
468
485
|
expect(
|
|
469
486
|
resolveTimelineExpansionRawId({
|
|
470
487
|
selectedElementId: null,
|
|
471
|
-
isPlaying: false,
|
|
472
488
|
currentTime: 12,
|
|
473
489
|
manifest,
|
|
474
490
|
parentMap,
|
|
@@ -491,7 +507,6 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
491
507
|
expect(
|
|
492
508
|
resolveTimelineExpansionRawId({
|
|
493
509
|
selectedElementId: null,
|
|
494
|
-
isPlaying: false,
|
|
495
510
|
currentTime: 5,
|
|
496
511
|
manifest,
|
|
497
512
|
parentMap,
|
|
@@ -499,7 +514,7 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
499
514
|
).toBe("second");
|
|
500
515
|
});
|
|
501
516
|
|
|
502
|
-
it("auto-expands the innermost active nested composition
|
|
517
|
+
it("auto-expands the innermost active nested composition", () => {
|
|
503
518
|
const manifest = [
|
|
504
519
|
clip({ id: "outer", start: 0, duration: 10 }),
|
|
505
520
|
clip({ id: "inner", start: 2, duration: 5 }),
|
|
@@ -513,7 +528,6 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
513
528
|
expect(
|
|
514
529
|
resolveTimelineExpansionRawId({
|
|
515
530
|
selectedElementId: null,
|
|
516
|
-
isPlaying: false,
|
|
517
531
|
currentTime: 3.5,
|
|
518
532
|
manifest,
|
|
519
533
|
parentMap,
|
|
@@ -521,7 +535,7 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
521
535
|
).toBe("inner");
|
|
522
536
|
});
|
|
523
537
|
|
|
524
|
-
it("
|
|
538
|
+
it("resolves an active composition from the current store time", () => {
|
|
525
539
|
const manifest = [
|
|
526
540
|
clip({ id: "scene", start: 0, duration: 5 }),
|
|
527
541
|
clip({ id: "headline", start: 1, duration: 2 }),
|
|
@@ -531,15 +545,49 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
531
545
|
expect(
|
|
532
546
|
resolveTimelineExpansionRawId({
|
|
533
547
|
selectedElementId: null,
|
|
534
|
-
isPlaying: true,
|
|
535
548
|
currentTime: 2,
|
|
536
549
|
manifest,
|
|
537
550
|
parentMap,
|
|
538
551
|
}),
|
|
539
|
-
).
|
|
552
|
+
).toBe("scene");
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("keeps inline children visible when the master timeline is playing", () => {
|
|
556
|
+
const elements = [
|
|
557
|
+
el({ id: "scene", domId: "scene", key: "index.html#scene", start: 0, duration: 5 }),
|
|
558
|
+
];
|
|
559
|
+
const manifest = [
|
|
560
|
+
clip({ id: "scene", start: 0, duration: 5, compositionSrc: "scene.html" }),
|
|
561
|
+
clip({ id: "headline", start: 1, duration: 2 }),
|
|
562
|
+
];
|
|
563
|
+
const parentMap = new Map([["headline", "scene"]]);
|
|
564
|
+
let rows: TimelineElement[] | undefined;
|
|
565
|
+
const host = document.createElement("div");
|
|
566
|
+
document.body.append(host);
|
|
567
|
+
const root = createRoot(host);
|
|
568
|
+
|
|
569
|
+
act(() => {
|
|
570
|
+
usePlayerStore.setState({
|
|
571
|
+
elements,
|
|
572
|
+
clipManifest: manifest,
|
|
573
|
+
clipParentMap: parentMap,
|
|
574
|
+
currentTime: 2,
|
|
575
|
+
isPlaying: true,
|
|
576
|
+
});
|
|
577
|
+
root.render(
|
|
578
|
+
React.createElement(TimelineExpansionHarness, {
|
|
579
|
+
onValue: (value) => (rows = value),
|
|
580
|
+
}),
|
|
581
|
+
);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
expect(usePlayerStore.getState().isPlaying).toBe(true);
|
|
585
|
+
expect(rows?.map((row) => row.domId ?? row.id)).toEqual(["scene", "headline"]);
|
|
586
|
+
|
|
587
|
+
act(() => root.unmount());
|
|
540
588
|
});
|
|
541
589
|
|
|
542
|
-
it("keeps selected elements ahead of
|
|
590
|
+
it("keeps selected elements ahead of active composition auto-expansion", () => {
|
|
543
591
|
const manifest = [
|
|
544
592
|
clip({ id: "scene", start: 0, duration: 6 }),
|
|
545
593
|
clip({ id: "headline", start: 1, duration: 2 }),
|
|
@@ -553,7 +601,6 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
553
601
|
expect(
|
|
554
602
|
resolveTimelineExpansionRawId({
|
|
555
603
|
selectedElementId: "caption",
|
|
556
|
-
isPlaying: false,
|
|
557
604
|
currentTime: 1.5,
|
|
558
605
|
manifest,
|
|
559
606
|
parentMap,
|
|
@@ -35,7 +35,6 @@ function resolveRawId(
|
|
|
35
35
|
|
|
36
36
|
interface TimelineExpansionRawIdInput {
|
|
37
37
|
selectedElementId: string | null;
|
|
38
|
-
isPlaying: boolean;
|
|
39
38
|
currentTime: number;
|
|
40
39
|
manifest: ClipManifestClip[];
|
|
41
40
|
parentMap: Map<string, string>;
|
|
@@ -97,14 +96,12 @@ function findActiveExpandableCompositionId(
|
|
|
97
96
|
|
|
98
97
|
export function resolveTimelineExpansionRawId({
|
|
99
98
|
selectedElementId,
|
|
100
|
-
isPlaying,
|
|
101
99
|
currentTime,
|
|
102
100
|
manifest,
|
|
103
101
|
parentMap,
|
|
104
102
|
}: TimelineExpansionRawIdInput): string | null {
|
|
105
103
|
const selectedRawId = resolveRawId(selectedElementId, manifest, parentMap);
|
|
106
104
|
if (selectedRawId) return selectedRawId;
|
|
107
|
-
if (isPlaying) return null;
|
|
108
105
|
return findActiveExpandableCompositionId(currentTime, manifest, parentMap);
|
|
109
106
|
}
|
|
110
107
|
|
|
@@ -334,7 +331,7 @@ export function buildExpandedElements(
|
|
|
334
331
|
: (el.key ?? el.id) === parentKey;
|
|
335
332
|
|
|
336
333
|
// ADDITIVE drill-in: the host row stays and its children are appended under
|
|
337
|
-
// it. Expansion is also triggered by the playhead alone (
|
|
334
|
+
// it. Expansion is also triggered by the playhead alone (active auto-expand),
|
|
338
335
|
// so substituting the host row made it vanish on an ordinary seek, and with
|
|
339
336
|
// it the host's keyframe lane, since diamonds render per row from
|
|
340
337
|
// `keyframeCache.get(elementKey)`. The synthetic fractional lanes above sit
|
|
@@ -356,13 +353,14 @@ export function useExpandedTimelineElements(): TimelineElement[] {
|
|
|
356
353
|
const clipParentMap = usePlayerStore((s) => s.clipParentMap);
|
|
357
354
|
const domClipChildren = usePlayerStore((s) => s.domClipChildren);
|
|
358
355
|
const selectedElementId = usePlayerStore((s) => s.selectedElementId);
|
|
359
|
-
const isPlaying = usePlayerStore((s) => s.isPlaying);
|
|
360
356
|
const currentTime = usePlayerStore((s) => s.currentTime);
|
|
361
357
|
|
|
362
|
-
// Resolve which raw clip drives expansion
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
// the
|
|
358
|
+
// Resolve which raw clip drives expansion from the store's committed playhead
|
|
359
|
+
// time. The RAF loop keeps live time out of React and Zustand during playback,
|
|
360
|
+
// so this target deliberately stays stable until the loop commits or a seek /
|
|
361
|
+
// pause updates the store. The scan re-runs on scrub ticks, but its RESULT only
|
|
362
|
+
// changes when the playhead crosses a composition boundary. Keying the expensive
|
|
363
|
+
// build below on these ids (not raw currentTime) avoids re-allocating
|
|
366
364
|
// expandedElements — and cascading TimelineClip re-renders — on every tick.
|
|
367
365
|
const { rawId, selectedRawId } = useMemo(() => {
|
|
368
366
|
if (!clipManifest || clipManifest.length === 0 || clipParentMap.size === 0) {
|
|
@@ -371,14 +369,13 @@ export function useExpandedTimelineElements(): TimelineElement[] {
|
|
|
371
369
|
return {
|
|
372
370
|
rawId: resolveTimelineExpansionRawId({
|
|
373
371
|
selectedElementId,
|
|
374
|
-
isPlaying,
|
|
375
372
|
currentTime,
|
|
376
373
|
manifest: clipManifest,
|
|
377
374
|
parentMap: clipParentMap,
|
|
378
375
|
}),
|
|
379
376
|
selectedRawId: resolveRawId(selectedElementId, clipManifest, clipParentMap),
|
|
380
377
|
};
|
|
381
|
-
}, [clipManifest, clipParentMap, selectedElementId,
|
|
378
|
+
}, [clipManifest, clipParentMap, selectedElementId, currentTime]);
|
|
382
379
|
|
|
383
380
|
return useMemo(() => {
|
|
384
381
|
if (!clipManifest || clipManifest.length === 0 || clipParentMap.size === 0) {
|
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
import {
|
|
36
36
|
readTimelineDurationFromDocument,
|
|
37
37
|
mergeTimelineElementsPreservingDowngrades,
|
|
38
|
-
parseTimelineFromDOM,
|
|
39
38
|
} from "../lib/timelineDOM";
|
|
40
39
|
import { normalizeToZones } from "../components/timelineZones";
|
|
41
40
|
import {
|
|
@@ -48,7 +47,7 @@ import { hasTimelinePerformanceFixtureLease } from "../lib/timelinePerformanceFi
|
|
|
48
47
|
import { applyCachedSourceDurations, probeMissingSourceDurations } from "../lib/mediaProbe";
|
|
49
48
|
import { shouldResumeForwardPlaybackAfterSeek, shouldStopAfterSeek } from "../lib/playbackSeek";
|
|
50
49
|
import { applyPreviewVariablesToUrl } from "../../hooks/previewVariablesStore";
|
|
51
|
-
import {
|
|
50
|
+
import { createPreviewMessageHandler } from "./previewMessageRouter";
|
|
52
51
|
import { timelineElementsChanged } from "./timelinePlayerSync";
|
|
53
52
|
|
|
54
53
|
export function useTimelinePlayer() {
|
|
@@ -488,51 +487,14 @@ export function useTimelinePlayer() {
|
|
|
488
487
|
const handleWindowKeyDown = (e: KeyboardEvent) => playbackKeyDownRef.current(e);
|
|
489
488
|
const handleWindowKeyUp = (e: KeyboardEvent) => playbackKeyUpRef.current(e);
|
|
490
489
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
if (data?.source === "hf-preview") {
|
|
501
|
-
if (!acceptStudioRuntimeMessage(data)) return;
|
|
502
|
-
}
|
|
503
|
-
if (data?.source === "hf-preview" && data?.type === "state") {
|
|
504
|
-
try {
|
|
505
|
-
if (usePlayerStore.getState().elements.length === 0) {
|
|
506
|
-
const iframeWin = ourIframe?.contentWindow as IframeWindow | null;
|
|
507
|
-
const manifest = iframeWin?.__clipManifest;
|
|
508
|
-
if (manifest && manifest.clips.length > 0) {
|
|
509
|
-
processTimelineMessageRef.current(manifest);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
const msSinceTimeline = Date.now() - lastTimelineMessageRef.current;
|
|
513
|
-
if (msSinceTimeline > 500) {
|
|
514
|
-
enrichMissingCompositionsRef.current();
|
|
515
|
-
}
|
|
516
|
-
} catch {}
|
|
517
|
-
}
|
|
518
|
-
if (data?.source === "hf-preview" && data?.type === "timeline" && Array.isArray(data.clips)) {
|
|
519
|
-
lastTimelineMessageRef.current = Date.now();
|
|
520
|
-
processTimelineMessageRef.current(data);
|
|
521
|
-
enrichMissingCompositionsRef.current();
|
|
522
|
-
if (usePlayerStore.getState().elements.length === 0) {
|
|
523
|
-
try {
|
|
524
|
-
const doc = ourIframe?.contentDocument;
|
|
525
|
-
const adapter = getAdapter();
|
|
526
|
-
if (doc && adapter) {
|
|
527
|
-
const els = parseTimelineFromDOM(doc, adapter.getDuration());
|
|
528
|
-
if (els.length > 0) {
|
|
529
|
-
syncTimelineElements(els);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
} catch {}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
};
|
|
490
|
+
const handleMessage = createPreviewMessageHandler({
|
|
491
|
+
iframeRef,
|
|
492
|
+
processTimelineMessageRef,
|
|
493
|
+
enrichMissingCompositionsRef,
|
|
494
|
+
lastTimelineMessageRef,
|
|
495
|
+
getAdapter,
|
|
496
|
+
syncTimelineElements,
|
|
497
|
+
});
|
|
536
498
|
|
|
537
499
|
const handleVisibilityChange = () => {
|
|
538
500
|
if (document.hidden && usePlayerStore.getState().isPlaying) {
|
|
@@ -12,6 +12,7 @@ import type { TimelineElement } from "../store/playerStore";
|
|
|
12
12
|
import type { ClipManifestClip } from "./playbackTypes";
|
|
13
13
|
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
14
14
|
import { readClipTiming } from "@hyperframes/core/composition-contract";
|
|
15
|
+
import { resolveAudioGroups } from "@hyperframes/core/audio-groups";
|
|
15
16
|
import {
|
|
16
17
|
resolveMediaElement,
|
|
17
18
|
applyMediaMetadataFromElement,
|
|
@@ -67,6 +68,37 @@ function resolveClipTag(clip: ClipManifestClip): string {
|
|
|
67
68
|
return clip.tagName || clip.kind || "div";
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
// One `<hf-audio-group>` scan per document, not per clip — resolveAudioGroups
|
|
72
|
+
// walks the whole tree, and a parse touches every clip in it.
|
|
73
|
+
interface GroupInfo {
|
|
74
|
+
label: string;
|
|
75
|
+
volume: number;
|
|
76
|
+
hidden: boolean;
|
|
77
|
+
fxChain?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const groupInfoCache = new WeakMap<Document, Map<string, GroupInfo>>();
|
|
81
|
+
|
|
82
|
+
function groupInfoFor(doc: Document | null | undefined, groupId: string): GroupInfo {
|
|
83
|
+
if (!doc) return { label: groupId, volume: 1, hidden: false };
|
|
84
|
+
let info = groupInfoCache.get(doc);
|
|
85
|
+
if (!info) {
|
|
86
|
+
info = new Map(
|
|
87
|
+
resolveAudioGroups(doc).map((group) => [
|
|
88
|
+
group.id,
|
|
89
|
+
{
|
|
90
|
+
label: group.label,
|
|
91
|
+
volume: group.volume,
|
|
92
|
+
hidden: group.hidden,
|
|
93
|
+
...(group.fxChain ? { fxChain: group.fxChain } : {}),
|
|
94
|
+
},
|
|
95
|
+
]),
|
|
96
|
+
);
|
|
97
|
+
groupInfoCache.set(doc, info);
|
|
98
|
+
}
|
|
99
|
+
return info.get(groupId) ?? { label: groupId, volume: 1, hidden: false };
|
|
100
|
+
}
|
|
101
|
+
|
|
70
102
|
// fallow-ignore-next-line complexity
|
|
71
103
|
export function createTimelineElementFromManifestClip(params: {
|
|
72
104
|
clip: ClipManifestClip;
|
|
@@ -138,6 +170,15 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
138
170
|
if (hostEl.hasAttribute("data-hidden")) entry.hidden = true;
|
|
139
171
|
const timelineRole = hostEl.getAttribute("data-timeline-role");
|
|
140
172
|
if (timelineRole) entry.timelineRole = timelineRole;
|
|
173
|
+
const audioGroup = hostEl.getAttribute("data-audio-group");
|
|
174
|
+
if (audioGroup) {
|
|
175
|
+
entry.audioGroup = audioGroup;
|
|
176
|
+
const info = groupInfoFor(doc ?? hostEl.ownerDocument, audioGroup);
|
|
177
|
+
entry.audioGroupLabel = info.label;
|
|
178
|
+
entry.audioGroupVolume = info.volume;
|
|
179
|
+
entry.audioGroupHidden = info.hidden;
|
|
180
|
+
if (info.fxChain) entry.audioGroupFxChain = info.fxChain;
|
|
181
|
+
}
|
|
141
182
|
const fxChain = hostEl.getAttribute("data-fx-chain");
|
|
142
183
|
if (fxChain) entry.fxChain = fxChain;
|
|
143
184
|
const automation = hostEl.getAttribute("data-automation");
|
|
@@ -356,6 +397,16 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
356
397
|
const timelineRole = el.getAttribute("data-timeline-role");
|
|
357
398
|
if (timelineRole) entry.timelineRole = timelineRole;
|
|
358
399
|
|
|
400
|
+
const domAudioGroup = el.getAttribute("data-audio-group");
|
|
401
|
+
if (domAudioGroup) {
|
|
402
|
+
entry.audioGroup = domAudioGroup;
|
|
403
|
+
const domGroupInfo = groupInfoFor(doc, domAudioGroup);
|
|
404
|
+
entry.audioGroupLabel = domGroupInfo.label;
|
|
405
|
+
entry.audioGroupVolume = domGroupInfo.volume;
|
|
406
|
+
entry.audioGroupHidden = domGroupInfo.hidden;
|
|
407
|
+
if (domGroupInfo.fxChain) entry.audioGroupFxChain = domGroupInfo.fxChain;
|
|
408
|
+
}
|
|
409
|
+
|
|
359
410
|
// Sub-compositions
|
|
360
411
|
const compSrc =
|
|
361
412
|
el.getAttribute("data-composition-src") || el.getAttribute("data-composition-file");
|