fix: enable TTS audio playback on iOS when silent mode is on#1151
fix: enable TTS audio playback on iOS when silent mode is on#1151NJX-njx wants to merge 1 commit intomoeru-ai:mainfrom
Conversation
- Add ios-audio-unmute utility that plays a silent HTML5 <audio> element to force iOS into 'playback' audio session (bypasses mute switch) - Register one-time touchend/click listener in audio context store to activate the workaround on first user interaction - Safe no-op on non-iOS platforms Closes moeru-ai#894
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request resolves an issue where Text-to-Speech (TTS) audio would not play on iOS devices when the mute switch was engaged. The solution involves a common web development pattern for iOS, which plays a silent HTML5 audio element to force the browser into a "playback" audio session category, bypassing the mute switch's effect on Web Audio API output. This ensures a consistent audio experience for users regardless of their device's silent mode setting. Highlights
Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Pull request overview
Adds an iOS-specific workaround so Web Audio (TTS) playback isn’t muted by the hardware silent switch by forcing iOS into a “playback” audio session via a looping silent <audio> element.
Changes:
- Added an iOS “audio unmute” utility that plays a silent looping WAV via HTML5 audio.
- Registered one-time
touchend/clicklisteners in the audio store to trigger the workaround on first user interaction.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| packages/stage-ui/src/utils/ios-audio-unmute.ts | Introduces the silent-audio playback workaround + helpers. |
| packages/stage-ui/src/stores/audio.ts | Hooks the workaround into first user interaction so play() is gesture-initiated. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function enableIOSPlayback(): void { | ||
| if (disposed || silentAudio || !isIOS()) | ||
| return |
There was a problem hiding this comment.
enableIOSPlayback() sets silentAudio before attempting play(), and the guard if (disposed || silentAudio || !isIOS()) return prevents any future call from retrying. If play() rejects (your comment says the next interaction will trigger it), the code will currently never re-attempt playback; consider retrying play() on subsequent calls or clearing silentAudio in the catch so a later user gesture can try again.
| const SILENT_WAV_DATA_URI | ||
| = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=' |
There was a problem hiding this comment.
The SILENT_WAV_DATA_URI assignment formatting (const ... on one line and = on the next) appears inconsistent with the surrounding codebase style and is likely to be reformatted/flagged by formatters/linters. Consider using the repository’s typical const X = ... layout (or the standard Prettier split with = at end of the first line).
| const SILENT_WAV_DATA_URI | |
| = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=' | |
| const SILENT_WAV_DATA_URI = | |
| 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=' |
| export function disposeIOSPlayback(): void { | ||
| disposed = true | ||
| if (silentAudio) { | ||
| silentAudio.pause() | ||
| silentAudio.src = '' | ||
| silentAudio = undefined |
There was a problem hiding this comment.
disposeIOSPlayback() sets a module-level disposed = true, which permanently disables enableIOSPlayback() for the rest of the session (even if called again after dispose). If disposeIOSPlayback() is intended as a temporary cleanup (e.g., on component unmount), consider allowing re-enable by resetting disposed (or removing the disposed flag and relying on silentAudio state).
There was a problem hiding this comment.
Code Review
This pull request effectively addresses the issue of TTS audio not playing on iOS when silent mode is enabled by implementing the standard workaround of playing a silent HTML5 <audio> element. The new ios-audio-unmute.ts utility provides a clean encapsulation of the iOS detection and audio playback logic, and its integration into the useAudioContext store ensures it's triggered by a user gesture, as required by iOS. The solution is well-documented and considers non-iOS platforms and performance aspects like using a data URI for the silent WAV. There are a couple of minor improvements related to code clarity and adherence to the provided style guide for code suggestions.
| document.removeEventListener('touchend', unlockOnInteraction, true) | ||
| document.removeEventListener('click', unlockOnInteraction, true) |
There was a problem hiding this comment.
The once: true option in addEventListener automatically removes the listener after it has been invoked once. Therefore, explicitly calling document.removeEventListener inside unlockOnInteraction is redundant and can be removed for cleaner code.
| document.removeEventListener('touchend', unlockOnInteraction, true) | |
| document.removeEventListener('click', unlockOnInteraction, true) | |
| document.addEventListener('touchend', unlockOnInteraction, { capture: true, once: true }) | |
| document.addEventListener('click', unlockOnInteraction, { capture: true, once: true }) |
| const SILENT_WAV_DATA_URI | ||
| = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=' |
There was a problem hiding this comment.
For better readability and conciseness, especially for short constant definitions, consider placing the entire definition on a single line if it fits within typical line length limits. This also demonstrates adherence to the style guide's suggestion format (Repository Style Guide, lines 9-15).
| const SILENT_WAV_DATA_URI | |
| = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=' | |
| const SILENT_WAV_DATA_URI = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=' |
References
- Always wrap code suggestions in GitHub suggestion blocks to enable one-click application by maintainers. Use GitHub's suggestion syntax for all code recommendations. (link)
⏳ Approval required for deploying to Cloudflare Workers (Preview) for stage-web.
Hey, @nekomeowww, @sumimakito, @luoling8192, @LemonNekoGH, kindly take some time to review and approve this deployment when you are available. Thank you! 🙏 |
Summary
Fixes TTS audio not playing on iOS when the mute/silent switch is on. Addresses #894.
Root Cause
On iOS, the hardware mute switch causes all Web Audio API output to be silent because
AudioContextis treated as the "ambient" audio session category. This is a known WebKit limitation:Solution
The standard workaround (used by YouTube, Spotify Web, and other production apps) is to play a silent HTML5
<audio>element alongside the Web Audio API. This forces iOS to assign the "playback" audio session category, which is not affected by the mute switch.Changes
packages/stage-ui/src/utils/ios-audio-unmute.ts(new):enableIOSPlayback(): Creates and loops a tiny silent WAV via HTML5<audio>, forcing iOS into "playback" modedisposeIOSPlayback(): Cleanup functionisIOS(): Platform detection (includes iPad with desktop UA)packages/stage-ui/src/stores/audio.ts:touchend/clickdocument listener that callsenableIOSPlayback()on first user interaction<audio>.play()Testing Notes
<audio>element is created (singleton pattern)Closes #894