2018年12月19日水曜日

HTML5 video / audioがiOSデバイスの消音スイッチの状態に従うようにする方法 (UIWebView編 / WKWebView編)

いろいろ調べていたのですが、遥か大昔に私が書いたブログ記事が長い年月を経て今や大間違いになっていたのでここに訂正させていただきたいと思います。

今回ご紹介するのはUIWebViewまたはWKWebViewで表示しているHTML5のvideo要素やaudio要素が音声を出すときに、iOSデバイスについている消音スイッチ (ミュートスイッチ, mute switch, silent switch) の状態を無視してしまう問題を解決する方法です。相変わらず紹介内容がとてもニッチですね。

ちなみにSafariはデフォルトでちゃんと消音スイッチの状態を反映してくれるので、SafariでYouTubeの動画を見ててもいきなり音が流れ出すことはありません。素晴らしい!というわけで我々のアプリもぜひそのようにしたいと思います。

消音スイッチの挙動について

まず基本的なおさらいとして、消音スイッチがどのような挙動を示すかについてこちらにまとめます。
  • 各プロセスごとに、AVAudioSessionが消音スイッチに対してどのように振る舞うかを定義している。
  • 具体的には、AVAudioSession.Categoryの値に応じて挙動が変化する。ドキュメントにも明記されている。
    • AVAudioSession.Category.ambientやAVAudioSession.Category.soloAmbientを指定すると、消音スイッチがONのときは音が出ないようになる。
    • 逆にAVAudioSession.Category.playbackを指定すると消音スイッチを一切無視するようになる。
  • 消音スイッチの現在の物理的な状態を取得するAPIは一切ない。Private APIで以前は可能だったが、穴が塞がれたためその方法も利用できない。そもそもアプリが提出時の審査で蹴られる。当然JS経由でHTMLコンテンツ上から状態を取得するのも不可能。

解決方法・UIWebView編

UIWebViewはWebKit1を利用しており、したがってUIWebViewのエンジンは我々のアプリ内のプロセスで動作します。そこでUIWebViewのインスタンスを生成するより先に、先述の通りAVAudioSessionのcategoryを変更して消音スイッチの状態を反映してやるように示してやるとうまくいきます。
try? AVAudioSession.sharedInstance().setCategory(.soloAmbient, mode: .default, options: [])
try? AVAudioSession.sharedInstance().setActive(true)
ただしご存知の通り、すでにUIWebViewはdeprecated扱いとなっており、WKWebViewへの移行が推奨されています。そこでWKWebViewでも同様の方法が使えないかやってみましょう。

解決方法・WKWebView編

ありません。

繰り返しますが、ありません。WebKit2の設計上のバグです。

今後修正される可能性もほぼ間違いなくありません。諦めてください。

一応、順を追って説明します。
  1. WKWebViewはWebKit2で実装されています。
  2. WebKit2は我々のアプリ内のプロセスで動作するのではなく、「共用の」WebCoreプロセス上で動作し、その結果が我々のアプリ内に転送されてくるような実装になっています。
  3. 「共用の」WebCoreプロセスは(これは推測ですが、audioやvideoを最大限に活かすため)AVAudioSession.Category.playbackとAVAudioSession.Mode.moviePlaybackで動作するように設定されている用に見えます。したがって消音スイッチは無視されます。
  4. 最初にご説明したとおり、AVAudioSessionによる消音スイッチに対する振る舞いは「プロセス単位」で制御されており、これは遥か大昔のiOS 2.0どころか下手するとNeXTSTEPの時代からCarbonレイヤーで決められている挙動だったりします。要するに今からの変更はほぼ不可能です。
  5. もうおわかりだと思いますが、「共用の」WebCoreプロセスの挙動を我々個別のアプリがAVAudioSession経由で勝手に変更することは不可能ですし、これを可能にするのはiOSとWebKitの設計上実質不可能なため、問題は解決されません。
    1. WebCoreプロセスを各個別のアプリごとに立ち上げて対応しろよ!と思うかもしれませんが、WebCoreプロセスは近年AppleのOSに組み込まれている特権階級モードで動作するプロセスとなっており(そのためメモリに無制限なアクセスが可能で、JSをJITコンパイルして高速に動作させる事が可能)、最近のセキュリティとバッテリーライフにうるさいAppleがそれを個別のアプリごとに立ち上げさせるなどということは現経営陣が全滅しない限りありえないと断言できます。
    2. 共用のWebCoreプロセスが使用するAVAudioSession.Categoryを変えてしまえばいいだろ!とも思いますが、実はAVAudioSession.Categoryをplaybackに指定しない限りバックグラウンドで音楽を流し続けることができません。したがって今度はWebViewで音楽プレイヤーを作ったりPicture in Pictureで動画を流し続けるアプリが全滅するため、これも不可能です。
    3. だったら消音スイッチの状態を自分で調べて自分で動画をmuteにすればよいのでは、と思いますが、これも先述の通り消音スイッチの状態を調べる方法は一切ないため、一律でmuteにしてしまうなどの乱暴な方法を用いない限り対応できません。
やばいですね☆