Ask iOS for the playback audio session on a page with a video
CI / Typecheck, test, build (pull_request) Successful in 12s
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:
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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 -------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user