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

前端 未结 1 2004
半阙折子戏
半阙折子戏 2020-12-21 06:38

The ask here is to play all kinds of web sounds regardless of the hardware silent switch , both muted and not muted devices must keep playing the sound in H

相关标签:
1条回答
  • 2020-12-21 06:55

    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').src=null;"
        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.controls=false;s.loop=true;s.play();document.body.appendChild(s)"
        } else {
            jsInject = "var s=document.getElementById('wkwebviewAudio');s.src=null;s.parentNode.removeChild(s);s=null;s=new Audio('\(silenceMono56kbps100msBase64Mp3)');s.id='wkwebviewAudio';s.controls=false;s.loop=true;s.play();document.body.appendChild(s)"
        }
        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 - 13.3

    0 讨论(0)
提交回复
热议问题