Ask iOS for the playback audio session on a page with a video
CI / Typecheck, test, build (pull_request) Successful in 12s

A TikTok post played perfectly and said nothing on an iPhone. The file was
not the problem: every rendition TikTok offers for it carries a full AAC
track, and the page as served measures loud audio in WebKit and Chromium
alike, straight through the /m/ proxy.

iOS is the problem. A video playing inline gets the "ambient" audio session,
which the Ring/Silent switch mutes; only going fullscreen gets the sound
back. Claiming "playback" says what is true of this page -- the sound is the
point, not decoration -- and the switch stops applying.

Declared only where there is a video, so an ordinary text post never claims
it, and the session activates when something plays rather than on load, so
it interrupts nothing on a page nobody presses play on.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01B3RUMiXa6eAW3okw9nB9NF
This commit is contained in:
2026-09-02 21:06:12 -03:00
co-authored by Claude Opus 5
parent d9fa2f5768
commit 4c78ce8a27
3 changed files with 23 additions and 0 deletions
+5
View File
@@ -100,6 +100,11 @@ Things worth knowing before editing:
`<video>` with no data has a natural size of 300x150, and WebKit sizes it from that — `<video>` with no data has a natural size of 300x150, and WebKit sizes it from that —
so one with no poster of its own gets an empty SVG of the right shape as a stand-in, so one with no poster of its own gets an empty SVG of the right shape as a stand-in,
without which a portrait video sits in a squat landscape box until you press play. without which a portrait video sits in a squat landscape box until you press play.
- **A page with a video claims the `playback` audio session** (`public/app.js`). iOS
hands inline video the ambient session, which the Ring/Silent switch mutes, so
without this the video plays and says nothing until it goes fullscreen — and the
report that arrives is "this one platform has no sound". The session only activates
once something plays, so declaring it costs nothing on a page nobody presses play on.
- **Never a bare error page.** A failed resolve renders a card carrying the platform, the - **Never a bare error page.** A failed resolve renders a card carrying the platform, the
original URL and the copy button. A broken adapter must still leave the link one tap original URL and the copy button. A broken adapter must still leave the link one tap
away. away.
+4
View File
@@ -171,6 +171,10 @@ The one exception is Bluesky video, which is an HLS playlist — proxying it wou
rewriting the manifest and every segment, so it is linked directly. Safari plays HLS rewriting the manifest and every segment, so it is linked directly. Safari plays HLS
natively; other browsers show a note. natively; other browsers show a note.
A page carrying a video asks iOS for the playback audio session. Without it a video
playing inline is treated as ambience and the Ring/Silent switch mutes it, so the post
plays perfectly and says nothing unless you go fullscreen.
Resolved posts are cached in memory for an hour, so a reload or a back button doesn't Resolved posts are cached in memory for an hour, so a reload or a back button doesn't
drive the browser again. drive the browser again.
+14
View File
@@ -155,6 +155,19 @@ function setupMedia(media) {
} }
} }
// On iOS a video playing inline gets the "ambient" audio session, which the
// Ring/Silent switch mutes -- so the post plays perfectly and says nothing,
// and the only way to hear it is to go fullscreen. Claiming "playback" says
// what is true here: the sound is the point, not decoration. The session is
// not activated until something actually plays, so declaring it up front
// interrupts nothing; it is declared only on a page that has a video so an
// ordinary text post never claims it at all.
function setupAudioSession() {
if (!('audioSession' in navigator)) return;
if (!document.querySelector('video')) return;
navigator.audioSession.type = 'playback';
}
// Every comment is a <details open>, so folding one already works with this // Every comment is a <details open>, so folding one already works with this
// file missing. All this adds is doing the whole page at once, which is why // file missing. All this adds is doing the whole page at once, which is why
// the button ships hidden and is only revealed here. // the button ships hidden and is only revealed here.
@@ -189,6 +202,7 @@ setupCopy();
applyBrowser(storedBrowser()); applyBrowser(storedBrowser());
setupBrowserPicker(); setupBrowserPicker();
for (const media of document.querySelectorAll('.media')) setupMedia(media); for (const media of document.querySelectorAll('.media')) setupMedia(media);
setupAudioSession();
setupComments(); setupComments();
// --- Verification puzzles ------------------------------------------------- // --- Verification puzzles -------------------------------------------------