iOS Webaudio Only Works on Headphones

ios, why webview load web with Phaser webaudio no sound when i mute the iphone or ipad

I encounter as the same issue as you, I use the HTML ,but abandon the webaudio to paly the sound.

Sound does not play on iPad speaker but works fine on headphones and on the iPod Touch/iPhone speakers

There is a code change solution to this, but also an end-user solution: turn the 'Ring/Silent switch' to on. Specifically, the problem is that the default setting of AVAudioSessions, AVAudioSessionCategorySoloAmbient, is to be silent if the phone is in silent mode.

As mentioned by the original poster, you can override this behavior by calling:

[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];

AVAudioSession Reference recommends setting the AVAudioSessionCategoryPlayback category:

For playing recorded music or other sounds that are central to the successful use of your app.

How to force WKWebView to ignore hardware silent switch on iOS?

Update: Added new hack working also for iOS 14(and 15)! (reflected in code, see bottom for extra details).

Since I have a solution to this nontrivial problem, I'd like to share it:

override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive),
name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive),
name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
configuration.mediaTypesRequiringUserActionForPlayback = []
wkWebView = WKWebView(frame: .zero, configuration: configuration)
}

@objc func willResignActive() {
disableIgnoreSilentSwitch(wkWebView)
}

@objc func didBecomeActive() {
//Always creates new js Audio object to ensure the audio session behaves correctly
forceIgnoreSilentHardwareSwitch(wkWebView, initialSetup: false)
}

And most importantly in WKNavigationDelegate:

private func disableIgnoreSilentSwitch(_ webView: WKWebView) {
//Nullifying the js Audio object src is critical to restore the audio sound session to consistent state for app background/foreground cycle
let jsInject = "document.getElementById('wkwebviewAudio').muted=true;"
webView.evaluateJavaScript(jsInject, completionHandler: nil)
}

private func forceIgnoreSilentHardwareSwitch(_ webView: WKWebView, initialSetup: Bool) {
//after some trial and error this seems to be minimal silence sound that still plays
let silenceMono56kbps100msBase64Mp3 = "data:audio/mp3;base64,//tAxAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAFAAAESAAzMzMzMzMzMzMzMzMzMzMzMzMzZmZmZmZmZmZmZmZmZmZmZmZmZmaZmZmZmZmZmZmZmZmZmZmZmZmZmczMzMzMzMzMzMzMzMzMzMzMzMzM//////////////////////////8AAAA5TEFNRTMuMTAwAZYAAAAAAAAAABQ4JAMGQgAAOAAABEhNIZS0AAAAAAD/+0DEAAPH3Yz0AAR8CPqyIEABp6AxjG/4x/XiInE4lfQDFwIIRE+uBgZoW4RL0OLMDFn6E5v+/u5ehf76bu7/6bu5+gAiIQGAABQIUJ0QolFghEn/9PhZQpcUTpXMjo0OGzRCZXyKxoIQzB2KhCtGobpT9TRVj/3Pmfp+f8X7Pu1B04sTnc3s0XhOlXoGVCMNo9X//9/r6a10TZEY5DsxqvO7mO5qFvpFCmKIjhpSItGsUYcRO//7QsQRgEiljQIAgLFJAbIhNBCa+JmorCbOi5q9nVd2dKnusTMQg4MFUlD6DQ4OFijwGAijRMfLbHG4nLVTjydyPlJTj8pfPflf9/5GD950A5e+jsrmNZSjSirjs1R7hnkia8vr//l/7Nb+crvr9Ok5ZJOylUKRxf/P9Zn0j2P4pJYXyKkeuy5wUYtdmOu6uobEtFqhIJViLEKIjGxchGev/L3Y0O3bwrIOszTBAZ7Ih28EUaSOZf/7QsQfg8fpjQIADN0JHbGgQBAZ8T//y//t/7d/2+f5m7MdCeo/9tdkMtGLbt1tqnabRroO1Qfvh20yEbei8nfDXP7btW7f9/uO9tbe5IvHQbLlxpf3DkAk0ojYcv///5/u3/7PTfGjPEPUvt5D6f+/3Lea4lz4tc4TnM/mFPrmalWbboeNiNyeyr+vufttZuvrVrt/WYv3T74JFo8qEDiJqJrmDTs///v99xDku2xG02jjunrICP/7QsQtA8kpkQAAgNMA/7FgQAGnobgfghgqA+uXwWQ3XFmGimSbe2X3ksY//KzK1a2k6cnNWOPJnPWUsYbKqkh8RJzrVf///P///////4vyhLKHLrCb5nIrYIUss4cthigL1lQ1wwNAc6C1pf1TIKRSkt+a//z+yLVcwlXKSqeSuCVQFLng2h4AFAFgTkH+Z/8jTX/zr//zsJV/5f//5UX/0ZNCNCCaf5lTCTRkaEdhNP//n/KUjf/7QsQ5AEhdiwAAjN7I6jGddBCO+WGTQ1mXrYatSAgaykxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg=="
//Plays 100ms silence once the web page has loaded through HTML5 Audio element (through Javascript)
//which as a side effect will switch WKWebView AudioSession to AVAudioSessionCategoryPlayback

var jsInject: String
if initialSetup {
jsInject =
"var s=new Audio('\(silenceMono56kbps100msBase64Mp3)');" +
"s.id='wkwebviewAudio';" +
"s.play();" +
"s.loop=true;" +
"document.body.appendChild(s);"
} else {
//Restore sound hack
jsInject = "document.getElementById('wkwebviewAudio').muted=false;"
}
webView.evaluateJavaScript(jsInject, completionHandler: nil)
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
//As a result the WKWebView ignores the silent switch
forceIgnoreSilentHardwareSwitch(webView, initialSetup: true)
}

