fix(studio): reapply manual edits during playback

This commit is contained in:
Vance Ingalls
2026-05-10 18:39:14 +00:00
committed by Miguel Ángel
parent fa208641be
commit 79b33aca0a
2 changed files with 252 additions and 14 deletions
@@ -698,13 +698,16 @@ describe("studio manual edits", () => {
it("reapplies the latest preview manifest after wrapped seeks", () => {
const window = new Window();
const seekArgs: unknown[][] = [];
const previewWindow = window as unknown as Parameters<
typeof installStudioManualEditSeekReapply
>[0] & {
__player: Record<string, unknown>;
};
previewWindow.__player = {
seek: () => {},
seek: (...args: unknown[]) => {
seekArgs.push(args);
},
};
let applied = 0;
@@ -713,8 +716,9 @@ describe("studio manual edits", () => {
applied += 1;
}),
).toBe(true);
(previewWindow.__player.seek as (time: number) => void)(1);
(previewWindow.__player.seek as (time: number, suppressEvents: boolean) => void)(1, false);
expect(applied).toBe(1);
expect(seekArgs).toEqual([[1, false]]);
expect(
installStudioManualEditSeekReapply(previewWindow, () => {
@@ -724,4 +728,88 @@ describe("studio manual edits", () => {
(previewWindow.__player.seek as (time: number) => void)(2);
expect(applied).toBe(11);
});
it("reapplies manual edits while fresh playback is active", () => {
const window = new Window();
const frames: FrameRequestCallback[] = [];
let playing = false;
const previewWindow = window as unknown as Parameters<
typeof installStudioManualEditSeekReapply
>[0] & {
__player: Record<string, unknown>;
requestAnimationFrame: (callback: FrameRequestCallback) => number;
};
previewWindow.requestAnimationFrame = (callback: FrameRequestCallback) => {
frames.push(callback);
return frames.length;
};
previewWindow.__player = {
play: () => {
playing = true;
},
isPlaying: () => playing,
};
let applied = 0;
expect(
installStudioManualEditSeekReapply(previewWindow, () => {
applied += 1;
}),
).toBe(true);
(previewWindow.__player.play as () => void)();
expect(applied).toBe(1);
expect(frames).toHaveLength(1);
frames.shift()?.(16);
expect(applied).toBe(2);
expect(frames).toHaveLength(1);
playing = false;
frames.shift()?.(32);
expect(applied).toBe(3);
expect(frames).toHaveLength(0);
});
it("stops playback reapply after an unpaused timeline has completed", () => {
const window = new Window();
const frames: FrameRequestCallback[] = [];
let currentTime = 0;
let paused = true;
const previewWindow = window as unknown as Parameters<
typeof installStudioManualEditSeekReapply
>[0] & {
__timeline: Record<string, unknown>;
requestAnimationFrame: (callback: FrameRequestCallback) => number;
};
previewWindow.requestAnimationFrame = (callback: FrameRequestCallback) => {
frames.push(callback);
return frames.length;
};
previewWindow.__timeline = {
play: () => {
paused = false;
},
paused: () => paused,
isActive: () => false,
time: () => currentTime,
duration: () => 2,
};
let applied = 0;
expect(
installStudioManualEditSeekReapply(previewWindow, () => {
applied += 1;
}),
).toBe(true);
(previewWindow.__timeline.play as () => void)();
expect(applied).toBe(1);
expect(frames).toHaveLength(1);
currentTime = 2;
frames.shift()?.(16);
expect(applied).toBe(2);
expect(frames).toHaveLength(0);
});
});
@@ -28,7 +28,8 @@ const STUDIO_ORIGINAL_DISPLAY_ATTR = "data-hf-studio-original-display";
const STUDIO_ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate";
const STUDIO_ORIGINAL_INLINE_ROTATE_ATTR = "data-hf-studio-original-inline-rotate";
const STUDIO_MANUAL_EDITS_APPLY_PROP = "__hfStudioManualEditsApply";
const STUDIO_MANUAL_EDITS_WRAPPED_SEEK_PROP = "__hfStudioManualEditsWrapped";
const STUDIO_MANUAL_EDITS_WRAPPED_PROP = "__hfStudioManualEditsWrapped";
const STUDIO_MANUAL_EDITS_PLAYBACK_FRAME_PROP = "__hfStudioManualEditsPlaybackFrame";
let studioManualEditGestureId = 0;
export interface StudioManualEditTarget {
@@ -74,6 +75,7 @@ type StudioManualEditSeekWindow = Window & {
__timeline?: Record<string, unknown>;
__timelines?: Record<string, Record<string, unknown>>;
__hfStudioManualEditsApply?: () => void;
__hfStudioManualEditsPlaybackFrame?: number | null;
};
export function emptyStudioManualEditManifest(): StudioManualEditManifest {
@@ -953,24 +955,24 @@ export function restoreStudioPathOffset(
);
}
function markSeekWrapped(fn: (time: number) => unknown): void {
function markWrapped(fn: (...args: unknown[]) => unknown): void {
try {
Object.defineProperty(fn, STUDIO_MANUAL_EDITS_WRAPPED_SEEK_PROP, {
Object.defineProperty(fn, STUDIO_MANUAL_EDITS_WRAPPED_PROP, {
configurable: false,
enumerable: false,
value: true,
});
} catch {
try {
(fn as unknown as Record<string, unknown>)[STUDIO_MANUAL_EDITS_WRAPPED_SEEK_PROP] = true;
(fn as unknown as Record<string, unknown>)[STUDIO_MANUAL_EDITS_WRAPPED_PROP] = true;
} catch {
// Ignore non-extensible functions.
}
}
}
function isSeekWrapped(fn: (time: number) => unknown): boolean {
return Boolean((fn as unknown as Record<string, unknown>)[STUDIO_MANUAL_EDITS_WRAPPED_SEEK_PROP]);
function isWrapped(fn: (...args: unknown[]) => unknown): boolean {
return Boolean((fn as unknown as Record<string, unknown>)[STUDIO_MANUAL_EDITS_WRAPPED_PROP]);
}
function wrapSeekReapplyFunction(
@@ -980,19 +982,147 @@ function wrapSeekReapplyFunction(
): boolean {
const fn = owner?.[key];
if (!owner || typeof fn !== "function") return false;
const seek = fn as (time: number) => unknown;
if (isSeekWrapped(seek)) return true;
const seek = fn as (...args: unknown[]) => unknown;
if (isWrapped(seek)) return true;
const wrappedSeek = function (this: unknown, time: number): unknown {
const result = seek.call(this, time);
const wrappedSeek = function (this: unknown, ...args: unknown[]): unknown {
const result = seek.apply(this, args);
win.__hfStudioManualEditsApply?.();
return result;
};
markSeekWrapped(wrappedSeek);
markWrapped(wrappedSeek);
owner[key] = wrappedSeek;
return true;
}
function readOwnerNumber(owner: Record<string, unknown>, key: string): number | null {
const fn = owner[key];
if (typeof fn !== "function") return null;
try {
return finiteNumber(fn.call(owner));
} catch {
return null;
}
}
function hasRemainingTimelineTime(owner: Record<string, unknown>): boolean {
const duration = readOwnerNumber(owner, "duration") ?? readOwnerNumber(owner, "getDuration");
if (duration == null) return true;
if (duration <= 0) return false;
const time =
readOwnerNumber(owner, "time") ??
readOwnerNumber(owner, "totalTime") ??
readOwnerNumber(owner, "getTime");
if (time == null) return true;
return time < duration;
}
function isTimelinePlaying(owner: Record<string, unknown> | undefined): boolean {
if (!owner) return false;
const isPlaying = owner.isPlaying;
if (typeof isPlaying === "function") {
try {
return Boolean(isPlaying.call(owner));
} catch {
return false;
}
}
const paused = owner.paused;
if (typeof paused === "function") {
try {
if (paused.call(owner)) return false;
} catch {
return false;
}
const isActive = owner.isActive;
if (typeof isActive === "function") {
try {
if (isActive.call(owner)) return true;
} catch {
return false;
}
}
return hasRemainingTimelineTime(owner);
}
const isActive = owner.isActive;
if (typeof isActive === "function") {
try {
return Boolean(isActive.call(owner));
} catch {
return false;
}
}
return false;
}
function isStudioManualEditPlaybackActive(win: StudioManualEditSeekWindow): boolean {
if (isTimelinePlaying(win.__player)) return true;
if (isTimelinePlaying(win.__timeline)) return true;
return Object.values(win.__timelines ?? {}).some(isTimelinePlaying);
}
function startStudioManualEditPlaybackReapply(win: StudioManualEditSeekWindow): void {
win.__hfStudioManualEditsApply?.();
if (win[STUDIO_MANUAL_EDITS_PLAYBACK_FRAME_PROP] != null) return;
const tick = () => {
win.__hfStudioManualEditsApply?.();
if (!isStudioManualEditPlaybackActive(win)) {
win[STUDIO_MANUAL_EDITS_PLAYBACK_FRAME_PROP] = null;
return;
}
win[STUDIO_MANUAL_EDITS_PLAYBACK_FRAME_PROP] = win.requestAnimationFrame(tick);
};
win[STUDIO_MANUAL_EDITS_PLAYBACK_FRAME_PROP] = win.requestAnimationFrame(tick);
}
function wrapPlayReapplyFunction(
win: StudioManualEditSeekWindow,
owner: Record<string, unknown> | undefined,
key: string,
): boolean {
const fn = owner?.[key];
if (!owner || typeof fn !== "function") return false;
const play = fn as (...args: unknown[]) => unknown;
if (isWrapped(play)) return true;
const wrappedPlay = function (this: unknown, ...args: unknown[]): unknown {
const result = play.apply(this, args);
startStudioManualEditPlaybackReapply(win);
return result;
};
markWrapped(wrappedPlay);
owner[key] = wrappedPlay;
return true;
}
function wrapApplyAfterFunction(
win: StudioManualEditSeekWindow,
owner: Record<string, unknown> | undefined,
key: string,
): boolean {
const fn = owner?.[key];
if (!owner || typeof fn !== "function") return false;
const applyAfter = fn as (...args: unknown[]) => unknown;
if (isWrapped(applyAfter)) return true;
const wrappedApplyAfter = function (this: unknown, ...args: unknown[]): unknown {
const result = applyAfter.apply(this, args);
win.__hfStudioManualEditsApply?.();
return result;
};
markWrapped(wrappedApplyAfter);
owner[key] = wrappedApplyAfter;
return true;
}
export function installStudioManualEditSeekReapply(win: Window, apply: () => void): boolean {
const studioWin = win as StudioManualEditSeekWindow;
studioWin[STUDIO_MANUAL_EDITS_APPLY_PROP] = apply;
@@ -1005,10 +1135,24 @@ export function installStudioManualEditSeekReapply(win: Window, apply: () => voi
"renderSeek",
);
const wrappedTimelineSeek = wrapSeekReapplyFunction(studioWin, studioWin.__timeline, "seek");
const wrappedPlayerPlay = wrapPlayReapplyFunction(studioWin, studioWin.__player, "play");
const wrappedTimelinePlay = wrapPlayReapplyFunction(studioWin, studioWin.__timeline, "play");
const wrappedPlayerPause = wrapApplyAfterFunction(studioWin, studioWin.__player, "pause");
const wrappedTimelinePause = wrapApplyAfterFunction(studioWin, studioWin.__timeline, "pause");
let wrappedNamedTimelineSeek = false;
let wrappedNamedTimelinePlay = false;
let wrappedNamedTimelinePause = false;
for (const timeline of Object.values(studioWin.__timelines ?? {})) {
wrappedNamedTimelineSeek =
wrapSeekReapplyFunction(studioWin, timeline, "seek") || wrappedNamedTimelineSeek;
wrappedNamedTimelinePlay =
wrapPlayReapplyFunction(studioWin, timeline, "play") || wrappedNamedTimelinePlay;
wrappedNamedTimelinePause =
wrapApplyAfterFunction(studioWin, timeline, "pause") || wrappedNamedTimelinePause;
}
if (isStudioManualEditPlaybackActive(studioWin)) {
startStudioManualEditPlaybackReapply(studioWin);
}
return (
@@ -1016,7 +1160,13 @@ export function installStudioManualEditSeekReapply(win: Window, apply: () => voi
wrappedPlayerSeek ||
wrappedPlayerRenderSeek ||
wrappedTimelineSeek ||
wrappedNamedTimelineSeek
wrappedPlayerPlay ||
wrappedTimelinePlay ||
wrappedPlayerPause ||
wrappedTimelinePause ||
wrappedNamedTimelineSeek ||
wrappedNamedTimelinePlay ||
wrappedNamedTimelinePause
);
}