diff --git a/packages/producer/src/services/htmlCompiler.ts b/packages/producer/src/services/htmlCompiler.ts
index 39a10a974..05a8ce3c6 100644
--- a/packages/producer/src/services/htmlCompiler.ts
+++ b/packages/producer/src/services/htmlCompiler.ts
@@ -1822,6 +1822,8 @@ export interface BrowserMediaElement {
loop: boolean;
hasAudio: boolean;
volume: number;
+ /** The `muted` attribute/property. Preview silences muted media; the mix must too. */
+ muted: boolean;
}
export interface BrowserAudioVolumeAutomation {
@@ -1842,6 +1844,7 @@ export async function discoverMediaFromBrowser(page: Page): Promise {
+ it("drops muted audio from the mix and clears hasAudio on muted video", () => {
+ const composition = {
+ videos: [
+ { id: "clip", hasAudio: true },
+ { id: "other", hasAudio: true },
+ ],
+ audios: [{ id: "bgm" }, { id: "voice" }],
+ };
+ const ids = new Set(["bgm", "voice"]);
+ pruneMutedBrowserMedia(
+ composition,
+ [
+ { id: "clip", tagName: "video", muted: true },
+ { id: "voice", tagName: "audio", muted: true },
+ { id: "bgm", tagName: "audio", muted: false },
+ ],
+ ids,
+ );
+ expect(composition.audios.map((a) => a.id)).toEqual(["bgm"]);
+ expect(ids.has("voice")).toBe(false);
+ expect(composition.videos.find((v) => v.id === "clip")?.hasAudio).toBe(false);
+ expect(composition.videos.find((v) => v.id === "other")?.hasAudio).toBe(true);
+ });
+
+ it("is a no-op when nothing is muted", () => {
+ const composition = { videos: [{ id: "v", hasAudio: true }], audios: [{ id: "a" }] };
+ pruneMutedBrowserMedia(composition, [
+ { id: "v", tagName: "video", muted: false },
+ { id: "a", tagName: "audio" },
+ ]);
+ expect(composition.audios.length).toBe(1);
+ expect(composition.videos[0]?.hasAudio).toBe(true);
+ });
+});
diff --git a/packages/producer/src/services/render/stages/probeStage.ts b/packages/producer/src/services/render/stages/probeStage.ts
index 297543b8f..273c96013 100644
--- a/packages/producer/src/services/render/stages/probeStage.ts
+++ b/packages/producer/src/services/render/stages/probeStage.ts
@@ -375,8 +375,11 @@ export async function runProbeStage(input: ProbeStageInput): Promise v.id));
const existingAudioIds = new Set(composition.audios.map((a) => a.id));
+ pruneMutedBrowserMedia(composition, browserMedia, existingAudioIds);
+
for (const el of browserMedia) {
if (!el.src || el.src === "about:blank") continue;
+ if (el.muted && el.tagName === "audio") continue;
// Convert absolute localhost URLs back to relative paths
let src = el.src;
@@ -411,7 +414,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise,
+): void {
+ for (const el of browserMedia) {
+ if (!el.muted) continue;
+ if (el.tagName === "video") {
+ const existing = composition.videos.find((v) => v.id === el.id);
+ if (existing) existing.hasAudio = false;
+ } else {
+ const idx = composition.audios.findIndex((a) => a.id === el.id);
+ if (idx >= 0) composition.audios.splice(idx, 1);
+ existingAudioIds?.delete(el.id);
+ }
+ }
+}