Interestingly a related Safari problem is mentioned here: IOS WebAudio only works on headphones where @Spencer Evans workaround looks very similar to mine.

However when I tried to apply his shorter base64 silence sound it didn't work for WKWebView, so I'm providing my own minimal silence sound tested on iOS12.

Why it works?

Playing an <audio> or <video> element (which in the workaround happens to be non audible silence) changes WKWebView audio session category from AVAudioSessionCategoryAmbient to AVAudioSessionCategoryPlayback. This will be valid until next load request resets it.

It's all great till the app is backgrounded. But upon subsequent foregrounding things will break in 2 possible ways:

  • user needs to tap for the sounds to reappear
  • rarely no user input will help and the WKWebView lands in semi frozen state

To counter that^ the hack is reverted with disableIgnoreSilentSwitch(wkWebView) and later reenabled with forceIgnoreSilentHardwareSwitch(wkWebView, initialSetup: false)

Since WKWebView core runs in an external process it cannot be accessed the way UIWebView shared (with our app) AVAudioSession can be.

Verified for:

iOS 11.4

iOS 12.4.1

iOS 13.3

iOS 14.1

iOS 14.5.1

iOS 14.8

iOS 15.0

iOS 14 update

Situation got pretty bad in iOS 14 where obsolete audio tag .src=null trick stopped working. Technically .src=null does work for a very short window of time (one can revert the hack using .src during initial setup). However once the silence loop is playing it becomes useless.

The new trick relies on .mute which miraculously works across all iOS versions including iOS14 (but only when accessing documentById directly not a var). No mediacenter when locking the screen neither. It took a lot of research, but we got it.

How to Play a sound using AVAudioPlayer when in Silent Mode in iPhone

Actually, you can do this. It is controlled via the Audio Session and has nothing to do with AVAudioPlayer itself. Why don't you want to use AudioSession? They play nice together...

In your app, you should initialize the Audio Session, and then you can also tell indicate what kind of audio you intend to play. If you're a music player, then it sort of makes sense that the user would want to hear the audio even with the ring/silent switch enabled.

    AudioSessionInitialize (NULL, NULL, NULL, NULL);
AudioSessionSetActive(true);

// Allow playback even if Ring/Silent switch is on mute
UInt32 sessionCategory = kAudioSessionCategory_MediaPlayback;
AudioSessionSetProperty (kAudioSessionProperty_AudioCategory,
sizeof(sessionCategory),&sessionCategory);

I have an app that I do this very thing, and use AVAudioPlayer to play audio, and with the ring/silent switch enabled, I can hear the audio.

UPDATE (11/6/2013)

In the app I mentioned above, where I used the code above successfully, I have (for some time) been using the following code instead to achieve the same result:


AVAudioSession *audioSession = [AVAudioSession sharedInstance];
NSError *error = nil;
BOOL result = NO;
if ([audioSession respondsToSelector:@selector(setActive:withOptions:error:)]) {
result = [audioSession setActive:YES withOptions:0 error:&error]; // iOS6+
} else {
[audioSession setActive:YES withFlags:0 error:&error]; // iOS5 and below
}
if (!result && error) {
// deal with the error
}

error = nil;
result = [audioSession setCategory:AVAudioSessionCategoryPlayback error:&error];

if (!result && error) {
// deal with the error
}

I thought I'd post this as an alternative, in light of the most recent comment to this answer. :-)

Pan audio in cordova (play only out of left / right headphone)

On Android 6.0, the following script works. It makes use of the Web Audio API. I have not yet tested it on iOS, but I have a good feeling it will work there (please comment if it does not).

You need an "omg.mp3" file in your root directory to test with. You also should build using Cordova and not worry about the CORS or Same-domain error you might get in your browser

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width">

<title>StereoPannerNode example</title>

<link rel="stylesheet" href="">
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>

<body>
<h1>StereoPannerNode example</h1>
<audio controls>
<source src="omg.mp3" type="audio/mp3">
<p>Browser too old to support HTML5 audio? How depressing!</p>
</audio>
<h2>Set stereo panning</h2>
<input class="panning-control" type="range" min="-1" max="1" step="0.1" value="0">
<span class="panning-value">0</span>

</body>
<script>
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var myAudio = document.querySelector('audio');

var panControl = document.querySelector('.panning-control');
var panValue = document.querySelector('.panning-value');

// Create a MediaElementAudioSourceNode
// Feed the HTMLMediaElement into it
var source = audioCtx.createMediaElementSource(myAudio);

// Create a stereo panner
var panNode = audioCtx.createStereoPanner();

// Event handler function to increase panning to the right and left
// when the slider is moved

panControl.oninput = function() {
panNode.pan.value = panControl.value;
panValue.innerHTML = panControl.value;
}

// connect the AudioBufferSourceNode to the gainNode
// and the gainNode to the destination, so we can play the
// music and adjust the panning using the controls
source.connect(panNode);
panNode.connect(audioCtx.destination);
</script>
</html>


Related Topics



Leave a reply



Submit