Improve sound playback on iOS by priming audio elements
Refactor the `NotificationPlayer` class to correctly handle audio playback initiation on iOS, ensuring that sounds play reliably after user gestures by properly managing the unlocked state based on successful priming of HTMLAudioElements. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: f541a3b8-ce77-4f70-9fa4-f1272f6e5c7a Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/mkNwf1z Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -104,10 +104,11 @@ class NotificationPlayer {
|
||||
isUnlocked(): boolean {
|
||||
if (!this.unlocked) return false;
|
||||
if (this.useIos) {
|
||||
// On iOS the "context" concept doesn't apply — being unlocked
|
||||
// means at least one HTMLAudioElement has cleared its silent
|
||||
// play+pause inside a gesture. We trust the flag.
|
||||
return true;
|
||||
// On iOS, "unlocked" is set only once at least one
|
||||
// HTMLAudioElement has finished its silent priming play+pause
|
||||
// inside a gesture (see `unlock()` for why this matters for
|
||||
// hint suppression timing).
|
||||
return this.htmlAudioUnlocked.size > 0;
|
||||
}
|
||||
return this.ctx?.state === "running";
|
||||
}
|
||||
@@ -227,24 +228,34 @@ class NotificationPlayer {
|
||||
// burst of all 8 sounds at once. After the silent play resolves
|
||||
// we pause + restore volume + reset currentTime so the next
|
||||
// real play() starts from the beginning at full volume.
|
||||
//
|
||||
// We only flip `this.unlocked = true` once at least one element
|
||||
// has actually finished a successful priming play. Setting it
|
||||
// synchronously would cause the hint suppression path in
|
||||
// `playIos()` (which checks `!this.unlocked`) to swallow a
|
||||
// legitimate NotAllowedError from a play attempt that arrives
|
||||
// between the gesture and the moment the priming promise
|
||||
// settles.
|
||||
for (const entry of NOTIFICATION_SOUND_MANIFEST) {
|
||||
if (this.htmlAudioUnlocked.has(entry.id)) continue;
|
||||
const el = this.getHtmlAudio(entry.id);
|
||||
if (!el) continue;
|
||||
const originalVolume = el.volume;
|
||||
el.volume = 0;
|
||||
const markPrimed = () => {
|
||||
try {
|
||||
el.pause();
|
||||
el.currentTime = 0;
|
||||
el.volume = originalVolume;
|
||||
this.htmlAudioUnlocked.add(entry.id);
|
||||
this.unlocked = true;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
const p = el.play();
|
||||
if (p && typeof p.then === "function") {
|
||||
p.then(() => {
|
||||
try {
|
||||
el.pause();
|
||||
el.currentTime = 0;
|
||||
el.volume = originalVolume;
|
||||
this.htmlAudioUnlocked.add(entry.id);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}).catch(() => {
|
||||
p.then(markPrimed).catch(() => {
|
||||
// Element couldn't start — likely no gesture yet, or the
|
||||
// user denied it. We'll try again on the next gesture.
|
||||
try {
|
||||
@@ -255,17 +266,9 @@ class NotificationPlayer {
|
||||
});
|
||||
} else {
|
||||
// Synchronous return (very old WebKit). Treat as success.
|
||||
try {
|
||||
el.pause();
|
||||
el.currentTime = 0;
|
||||
el.volume = originalVolume;
|
||||
this.htmlAudioUnlocked.add(entry.id);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
markPrimed();
|
||||
}
|
||||
}
|
||||
this.unlocked = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user