demo · v133

Cross-origin iframe routing — without postMessage

The whole reason setPreferredSinkId() shipped: a top-level page can route audio for every cross-origin iframe inside it without negotiating per-frame. Pick a device on the parent — the embedded player's output follows.

The embedded player is loaded with sandbox="allow-scripts" and deliberately omits allow-same-origin. That gives the child frame an opaque origin, so the parent cannot reach into its DOM, but the top-level setPreferredSinkId() preference still applies to audio renderers created in the frame tree.

embedded player (sandboxed opaque origin)

before this shipped

Each cross-origin iframe was an island. To honour a user-picked output device, the parent had to postMessage the device ID into every frame, and every frame had to listen for it and call HTMLMediaElement.setSinkId on each of its audio elements. SDKs (call apps, podcast players, ad widgets) had to publish a documented message protocol for this. setPreferredSinkId on the top frame replaces all of that — the chosen device is the new default sink for every renderer in the frame tree, including cross-origin children, until the page picks another.

// Top-level page:
await navigator.mediaDevices.setPreferredSinkId(deviceId);

// Cross-origin iframe inside it: every <audio> / Web Audio context
// it creates now plays through the chosen device by default.
// No postMessage handshake. No per-frame setSinkId.

see also