diff --git a/packages/studio/src/components/editor/propertyPanelFlatMediaSection.test.tsx b/packages/studio/src/components/editor/propertyPanelFlatMediaSection.test.tsx
index 958d265da..d77472a88 100644
--- a/packages/studio/src/components/editor/propertyPanelFlatMediaSection.test.tsx
+++ b/packages/studio/src/components/editor/propertyPanelFlatMediaSection.test.tsx
@@ -244,3 +244,123 @@ describe("FlatMediaSection — volume/rate/media-start", () => {
act(() => root.unmount());
});
});
+
+describe("FlatMediaSection — loop/muted/has-audio", () => {
+ it("toggles loop via onSetHtmlAttribute and shows has-audio-track for video", () => {
+ const onSetHtmlAttribute = vi.fn();
+ const onSetAttribute = vi.fn();
+ const element = makeVideoElement({ dataAttributes: { "has-audio": "true" } });
+ const host = document.createElement("div");
+ document.body.append(host);
+ const root = createRoot(host);
+ act(() => {
+ root.render(
+ ,
+ );
+ });
+ const loopToggle = host.querySelector(
+ '[data-flat-toggle="true"][aria-label="Loop"]',
+ );
+ act(() => loopToggle?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
+ expect(onSetHtmlAttribute).toHaveBeenCalledWith("loop", "true");
+
+ const hasAudioToggle = host.querySelector(
+ '[data-flat-toggle="true"][aria-label="Has audio track"]',
+ );
+ expect(hasAudioToggle?.getAttribute("aria-checked")).toBe("true");
+ act(() => root.unmount());
+ });
+
+ it("toggles muted via onSetHtmlAttribute", () => {
+ const onSetHtmlAttribute = vi.fn();
+ const onSetAttribute = vi.fn();
+ const element = makeVideoElement();
+ const host = document.createElement("div");
+ document.body.append(host);
+ const root = createRoot(host);
+ act(() => {
+ root.render(
+ ,
+ );
+ });
+ const mutedToggle = host.querySelector(
+ '[data-flat-toggle="true"][aria-label="Muted"]',
+ );
+ expect(mutedToggle?.getAttribute("aria-checked")).toBe("false");
+ act(() => mutedToggle?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
+ expect(onSetHtmlAttribute).toHaveBeenCalledWith("muted", "true");
+ act(() => root.unmount());
+ });
+
+ it("enables has-audio-track and clears muted on click", () => {
+ const onSetHtmlAttribute = vi.fn();
+ const onSetAttribute = vi.fn();
+ const element = makeVideoElement();
+ const host = document.createElement("div");
+ document.body.append(host);
+ const root = createRoot(host);
+ act(() => {
+ root.render(
+ ,
+ );
+ });
+ const hasAudioToggle = host.querySelector(
+ '[data-flat-toggle="true"][aria-label="Has audio track"]',
+ );
+ expect(hasAudioToggle?.getAttribute("aria-checked")).toBe("false");
+ act(() => hasAudioToggle?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
+ expect(onSetAttribute).toHaveBeenCalledWith("has-audio", "true");
+ expect(onSetHtmlAttribute).toHaveBeenCalledWith("muted", null);
+ act(() => root.unmount());
+ });
+
+ it("disables has-audio-track and sets muted on click", () => {
+ const onSetHtmlAttribute = vi.fn();
+ const onSetAttribute = vi.fn();
+ const element = makeVideoElement({ dataAttributes: { "has-audio": "true" } });
+ const host = document.createElement("div");
+ document.body.append(host);
+ const root = createRoot(host);
+ act(() => {
+ root.render(
+ ,
+ );
+ });
+ const hasAudioToggle = host.querySelector(
+ '[data-flat-toggle="true"][aria-label="Has audio track"]',
+ );
+ expect(hasAudioToggle?.getAttribute("aria-checked")).toBe("true");
+ act(() => hasAudioToggle?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
+ expect(onSetAttribute).toHaveBeenCalledWith("has-audio", "");
+ expect(onSetHtmlAttribute).toHaveBeenCalledWith("muted", "true");
+ act(() => root.unmount());
+ });
+});
diff --git a/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx b/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx
index 9949489e8..37f7a0c71 100644
--- a/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx
+++ b/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx
@@ -56,6 +56,9 @@ export function FlatMediaSection({
(el as HTMLMediaElement).duration ||
0;
const mediaStartMax = Math.max(30, Math.ceil(sourceDuration || mediaStart + 10));
+ const hasLoop = el.hasAttribute("loop");
+ const hasMuted = el.hasAttribute("muted");
+ const hasAudio = element.dataAttributes["has-audio"] === "true";
const srcAttr = el.getAttribute("src") ?? "";
const [copied, setCopied] = useState(false);
@@ -218,6 +221,31 @@ export function FlatMediaSection({
displayValue={formatTimingValue(mediaStart)}
onCommit={(next) => void onSetAttribute("media-start", (next / 100).toFixed(2))}
/>
+ void onSetHtmlAttribute("loop", next ? "true" : null)}
+ />
+ void onSetHtmlAttribute("muted", next ? "true" : null)}
+ />
+ {isVideo && (
+ {
+ if (next) {
+ void onSetAttribute("has-audio", "true");
+ void onSetHtmlAttribute("muted", null);
+ } else {
+ void onSetAttribute("has-audio", "");
+ void onSetHtmlAttribute("muted", "true");
+ }
+ }}
+ />
+ )}
>
)}