fix(core): a preview media load that fails is reported, not swallowed (#6666)

* fix(core): a preview media load that fails is reported, not swallowed

* fix(core): an audio fed by source children reports its failed load; the shape lives in the protocol

* fix(core): the audio load watcher finds elements from any realm

* build(core): regenerate the inline position-edits render script
This commit is contained in:
Miguel Ángel
2026-10-28 13:49:30 +00:00
committed by GitHub
parent cd3b90d1ed
commit cf627a8619
11 changed files with 274 additions and 3 deletions
@@ -1,6 +1,6 @@
// Generated by scripts/build-position-edits-render.ts; do not edit.
const POSITION_EDITS_RENDER_IIFE: string =
'"use strict";(()=>{function x(){return globalThis}function P(e,t){if(typeof window>"u")return;let o=x(),r=o.__hf?.onSwallowed;if(r)try{r({label:e,error:t})}catch(s){}(o.__hfDebug||o.__HYPERFRAMES_DEBUG)&&console.debug(`[hyperframes] ${e} swallowed:`,t)}var T=null;function R(e,t){if(T)try{T({source:"hf-preview",type:"analytics",event:e,properties:t??{}})}catch(o){P("runtime.analytics.site1",o)}}var M="http://www.w3.org/1999/xhtml";function N(e){return e!=null&&e.nodeType===1}function _(e){return N(e)&&(e.namespaceURI===M||e.namespaceURI==="http://www.w3.org/2000/svg")}var y="data-hf-edit-base-x",w="data-hf-edit-base-y",m="data-hf-edit-original-translate",k=e=>{let t=parseFloat(e??"");return Number.isFinite(t)?t:0},C=e=>{let t=[],o=0,r="";for(let s of e.trim())s==="("&&(o+=1),s===")"&&(o=Math.max(0,o-1)),/\\s/.test(s)&&o===0?(r&&t.push(r),r=""):r+=s;return r&&t.push(r),t},$=/^-?(?:\\d+(?:\\.\\d+)?|\\.\\d+)px$/,E=(e,t)=>$.test(e)&&$.test(t)?`${parseFloat(e)+parseFloat(t)}px`:`calc(${e} + ${t})`,O=(e,t,o)=>{if(!e||e==="none")return`${t} ${o}`;let[r,s,a]=C(e);if(r===void 0)return`${t} ${o}`;if(s===void 0)return`${E(r,t)} ${o}`;let i=a===void 0?"":` ${a}`;return`${E(r,t)} ${E(s,o)}${i}`},j=e=>{try{e.ownerDocument.defaultView?.gsap?.getProperty?.(e,"x")}catch{}},V=e=>{let t=e.style.getPropertyValue("translate").trim();if(t)return t==="none"?"":t;try{let o=e.ownerDocument.defaultView,r=o?o.getComputedStyle(e).getPropertyValue("translate").trim():"";return r==="none"?"":r}catch{return""}},b=new WeakMap;function B(e,t){let o=b.get(e);if(!t?.force&&o!==void 0&&e.style.getPropertyValue("translate")!==o){R("position_edit_fold_skipped",{hfId:e.getAttribute("data-hf-id")});return}let r=k(e.getAttribute("data-x"))-k(e.getAttribute(y)),s=k(e.getAttribute("data-y"))-k(e.getAttribute(w));e.hasAttribute(m)||e.setAttribute(m,V(e)),o===void 0&&j(e);let a=e.getAttribute(m)??"",i=O(a,`${r}px`,`${s}px`);e.style.setProperty("translate",i),b.set(e,e.style.getPropertyValue("translate"))}function h(e,t){let o=e.querySelectorAll(`[${m}]:not([${y}]):not([${w}])`);for(let a=0;a<o.length;a++){let i=o[a];if(i===void 0||!_(i))continue;let g=i.getAttribute(m)??"";g===""?i.style.removeProperty("translate"):i.style.setProperty("translate",g),i.removeAttribute(m),b.delete(i)}let r=e.querySelectorAll(`[${y}], [${w}]`),s=0;for(let a=0;a<r.length;a++){let i=r[a];i===void 0||!_(i)||(B(i,t),s+=1)}return s}var L="__hfPositionEditsSeekReapplyWrapped",D=new WeakSet,F=new WeakMap,H=new WeakMap;function W(e){let t=e,o=()=>{try{h(t.document)}catch{}},r=n=>typeof n=="function"&&(D.has(n)||!!n[L]),s=n=>{D.add(n);try{Object.defineProperty(n,L,{value:!0})}catch{}},a=n=>{if(typeof n!="function"||r(n))return n;let l=function(...u){let d=n.apply(this,u);return o(),d};return s(l),l},i=(n,l)=>{let u=F.get(n);if(u?.has(l))return!0;let d=Object.getOwnPropertyDescriptor(n,l);if(d?.configurable===!1){let c=n[l];return typeof c=="function"&&(n[l]=a(c),o()),!1}let p=n[l],f=d?.set;return Object.defineProperty(n,l,{configurable:!0,enumerable:d?.enumerable??!0,get:()=>p,set:c=>{p=a(c),f?.call(n,c)}}),p=a(p),u??(u=new Set),u.add(l),F.set(n,u),o(),!0},g=(n,l)=>{let u=H.get(t),d=Object.getOwnPropertyDescriptor(t,n);if(!u?.has(n)){if(d?.configurable===!1){let c=t[n];return c?i(c,l):!1}let f=t[n];Object.defineProperty(t,n,{configurable:!0,enumerable:d?.enumerable??!0,get:()=>f,set:c=>{f=c,f&&i(f,l)}}),u??(u=new Set),u.add(n),H.set(t,u)}let p=t[n];return p?i(p,l):!1},S=()=>{let n=g("__hf","seek"),l=g("__player","renderSeek");return n&&l};if(S())return;let v=120,A=t.setInterval(()=>{if(S()){t.clearInterval(A);return}v-=1,v<=0&&t.clearInterval(A)},50)}function I(){document.querySelector(`[${y}], [${w}]`)&&(h(document),W(window))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",I,{once:!0}):I();})();\n';
'"use strict";(()=>{function x(){return globalThis}function T(e,t){if(typeof window>"u")return;let o=x(),r=o.__hf?.onSwallowed;if(r)try{r({label:e,error:t})}catch(s){}(o.__hfDebug||o.__HYPERFRAMES_DEBUG)&&console.debug(`[hyperframes] ${e} swallowed:`,t)}var P=null;function R(e,t){if(P)try{P({source:"hf-preview",type:"analytics",event:e,properties:t??{}})}catch(o){T("runtime.analytics.site1",o)}}var I="http://www.w3.org/1999/xhtml";function N(e){return e!=null&&e.nodeType===1}function _(e){return N(e)&&(e.namespaceURI===I||e.namespaceURI==="http://www.w3.org/2000/svg")}var y="data-hf-edit-base-x",w="data-hf-edit-base-y",m="data-hf-edit-original-translate",k=e=>{let t=parseFloat(e??"");return Number.isFinite(t)?t:0},C=e=>{let t=[],o=0,r="";for(let s of e.trim())s==="("&&(o+=1),s===")"&&(o=Math.max(0,o-1)),/\\s/.test(s)&&o===0?(r&&t.push(r),r=""):r+=s;return r&&t.push(r),t},$=/^-?(?:\\d+(?:\\.\\d+)?|\\.\\d+)px$/,E=(e,t)=>$.test(e)&&$.test(t)?`${parseFloat(e)+parseFloat(t)}px`:`calc(${e} + ${t})`,O=(e,t,o)=>{if(!e||e==="none")return`${t} ${o}`;let[r,s,a]=C(e);if(r===void 0)return`${t} ${o}`;if(s===void 0)return`${E(r,t)} ${o}`;let i=a===void 0?"":` ${a}`;return`${E(r,t)} ${E(s,o)}${i}`},j=e=>{try{e.ownerDocument.defaultView?.gsap?.getProperty?.(e,"x")}catch{}},V=e=>{let t=e.style.getPropertyValue("translate").trim();if(t)return t==="none"?"":t;try{let o=e.ownerDocument.defaultView,r=o?o.getComputedStyle(e).getPropertyValue("translate").trim():"";return r==="none"?"":r}catch{return""}},b=new WeakMap;function B(e,t){let o=b.get(e);if(!t?.force&&o!==void 0&&e.style.getPropertyValue("translate")!==o){R("position_edit_fold_skipped",{hfId:e.getAttribute("data-hf-id")});return}let r=k(e.getAttribute("data-x"))-k(e.getAttribute(y)),s=k(e.getAttribute("data-y"))-k(e.getAttribute(w));e.hasAttribute(m)||e.setAttribute(m,V(e)),o===void 0&&j(e);let a=e.getAttribute(m)??"",i=O(a,`${r}px`,`${s}px`);e.style.setProperty("translate",i),b.set(e,e.style.getPropertyValue("translate"))}function S(e,t){let o=e.querySelectorAll(`[${m}]:not([${y}]):not([${w}])`);for(let a=0;a<o.length;a++){let i=o[a];if(i===void 0||!_(i))continue;let g=i.getAttribute(m)??"";g===""?i.style.removeProperty("translate"):i.style.setProperty("translate",g),i.removeAttribute(m),b.delete(i)}let r=e.querySelectorAll(`[${y}], [${w}]`),s=0;for(let a=0;a<r.length;a++){let i=r[a];i===void 0||!_(i)||(B(i,t),s+=1)}return s}var L="__hfPositionEditsSeekReapplyWrapped",D=new WeakSet,F=new WeakMap,H=new WeakMap;function M(e){let t=e,o=()=>{try{S(t.document)}catch{}},r=n=>typeof n=="function"&&(D.has(n)||!!n[L]),s=n=>{D.add(n);try{Object.defineProperty(n,L,{value:!0})}catch{}},a=n=>{if(typeof n!="function"||r(n))return n;let l=function(...u){let d=n.apply(this,u);return o(),d};return s(l),l},i=(n,l)=>{let u=F.get(n);if(u?.has(l))return!0;let d=Object.getOwnPropertyDescriptor(n,l);if(d?.configurable===!1){let c=n[l];return typeof c=="function"&&(n[l]=a(c),o()),!1}let p=n[l],f=d?.set;return Object.defineProperty(n,l,{configurable:!0,enumerable:d?.enumerable??!0,get:()=>p,set:c=>{p=a(c),f?.call(n,c)}}),p=a(p),u??(u=new Set),u.add(l),F.set(n,u),o(),!0},g=(n,l)=>{let u=H.get(t),d=Object.getOwnPropertyDescriptor(t,n);if(!u?.has(n)){if(d?.configurable===!1){let c=t[n];return c?i(c,l):!1}let f=t[n];Object.defineProperty(t,n,{configurable:!0,enumerable:d?.enumerable??!0,get:()=>f,set:c=>{f=c,f&&i(f,l)}}),u??(u=new Set),u.add(n),H.set(t,u)}let p=t[n];return p?i(p,l):!1},h=()=>{let n=g("__hf","seek"),l=g("__player","renderSeek");return n&&l};if(h())return;let v=120,A=t.setInterval(()=>{if(h()){t.clearInterval(A);return}v-=1,v<=0&&t.clearInterval(A)},50)}function W(){document.querySelector(`[${y}], [${w}]`)&&(S(document),M(window))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",W,{once:!0}):W();})();\n';
/** Returns the pre-built position-edits render IIFE as a string constant. */
export function getPositionEditsRenderScript(): string {
+5
View File
@@ -76,6 +76,11 @@ export function isAudioElement(value: unknown): value is HTMLAudioElement {
return isHtmlTag(value, "audio");
}
/** `<source>`, from any realm. Mirrors `instanceof HTMLSourceElement`. */
export function isSourceElement(value: unknown): value is HTMLSourceElement {
return isHtmlTag(value, "source");
}
/** `<video>` or `<audio>`, from any realm. Mirrors `instanceof HTMLMediaElement`. */
export function isMediaElement(value: unknown): value is HTMLMediaElement {
return isVideoElement(value) || isAudioElement(value);
+2
View File
@@ -167,6 +167,7 @@ import {
} from "./captureMotionBlur";
import { TransportClock } from "./clock";
import { WebAudioTransport } from "./webAudioTransport";
import { watchAudioLoadErrors } from "./mediaLoadFailure";
import {
classifyWebAudioMediaRoute,
isRouteSelectionSettled,
@@ -3601,6 +3602,7 @@ export function initSandboxRuntimeModular(): void {
runtimeCleanupCallbacks.push(() => {
document.removeEventListener("play", onMediaPlayWakeTransport, true);
});
runtimeCleanupCallbacks.push(watchAudioLoadErrors(document));
let lastVisibilityPassSeconds: number | null = null;
const syncMediaForCurrentState = (timingRevision?: number) => {
@@ -0,0 +1,105 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { postRuntimeMessage } from "./bridge";
import { watchAudioLoadErrors } from "./mediaLoadFailure";
import { RUNTIME_MEDIA_LOAD_FAILED } from "./protocol";
import { WebAudioTransport } from "./webAudioTransport";
vi.mock("./bridge", () => ({ postRuntimeMessage: vi.fn() }));
const posted = vi.mocked(postRuntimeMessage);
const failures = () =>
posted.mock.calls
.map(([message]) => message)
.filter((m) => m.type === "diagnostic" && m.code === RUNTIME_MEDIA_LOAD_FAILED)
.map((m) => (m as { details: Record<string, unknown> }).details);
function audio(src: string, id = "music"): HTMLAudioElement {
const el = document.createElement("audio");
el.id = id;
el.setAttribute("src", src);
return el;
}
function transport(decode: () => Promise<AudioBuffer>) {
const t = new WebAudioTransport();
(t as unknown as { _ctx: unknown })._ctx = { state: "running", decodeAudioData: vi.fn(decode) };
return t;
}
beforeEach(() => {
vi.spyOn(console, "info").mockImplementation(() => {});
});
afterEach(() => {
posted.mockReset();
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
describe("a preview media load that fails is reported, not swallowed", () => {
it("a fetch the server refuses says its status, once however often playback retries", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => ({ ok: false, status: 404 })),
);
const t = transport(async () => ({}) as AudioBuffer);
const el = audio("assets/Don't (mix).mp3");
await t.decodeAudioElement(el);
await t.decodeAudioElement(el);
expect(failures()).toEqual([
expect.objectContaining({
elementId: "music",
kind: "audio",
stage: "fetch",
code: 404,
extension: "mp3",
}),
]);
});
it("bytes the browser cannot decode say decode and how many bytes came", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => ({ ok: true, arrayBuffer: async () => new ArrayBuffer(8) })),
);
const t = transport(async () => {
throw new Error("unsupported codec");
});
await t.decodeAudioElement(audio("assets/song.wav"));
expect(failures()).toEqual([
expect.objectContaining({ stage: "decode", code: null, bytes: 8, extension: "wav" }),
]);
});
it("an audio element that fails to load says its error code; a video's is left to the proxy", () => {
const stop = watchAudioLoadErrors(document);
const el = audio("assets/gone.mp3", "gone");
Object.defineProperty(el, "error", { value: { code: 4 } });
const video = document.createElement("video");
document.body.append(el, video);
el.dispatchEvent(new Event("error"));
video.dispatchEvent(new Event("error"));
stop();
el.dispatchEvent(new Event("error"));
expect(failures()).toEqual([
expect.objectContaining({ elementId: "gone", stage: "element", code: 4 }),
]);
el.remove();
video.remove();
});
it("an audio element fed by <source> children reports when a source fails to load", () => {
const stop = watchAudioLoadErrors(document);
const el = document.createElement("audio");
el.id = "sourced";
const source = document.createElement("source");
source.setAttribute("src", "assets/gone.ogg");
el.append(source);
document.body.append(el);
source.dispatchEvent(new Event("error"));
stop();
expect(failures()).toEqual([
expect.objectContaining({ elementId: "sourced", stage: "element" }),
]);
el.remove();
});
});
@@ -0,0 +1,65 @@
import { postRuntimeMessage } from "./bridge";
import { isAudioElement, isSourceElement } from "./domRealm";
import { isRenderMode } from "./mediaProxy";
import { RUNTIME_MEDIA_LOAD_FAILED, type MediaLoadStage } from "./protocol";
import type { RuntimeJson } from "./types";
const reported = new WeakMap<HTMLMediaElement, Set<MediaLoadStage>>();
/** The file's extension, lower-cased, from the URL path; never its name. */
function extensionOf(el: HTMLMediaElement): string | null {
const src = el.currentSrc || el.getAttribute("src") || "";
try {
const match = /\.([a-z0-9]{1,5})$/i.exec(new URL(src, document.baseURI).pathname);
return match?.[1]?.toLowerCase() ?? null;
} catch {
return null;
}
}
/** Tells the host once per element and stage that its media failed to load. `code` is the HTTP status of a fetch
* that answered or the element's MediaError code; `bytes` the size fetched before a decode failed. */
export function reportMediaLoadFailure(
el: HTMLMediaElement,
stage: MediaLoadStage,
code: number | null,
bytes: number | null = null,
): void {
if (isRenderMode(el)) return;
const stages = reported.get(el) ?? new Set<MediaLoadStage>();
if (stages.has(stage)) return;
stages.add(stage);
reported.set(el, stages);
const asset = el.getAttribute("src") ?? "";
const details: Record<string, RuntimeJson> = {
asset,
elementId: el.id || null,
kind: el.tagName.toLowerCase(),
stage,
code,
bytes,
extension: extensionOf(el),
};
postRuntimeMessage({
source: "hf-preview",
type: "diagnostic",
code: RUNTIME_MEDIA_LOAD_FAILED,
details,
});
console.info(
`[hyperframes] ${RUNTIME_MEDIA_LOAD_FAILED}: "${asset}" failed at ${stage}${code === null ? "" : ` (${code})`}.`,
);
}
/** Reports each `<audio>` that fails to load; returns the cleanup. `error` does not bubble, so this listens in capture.
* A `<source>` child gets the error instead of its `<audio>`.
* Video is left out: mediaProxy answers a video's load error with a lighter copy, so it is not yet a failure. */
export function watchAudioLoadErrors(doc: Document): () => void {
const onError = (event: Event) => {
const target = event.target;
const el = isSourceElement(target) ? target.parentElement : target;
if (isAudioElement(el)) reportMediaLoadFailure(el, "element", el.error?.code ?? null);
};
doc.addEventListener("error", onError, true);
return () => doc.removeEventListener("error", onError, true);
}
+32
View File
@@ -2,6 +2,8 @@ export const RUNTIME_PROTOCOL_VERSION = 1 as const;
export const RUNTIME_FILLER = "hf-runtime-filler";
export const RUNTIME_MEDIA_LOAD_FAILED = "runtime_media_load_failed";
export const RUNTIME_PROTOCOL_CAPABILITIES = [
"seconds-time",
"rational-fps",
@@ -106,3 +108,33 @@ export function inspectRuntimeProtocol(value: unknown, legacyFps = 30): RuntimeP
metadata: message as RuntimeProtocolV1,
};
}
const STAGES = ["fetch", "decode", "element"] as const;
export type MediaLoadStage = (typeof STAGES)[number];
export interface MediaLoadFailure {
elementId: string | null;
kind: string;
stage: MediaLoadStage;
code: number | null;
bytes: number | null;
extension: string | null;
}
const numberOrNull = (n: unknown) => (typeof n === "number" ? n : null);
const stringOrNull = (s: unknown) => (typeof s === "string" ? s : null);
export function parseMediaLoadFailure(value: unknown): MediaLoadFailure | null {
if (typeof value !== "object" || value === null) return null;
const { elementId, kind, stage, code, bytes, extension } = value as Record<string, unknown>;
const known = STAGES.find((s) => s === stage);
if (!known || typeof kind !== "string") return null;
return {
elementId: stringOrNull(elementId),
kind,
stage: known,
code: numberOrNull(code),
bytes: numberOrNull(bytes),
extension: stringOrNull(extension),
};
}
@@ -1776,6 +1776,7 @@ describe("WebAudioTransport", () => {
}
const el = (src: string) =>
({
tagName: "AUDIO",
getAttribute: (name: string) => (name === "src" ? src : null),
currentSrc: "",
}) as unknown as HTMLMediaElement;
@@ -14,6 +14,7 @@ import {
resolveMasterBusElement,
} from "../audioGroups.js";
import { swallow } from "./diagnostics";
import { reportMediaLoadFailure } from "./mediaLoadFailure";
import { createLevelTap, type LevelTap, type StereoLevel } from "./levelTap.js";
import { clampAudioGain } from "../audioGain.js";
import { getDebugSurface } from "./globals.js";
@@ -355,12 +356,12 @@ export class WebAudioTransport {
if (!src) return Promise.resolve(null);
const pending = this._decoding.get(src);
if (pending) return pending;
const decoding = this.fetchAndDecode(src).finally(() => this._decoding.delete(src));
const decoding = this.fetchAndDecode(src, el).finally(() => this._decoding.delete(src));
this._decoding.set(src, decoding);
return decoding;
}
private async fetchAndDecode(src: string): Promise<AudioBuffer | null> {
private async fetchAndDecode(src: string, el: HTMLMediaElement): Promise<AudioBuffer | null> {
if (this._bufferCache.has(src)) return this._bufferCache.get(src)!;
if (this._failedSrcs.has(src)) return null;
if (!this._ctx) return null;
@@ -378,11 +379,13 @@ export class WebAudioTransport {
const response = await fetch(src, { cache: "no-store" });
if (!response.ok) {
swallow("webAudioTransport.fetch", new Error(`${response.status} ${src}`));
reportMediaLoadFailure(el, "fetch", response.status);
return null;
}
arrayBuffer = await response.arrayBuffer();
} catch (err) {
swallow("webAudioTransport.fetch", err);
reportMediaLoadFailure(el, "fetch", null);
return null;
}
@@ -396,6 +399,7 @@ export class WebAudioTransport {
} catch (err) {
this._failedSrcs.add(src);
swallow("webAudioTransport.decode", err);
reportMediaLoadFailure(el, "decode", null, arrayBuffer.byteLength);
return null;
}
}
@@ -128,6 +128,46 @@ describe("preview quality messages", () => {
expect(usePlayerStore.getState().previewFacts).toBeNull();
});
it("keeps each failed media load the preview reports, and ignores other diagnostics", () => {
const details = {
asset: "assets/x.mp3",
elementId: "music",
kind: "audio",
stage: "decode",
code: null,
bytes: 8,
extension: "mp3",
};
routeMessage({
source: "hf-preview",
type: "diagnostic",
code: "runtime_media_load_failed",
details,
});
expect(usePlayerStore.getState().mediaLoadFailure).toEqual({
elementId: "music",
kind: "audio",
stage: "decode",
code: null,
bytes: 8,
extension: "mp3",
});
const kept = usePlayerStore.getState().mediaLoadFailure;
routeMessage({
source: "hf-preview",
type: "diagnostic",
code: "runtime_web_audio_bypass",
details,
});
routeMessage({
source: "hf-preview",
type: "diagnostic",
code: "runtime_media_load_failed",
details: { ...details, stage: "?" },
});
expect(usePlayerStore.getState().mediaLoadFailure).toBe(kept);
});
it("sends the chosen quality to a preview that has just loaded", () => {
usePlayerStore.getState().setPreviewQuality(0.25);
const postMessage = vi.fn();
@@ -13,6 +13,10 @@ import { usePlayerStore, type TimelineElement } from "../store/playerStore";
import type { ClipManifestClip, IframeWindow, PlaybackAdapter } from "../lib/playbackTypes";
import { hasTimelinePerformanceFixtureLease } from "../lib/timelinePerformanceFixture";
import { acceptStudioRuntimeMessage, postRuntimeControlMessage } from "../lib/runtimeProtocol";
import {
parseMediaLoadFailure,
RUNTIME_MEDIA_LOAD_FAILED,
} from "@hyperframes/core/runtime/protocol";
import {
parsePlaybackHealth,
parsePreviewCopies,
@@ -38,6 +42,8 @@ interface PreviewMessage {
copies?: unknown;
health?: unknown;
facts?: unknown;
code?: unknown;
details?: unknown;
}
export interface PreviewMessageRouterDeps {
@@ -136,6 +142,12 @@ function applyStoreReport(data: PreviewMessage): boolean {
case "preview-facts":
store.setPreviewFacts(parsePreviewFacts(data.facts));
return true;
case "diagnostic": {
if (data.code !== RUNTIME_MEDIA_LOAD_FAILED) return false;
const failure = parseMediaLoadFailure(data.details);
if (failure) store.setMediaLoadFailure(failure);
return true;
}
default:
return false;
}
@@ -1,5 +1,6 @@
import type { StoreApi } from "zustand";
import { isPreviewQuality, type PreviewQuality } from "@hyperframes/core/preview-quality";
import type { MediaLoadFailure } from "@hyperframes/core/runtime/protocol";
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
/** The lighter copies of heavy videos the preview wants at its current quality. */
@@ -53,6 +54,8 @@ export interface PreviewQualitySlice {
setPlaybackHealth: (health: PlaybackHealth | null) => void;
previewFacts: PreviewFacts | null;
setPreviewFacts: (facts: PreviewFacts | null) => void;
mediaLoadFailure: MediaLoadFailure | null;
setMediaLoadFailure: (failure: MediaLoadFailure | null) => void;
/** Bumped by requestPreviewFacts; the preview owner forwards each bump and the answer lands in previewFacts. */
previewFactsRequests: number;
requestPreviewFacts: () => void;
@@ -87,6 +90,8 @@ export function createPreviewQualitySlice(
setPlaybackHealth: (playbackHealth) => set({ playbackHealth }),
previewFacts: null,
setPreviewFacts: (previewFacts) => set({ previewFacts }),
mediaLoadFailure: null,
setMediaLoadFailure: (mediaLoadFailure) => set({ mediaLoadFailure }),
previewFactsRequests: 0,
requestPreviewFacts: () =>
set((state) => ({ previewFactsRequests: state.previewFactsRequests + 1 })),