如何强制 WKWebView 忽略 iOS 上的硬件静默开关?
How to force WKWebView to ignore hardware silent switch on iOS?
这里的要求是播放各种网络声音,不管硬件静音开关,静音和非静音设备必须继续播放 HTML中的声音应用程序处于前台时的页面。
已弃用 UIWebView
的解决方案非常简单
let localWebView = UIWebView(frame: .zero)
localWebView.allowsInlineMediaPlayback = true
localWebView.mediaPlaybackRequiresUserAction = false
如何为 WKWebView
实现相同的行为?
更新:添加了也适用于 iOS 14(和 15)的新 hack! (反映在代码中,更多细节见底部)。
由于我有解决这个重要问题的方法,所以我想分享它:
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)
}
最重要的是 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)
}
有趣的是,这里提到了一个相关的 Safari 问题:IOS WebAudio only works on headphones @Spencer Evans 的解决方法与我的非常相似。
然而,当我尝试应用他的较短的 base64 静音时,它对 WKWebView
不起作用,所以我提供了我自己在 iOS12 上测试过的最小静音。
为什么有效?
播放 <audio>
或 <video>
元素(在解决方法中碰巧是听不见的静音)将 WKWebView
音频会话类别从 AVAudioSessionCategoryAmbient
更改为 AVAudioSessionCategoryPlayback
.这将一直有效,直到下一个加载请求将其重置。
在应用程序后台运行之前一切都很好。但是在随后的前景中,事情将以两种可能的方式中断:
- 用户需要点击才能重新出现声音
- 很少有没有用户输入的情况,WKWebView 会处于半冻结状态
为了 反击 ^ 黑客被 disableIgnoreSilentSwitch(wkWebView)
恢复,后来又被 forceIgnoreSilentHardwareSwitch(wkWebView, initialSetup: false)
重新启用
由于 WKWebView
核心在外部进程中运行,因此无法以 UIWebView
共享(与我们的应用程序)AVAudioSession
的方式进行访问。
已验证:
iOS11.4
iOS12.4.1
iOS13.3
iOS14.1
iOS14.5.1
iOS14.8
iOS15.0
iOS 14更新
iOS 14 中的情况变得非常糟糕,过时的音频标签 .src=null
技巧停止工作。从技术上讲,.src=null
确实在很短的 window 时间内有效(可以在初始设置期间使用 .src
恢复黑客攻击)。然而,一旦播放静音循环,它就变得无用了。
新技巧依赖于 .mute
,它奇迹般地适用于所有 iOS 版本,包括 iOS14(但仅当直接访问 documentById 而不是 var 时)。锁定屏幕时也没有媒体中心。花了很多研究,但我们明白了。
这里的要求是播放各种网络声音,不管硬件静音开关,静音和非静音设备必须继续播放 HTML中的声音应用程序处于前台时的页面。
已弃用 UIWebView
的解决方案非常简单
let localWebView = UIWebView(frame: .zero)
localWebView.allowsInlineMediaPlayback = true
localWebView.mediaPlaybackRequiresUserAction = false
如何为 WKWebView
实现相同的行为?
更新:添加了也适用于 iOS 14(和 15)的新 hack! (反映在代码中,更多细节见底部)。
由于我有解决这个重要问题的方法,所以我想分享它:
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)
}
最重要的是 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)
}
有趣的是,这里提到了一个相关的 Safari 问题:IOS WebAudio only works on headphones @Spencer Evans 的解决方法与我的非常相似。
然而,当我尝试应用他的较短的 base64 静音时,它对 WKWebView
不起作用,所以我提供了我自己在 iOS12 上测试过的最小静音。
为什么有效?
播放 <audio>
或 <video>
元素(在解决方法中碰巧是听不见的静音)将 WKWebView
音频会话类别从 AVAudioSessionCategoryAmbient
更改为 AVAudioSessionCategoryPlayback
.这将一直有效,直到下一个加载请求将其重置。
在应用程序后台运行之前一切都很好。但是在随后的前景中,事情将以两种可能的方式中断:
- 用户需要点击才能重新出现声音
- 很少有没有用户输入的情况,WKWebView 会处于半冻结状态
为了 反击 ^ 黑客被 disableIgnoreSilentSwitch(wkWebView)
恢复,后来又被 forceIgnoreSilentHardwareSwitch(wkWebView, initialSetup: false)
由于 WKWebView
核心在外部进程中运行,因此无法以 UIWebView
共享(与我们的应用程序)AVAudioSession
的方式进行访问。
已验证:
iOS11.4
iOS12.4.1
iOS13.3
iOS14.1
iOS14.5.1
iOS14.8
iOS15.0
iOS 14更新
iOS 14 中的情况变得非常糟糕,过时的音频标签 .src=null
技巧停止工作。从技术上讲,.src=null
确实在很短的 window 时间内有效(可以在初始设置期间使用 .src
恢复黑客攻击)。然而,一旦播放静音循环,它就变得无用了。
新技巧依赖于 .mute
,它奇迹般地适用于所有 iOS 版本,包括 iOS14(但仅当直接访问 documentById 而不是 var 时)。锁定屏幕时也没有媒体中心。花了很多研究,但我们明白了。