fix(player): preserve iframe media attributes for runtime sync (#291)

## Summary

- `_setupParentMedia()` (added in #266) was stripping `data-start`, `data-duration`, and `src` from audio/video elements inside the composition iframe
- The runtime's `syncRuntimeMedia` queries `audio[data-start]` to find media clips — removing these attributes made the runtime unable to find, sync, or play audio
- Result: silent audio in studio preview and any context where `__player.play()` is called directly (not through the web component)

## Fix

- Keep all iframe media attributes intact so the runtime can track time position and manage playback
- When parent-frame media `play()` succeeds (mobile use case), mute the iframe copies via `volume = 0` to prevent double audio
- On desktop and in the studio (which calls `__player.play()` directly), the runtime's own media sync handles playback normally

## Test plan

- [x] 21 player unit tests pass
- [x] Verified with John Wu's slideshow project: audio element preserves `data-start`, `data-duration`, `src` after runtime init
- [x] Verified runtime `syncRuntimeMedia` finds and plays audio (currentTime advances in sync with timeline)
- [x] Build passes (lint, format, typecheck)
This commit is contained in:
Miguel Ángel
2026-04-16 06:20:28 +02:00
committed by GitHub
parent 87ce26de8a
commit a6ff9e2d9f
+31 -6
View File
@@ -489,7 +489,30 @@ class HyperframesPlayer extends HTMLElement {
private _playParentMedia() {
for (const m of this._parentMedia) {
if (m.el.src) m.el.play().catch(() => {});
if (m.el.src) {
m.el
.play()
.then(() => {
// Parent play succeeded — mute the iframe copy to prevent double audio.
// This runs asynchronously, so the runtime may briefly play both copies,
// but the overlap is inaudible (same audio at the same position).
this._muteIframeMedia();
})
.catch(() => {});
}
}
}
private _muteIframeMedia() {
try {
const doc = this.iframe.contentDocument;
if (!doc) return;
const mediaEls = doc.querySelectorAll<HTMLMediaElement>(
"audio[data-start], video[data-start]",
);
for (const el of mediaEls) el.volume = 0;
} catch {
// cross-origin
}
}
@@ -549,11 +572,13 @@ class HyperframesPlayer extends HTMLElement {
this._createParentMedia(src, tag, start, duration);
// Disable the iframe element so the runtime ignores it
iframeEl.removeAttribute("src");
iframeEl.removeAttribute("data-start");
iframeEl.removeAttribute("data-duration");
iframeEl.querySelectorAll("source").forEach((s) => s.remove());
// DO NOT strip data-start, data-duration, or src from the iframe elements.
// The runtime's syncRuntimeMedia queries audio[data-start] — removing these
// attributes makes the runtime unable to find, sync, or play media clips.
// The iframe copies remain fully functional for the runtime. On mobile,
// parent copies provide the audible output via the component's play() method.
// On desktop and in the studio (which calls __player.play() directly),
// the runtime's own media sync handles playback.
}
} catch {
// Cross-origin iframe — can't access DOM, fall back to iframe media