@hyperframes/studio 0.8.148 → 0.8.150
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-BBBg-uvu.js → hyperframes-player-kup62bDR.js} +1 -1
- package/dist/assets/{index-BXJLeCqB.js → index-B8BBYY8L.js} +1 -1
- package/dist/assets/{index-BJIpZeuk.js → index-CS_m0TkX.js} +131 -131
- package/dist/assets/{index-DARhiE_i.js → index-DQ5jGodF.js} +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.html +1 -1
- package/dist/index.js +8 -1
- package/package.json +8 -8
- package/src/hooks/usePersistentEditHistory.test.ts +43 -1
- package/src/hooks/usePersistentEditHistory.ts +3 -1
- package/src/player/lib/timelineDOM.test.ts +55 -0
- package/src/player/lib/timelineDOM.ts +5 -0
- package/src/player/store/timelineElement.ts +2 -1
- package/src/utils/audioMeterMath.test.ts +9 -0
package/dist/index.d.ts
CHANGED
|
@@ -354,9 +354,10 @@ interface TimelineElement {
|
|
|
354
354
|
/** Clip-edge fades from `data-fade-in` / `data-fade-out`, seconds; absent means none. */
|
|
355
355
|
fadeIn?: number;
|
|
356
356
|
fadeOut?: number;
|
|
357
|
-
/** Verbatim `data-fx-chain` / `data-automation
|
|
357
|
+
/** Verbatim `data-fx-chain` / `data-automation` (`masterChain`: the root's); see automationLaneData. */
|
|
358
358
|
fxChain?: string;
|
|
359
359
|
automation?: string;
|
|
360
|
+
masterChain?: string;
|
|
360
361
|
/** Path from data-composition-src — identifies sub-composition elements */
|
|
361
362
|
compositionSrc?: string;
|
|
362
363
|
/** Set by data-timeline-locked on the host element — disables move and trim in Studio. */
|
package/dist/index.html
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
document.documentElement.dataset.theme = "paper";
|
|
15
15
|
}
|
|
16
16
|
</script>
|
|
17
|
-
<script type="module" crossorigin src="/assets/index-
|
|
17
|
+
<script type="module" crossorigin src="/assets/index-CS_m0TkX.js"></script>
|
|
18
18
|
<link rel="stylesheet" crossorigin href="/assets/index-DkQKsFxx.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
package/dist/index.js
CHANGED
|
@@ -29115,6 +29115,7 @@ import { useCallback as useCallback38, useRef as useRef46 } from "react";
|
|
|
29115
29115
|
// src/player/lib/timelineDOM.ts
|
|
29116
29116
|
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
29117
29117
|
import { readClipTiming as readClipTiming2 } from "@hyperframes/core/composition-contract";
|
|
29118
|
+
import { readMasterFxChain } from "@hyperframes/core/audio-groups";
|
|
29118
29119
|
import { linkScopeOf } from "@hyperframes/core/media-link";
|
|
29119
29120
|
import { createRuntimeStartTimeResolver as createRuntimeStartTimeResolver2 } from "@hyperframes/core/runtime/start-resolver";
|
|
29120
29121
|
|
|
@@ -29693,6 +29694,8 @@ function createTimelineElementFromManifestClip(params) {
|
|
|
29693
29694
|
if (info.fxChain) entry2.audioGroupFxChain = info.fxChain;
|
|
29694
29695
|
if (info.automation) entry2.audioGroupAutomation = info.automation;
|
|
29695
29696
|
}
|
|
29697
|
+
const masterChain = readMasterFxChain(doc ?? hostEl.ownerDocument);
|
|
29698
|
+
if (masterChain) entry2.masterChain = masterChain;
|
|
29696
29699
|
const fxChain = hostEl.getAttribute("data-fx-chain");
|
|
29697
29700
|
if (fxChain) entry2.fxChain = fxChain;
|
|
29698
29701
|
const automation = hostEl.getAttribute("data-automation");
|
|
@@ -29815,6 +29818,8 @@ function parseTimelineFromDOM(doc, rootDuration, timelines) {
|
|
|
29815
29818
|
const resolvedSrc = mediaEl.src || void 0;
|
|
29816
29819
|
if (resolvedSrc) entry2.src = resolvedSrc;
|
|
29817
29820
|
}
|
|
29821
|
+
const domMasterChain = readMasterFxChain(doc);
|
|
29822
|
+
if (domMasterChain) entry2.masterChain = domMasterChain;
|
|
29818
29823
|
const domFxChain = el.getAttribute("data-fx-chain");
|
|
29819
29824
|
if (domFxChain) entry2.fxChain = domFxChain;
|
|
29820
29825
|
const domAutomation = el.getAttribute("data-automation");
|
|
@@ -74831,7 +74836,9 @@ function claimHeld(reply, label) {
|
|
|
74831
74836
|
if (reply.ok)
|
|
74832
74837
|
return reply.body?.claimed?.id ?? null;
|
|
74833
74838
|
if (reply.status !== 404)
|
|
74834
|
-
console.error(
|
|
74839
|
+
console.error(
|
|
74840
|
+
`[history] ${reply.status}: "${label}" was not recorded as your edit: ${reply.error}`
|
|
74841
|
+
);
|
|
74835
74842
|
return null;
|
|
74836
74843
|
}
|
|
74837
74844
|
async function overwroteVersions(files) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.150",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"dockview-react": "8.3.1",
|
|
61
61
|
"gsap": "^3.13.0",
|
|
62
62
|
"mediabunny": "^1.45.3",
|
|
63
|
-
"@hyperframes/parsers": "0.8.
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/sdk": "0.8.
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/
|
|
68
|
-
"@hyperframes/
|
|
63
|
+
"@hyperframes/parsers": "0.8.150",
|
|
64
|
+
"@hyperframes/player": "0.8.150",
|
|
65
|
+
"@hyperframes/sdk": "0.8.150",
|
|
66
|
+
"@hyperframes/studio-server": "0.8.150",
|
|
67
|
+
"@hyperframes/core": "0.8.150",
|
|
68
|
+
"@hyperframes/shader-transitions": "0.8.150"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"vite": "^6.4.2",
|
|
83
83
|
"vitest": "^4.1.11",
|
|
84
84
|
"zustand": "^5.0.0",
|
|
85
|
-
"@hyperframes/producer": "0.8.
|
|
85
|
+
"@hyperframes/producer": "0.8.150"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
88
|
"react": "19",
|
|
@@ -561,7 +561,9 @@ it("a claim the server refused is logged, and the write stays undoable as a chan
|
|
|
561
561
|
files: { "index.html": { before: "A", after: "B" } },
|
|
562
562
|
}),
|
|
563
563
|
);
|
|
564
|
-
expect(logged).toHaveBeenCalledWith(
|
|
564
|
+
expect(logged).toHaveBeenCalledWith(
|
|
565
|
+
'[history] 500: "Moved Title" was not recorded as your edit: disk full',
|
|
566
|
+
);
|
|
565
567
|
restoreFetch();
|
|
566
568
|
expect(await act(() => hook().undo({ readFile }))).toMatchObject({
|
|
567
569
|
ok: true,
|
|
@@ -570,6 +572,46 @@ it("a claim the server refused is logged, and the write stays undoable as a chan
|
|
|
570
572
|
expect(file()).toBe("A");
|
|
571
573
|
});
|
|
572
574
|
|
|
575
|
+
it.each([
|
|
576
|
+
[
|
|
577
|
+
"the server refused",
|
|
578
|
+
409,
|
|
579
|
+
() => answer("/history/claim", 409, { error: "newer edit" }),
|
|
580
|
+
"newer edit",
|
|
581
|
+
],
|
|
582
|
+
[
|
|
583
|
+
"could not reach the server",
|
|
584
|
+
0,
|
|
585
|
+
() => {
|
|
586
|
+
const real = globalThis.fetch;
|
|
587
|
+
vi.stubGlobal("fetch", (url: string, init?: RequestInit) =>
|
|
588
|
+
url.endsWith("/history/claim")
|
|
589
|
+
? Promise.reject(new TypeError("fetch failed"))
|
|
590
|
+
: real(url, init),
|
|
591
|
+
);
|
|
592
|
+
},
|
|
593
|
+
"Studio could not reach its server.",
|
|
594
|
+
],
|
|
595
|
+
] as const)(
|
|
596
|
+
"a claim that %s logs under [history] with its status",
|
|
597
|
+
async (_, status, stub, reason) => {
|
|
598
|
+
const { hook, save } = await studio();
|
|
599
|
+
const logged = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
600
|
+
cleanup.push(() => logged.mockRestore());
|
|
601
|
+
stub();
|
|
602
|
+
save("B");
|
|
603
|
+
await act(() =>
|
|
604
|
+
hook().recordEdit({
|
|
605
|
+
label: "Moved Title",
|
|
606
|
+
files: { "index.html": { before: "A", after: "B" } },
|
|
607
|
+
}),
|
|
608
|
+
);
|
|
609
|
+
expect(logged).toHaveBeenCalledWith(
|
|
610
|
+
`[history] ${status}: "Moved Title" was not recorded as your edit: ${reason}`,
|
|
611
|
+
);
|
|
612
|
+
},
|
|
613
|
+
);
|
|
614
|
+
|
|
573
615
|
it("a step whose reply cannot be read says so, instead of throwing", async () => {
|
|
574
616
|
const { hook, readFile } = await studio();
|
|
575
617
|
answer("/history/step", 200, "<html>");
|
|
@@ -154,7 +154,9 @@ function claimHeld(reply: Awaited<ReturnType<typeof post>>, label: string): stri
|
|
|
154
154
|
if (reply.ok)
|
|
155
155
|
return (reply.body as { claimed: { id: string } | null } | null)?.claimed?.id ?? null;
|
|
156
156
|
if (reply.status !== 404)
|
|
157
|
-
console.error(
|
|
157
|
+
console.error(
|
|
158
|
+
`[history] ${reply.status}: "${label}" was not recorded as your edit: ${reply.error}`,
|
|
159
|
+
);
|
|
158
160
|
return null;
|
|
159
161
|
}
|
|
160
162
|
|
|
@@ -632,6 +632,61 @@ describe("audio FX attributes on parsed elements", () => {
|
|
|
632
632
|
});
|
|
633
633
|
});
|
|
634
634
|
|
|
635
|
+
describe("the master chain on parsed elements", () => {
|
|
636
|
+
const CHAIN = '{"version":1,"nodes":[{"type":"truepeak","id":"m1","params":{}}]}';
|
|
637
|
+
const parseVideo = (rootAttrs: string, inner = ""): TimelineElement | undefined => {
|
|
638
|
+
const doc = new DOMParser().parseFromString(
|
|
639
|
+
`<div data-composition-id="main" data-start="0" data-duration="10" ${rootAttrs}>
|
|
640
|
+
<video id="talk" data-start="0" data-duration="10" data-has-audio="true"></video>${inner}
|
|
641
|
+
</div>`,
|
|
642
|
+
"text/html",
|
|
643
|
+
);
|
|
644
|
+
return parseTimelineFromDOM(doc, 10).find((e) => e.domId === "talk");
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
it("copies the root's data-fx-chain onto an element that has none of its own", () => {
|
|
648
|
+
const talk = parseVideo(`data-fx-chain='${CHAIN}'`);
|
|
649
|
+
expect(talk?.masterChain).toBe(CHAIN);
|
|
650
|
+
expect(talk?.fxChain).toBeUndefined();
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("leaves it unset when the root carries none", () => {
|
|
654
|
+
expect(parseVideo("")?.masterChain).toBeUndefined();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("ignores a sub-composition root's chain", () => {
|
|
658
|
+
const inner = `<div data-composition-id="inner" data-fx-chain='${CHAIN}'></div>`;
|
|
659
|
+
expect(parseVideo("", inner)?.masterChain).toBeUndefined();
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("carries it on the manifest path too", () => {
|
|
663
|
+
const doc = makeDoc(`
|
|
664
|
+
<div data-composition-id="main" data-fx-chain='${CHAIN}'>
|
|
665
|
+
<video id="talk" data-has-audio="true"></video>
|
|
666
|
+
</div>
|
|
667
|
+
`);
|
|
668
|
+
const element = createTimelineElementFromManifestClip({
|
|
669
|
+
clip: {
|
|
670
|
+
id: "talk",
|
|
671
|
+
label: "Talk",
|
|
672
|
+
kind: "video",
|
|
673
|
+
tagName: "video",
|
|
674
|
+
start: 0,
|
|
675
|
+
duration: 10,
|
|
676
|
+
track: 0,
|
|
677
|
+
compositionId: null,
|
|
678
|
+
parentCompositionId: "main",
|
|
679
|
+
compositionSrc: null,
|
|
680
|
+
assetUrl: null,
|
|
681
|
+
},
|
|
682
|
+
fallbackIndex: 0,
|
|
683
|
+
doc,
|
|
684
|
+
hostEl: doc.getElementById("talk"),
|
|
685
|
+
});
|
|
686
|
+
expect(element.masterChain).toBe(CHAIN);
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
635
690
|
// A composition clip trimmed by 30 px collapsed to its first nested video's 1.77 s: that length capped the trim.
|
|
636
691
|
describe("a composition clip's source length", () => {
|
|
637
692
|
const scene = `
|
|
@@ -12,6 +12,7 @@ import type { TimelineElement } from "../store/playerStore";
|
|
|
12
12
|
import type { ClipManifestClip, IframeWindow, TimelineLike } from "./playbackTypes";
|
|
13
13
|
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
14
14
|
import { readClipTiming } from "@hyperframes/core/composition-contract";
|
|
15
|
+
import { readMasterFxChain } from "@hyperframes/core/audio-groups";
|
|
15
16
|
import { linkScopeOf } from "@hyperframes/core/media-link";
|
|
16
17
|
import { createRuntimeStartTimeResolver } from "@hyperframes/core/runtime/start-resolver";
|
|
17
18
|
import { groupInfoFor } from "./timelineGroupInfo";
|
|
@@ -167,6 +168,8 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
167
168
|
if (info.fxChain) entry.audioGroupFxChain = info.fxChain;
|
|
168
169
|
if (info.automation) entry.audioGroupAutomation = info.automation;
|
|
169
170
|
}
|
|
171
|
+
const masterChain = readMasterFxChain(doc ?? hostEl.ownerDocument);
|
|
172
|
+
if (masterChain) entry.masterChain = masterChain;
|
|
170
173
|
const fxChain = hostEl.getAttribute("data-fx-chain");
|
|
171
174
|
if (fxChain) entry.fxChain = fxChain;
|
|
172
175
|
const automation = hostEl.getAttribute("data-automation");
|
|
@@ -330,6 +333,8 @@ export function parseTimelineFromDOM(
|
|
|
330
333
|
// Read from the element, like the manifest path does: without these an audio
|
|
331
334
|
// clip parsed straight from the DOM reserved no automation height and drew no
|
|
332
335
|
// lanes, while the property panel still showed its chain.
|
|
336
|
+
const domMasterChain = readMasterFxChain(doc);
|
|
337
|
+
if (domMasterChain) entry.masterChain = domMasterChain;
|
|
333
338
|
const domFxChain = el.getAttribute("data-fx-chain");
|
|
334
339
|
if (domFxChain) entry.fxChain = domFxChain;
|
|
335
340
|
const domAutomation = el.getAttribute("data-automation");
|
|
@@ -62,9 +62,10 @@ export interface TimelineElement {
|
|
|
62
62
|
/** Clip-edge fades from `data-fade-in` / `data-fade-out`, seconds; absent means none. */
|
|
63
63
|
fadeIn?: number;
|
|
64
64
|
fadeOut?: number;
|
|
65
|
-
/** Verbatim `data-fx-chain` / `data-automation
|
|
65
|
+
/** Verbatim `data-fx-chain` / `data-automation` (`masterChain`: the root's); see automationLaneData. */
|
|
66
66
|
fxChain?: string;
|
|
67
67
|
automation?: string;
|
|
68
|
+
masterChain?: string;
|
|
68
69
|
/** Path from data-composition-src — identifies sub-composition elements */
|
|
69
70
|
compositionSrc?: string;
|
|
70
71
|
/** Set by data-timeline-locked on the host element — disables move and trim in Studio. */
|
|
@@ -69,6 +69,15 @@ describe("hasProjectAudio", () => {
|
|
|
69
69
|
expect(hasProjectAudio([clip({ tag: "video", hasAudio: true, ...fields })])).toBe(true);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
it("counts an audible video when the composition root carries a master chain", () => {
|
|
73
|
+
const masterChain = '{"version":1,"nodes":[]}';
|
|
74
|
+
expect(hasProjectAudio([clip({ tag: "video", hasAudio: true, masterChain })])).toBe(true);
|
|
75
|
+
expect(
|
|
76
|
+
hasProjectAudio([clip({ tag: "video", muted: true, hasAudio: true, masterChain })]),
|
|
77
|
+
).toBe(false);
|
|
78
|
+
expect(hasProjectAudio([clip({ tag: "video", masterChain })])).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
72
81
|
it("ignores a muted video even when it carries processing", () => {
|
|
73
82
|
expect(hasProjectAudio([clip({ tag: "video", hasAudio: true, muted: true, volume: 2 })])).toBe(
|
|
74
83
|
false,
|