モバイルブラウザオーディオ調査・伝説の最期編

Web Audio の対応形式を HTML Audio インスタンスを生成して canPlay で判定しちゃいけないんだ…(2016/10/05)

HTML Audio、もしくはHTML Videoをページ内で1つでも使用していた場合、 そのページでは WebAudioAPI の音がマナーモード時にも鳴ってしまう ということが判明しました。


Windows 8.1 + Firefox 44.0.2 で発生している HTML Audio で再生開始時のシークに失敗する問題の手当てしました。その時の音声ファイルは mp3 CBR でした。2016.3.9


端末スペックによって iOS9 以降でも HTML Audio で再生したいケースがあるため、iOS7 以降について追調査結果を記入しコードも更新しました。2016.1.5

天駆ける龍の如く怒涛の調査結果を投下します。実際のソースは OSDN でご確認ください。

はじめに、モバイルブラウザオーディオの闇を晴らす

この間、個人や企業のリリースしたモバイルオーディオライブラリを読み込み、ブログ記事を拝見し、自腹で中古端末を揃えて動作確認を行ってきました。

とくに Android は OS バージョン毎の差異は元より端末メーカーが独自に加えたシステムの変更によってオーディオの挙動が変わるものまでありました。

音声が鳴らず首をかしげるもの(AOSP4.1.1 HTL21 例外的に load() が必要)から、甚だしきはクラッシュ(Web Audio が有効で意図せずそれを呼び出していた AOSP4.4.2 ARROWS NX F-01F)も経験しました。

賢いオーディオライブラリがないとたまらない、ということがはっきりしました。

膨大な差異情報こそ大人なソーシャルゲーム開発の肝

とある企業のリリースしたライブラリは、超メジャーな自社ソーシャルサービスにも使われているといいます。しかし、調査を進めていくとサービスの規模に相応しく多様な端末に対応するには、細かくパラメータを与えて動作を分岐させる必要があります。

そしてこのパラメータ情報を伏せて当たり障りのない部分をリリースしているようにも見えます。

このような膨大な差異情報は大手ソーシャルゲームメーカーと弱小開発会社や個人の間に立ちふさがる絶壁です。

僕は弱小開発会社の従業員で、ささやかなオープンソースプロジェクトに携わる個人ですので、ちゃっちゃと情報を放流します。

はてさて大手さんの集積した情報とどれほど開きがあるやら~、そしてその開きの程を僕が知ることは無いでしょう…

ライブラリ作者のみなさまへ

国産ゲームライブラリの作者のみなさん、オーディオ API にモヤモヤしたコメントや空のメソッドを並べてる暇で、pettanR からまるっともってくればいいじゃない?応援してます。

pettanR は元々ドローイング・デザイン系の Web アプリケーションの GUI のためのプロジェクトなのですけど、おかげさまでオーディオ機能が充実しつつあります…ちょうどいいのでsencha touch みたくゲームも作ってみたい。

用語の解説

ブラウザについて

AOSP

Android 4.4.2 までの Android 標準ブラウザ。2.3 からサポートした HTML Audio は、4.0 以降同時再生数が一音になり、4.2 以降は音声のロードにタッチが必要になった。

メーカーのカスタマイズによって Web Audio が使えるようになっていたり、オーディオの周りの挙動が変化しているものもあって多難。

Chrome WebView

Android 4.4.3 からの Android 標準ブラウザ。オーディオ周りの挙動が微妙に変化している。Android 5.0 以降でついに Web Audio をサポートする。

Android 4.4 (KitKat) からは、 AOSP Stock Browser(通称 Android Browser)のサポートが終了し、代わりに、Chrome WebView (Chromium) をラップしたブラウザが提供されています。

(中略)UserAgent が書き換え可能なため、旧来のAOSPブラウザの UserAgent を偽装した形で配布されているケースがあります。

HTML Audio で遭遇する不具合たち

HTML Audio で遭遇する不具合の回避手段について。命名は僕です。

seekFix

一部の環境でシーク時に再生が止まる問題を回避する。

durationFix

一部の環境で play() 後しばらく経過しないと正しい曲の長さが取れない問題を回避する。

この中には正しい duration が取れないうちに currentTime にセットするとエラーになるものまである。(iOS4, iOS6, Win+Opera12)

currentTimeFix

ごく一部の環境で currentTime が更新されず(getterのみ、setter は可能)正しい現在位置が取得できない問題を回避する。Date.now() 的な値を使って再生位置を求める。

pauseFix

ごく一部の環境で pause() しても再生が止まらない問題を回避する。

endedFix

一部の環境で ended イベント時にシークができなくなる問題を回避する。解決できる環境と発生頻度が改善する程度の環境がある。後者も音声の最後に無音を追加して ended に至らないようにすることで完全に解決できる。

timeupdateFix

ごく一部の環境で seeking waiting が発生せず、前回から currentTime が更新されていないのに timeupdate が発生する。直前の currentTime を控えておいて現在のものと比較して timeupdate か seeking か判定する。(Windows Phone 7.8)

Web Audioのサポートモバイルブラウザ(公証)とその実際

Web Audio の使える環境ではひとまずはそちらを使うことにします。一般に後発の Web Audio の方が問題が少なく使用可能です。

しかし、まれに Web Audio の腐ったブラウザがリリースされることがあるため HTML Audio にフォールバックする体制を続ける必要があるように思います。(Windows8 + Firefox40以降でしばらくすると Web Audio が止まる、41 でも確認)(逆に Windows8.1 Opera 35.0 は HTML Audio がおかしい。)

しかし、Android2.2 以降にインストールできる Firefox の Web Audio は 2.x では上手く動作しないときがあります。2.x 時代の端末では性能が足りていないようです。

表1:Web Audioのサポートの開始

ブラウザ対応OSWeb Audioのサポート(公称)
SafariiOS6.0+
標準ブラウザAndroid1.0+5+
Webkit OperaAndroid4.0+?
Blink OperaAndroid4.0+15?
FirefoxAndroid2.2+23+(25)
ChromeAndroid4.0+28+(29)
OperaPC15+(22)
FirefoxPC23+(25)
ChromePC10+(14)

表2:Web Audioのサポートの実際

OS(端末)ブラウザWeb Audio
iOS7 (iPad 2G)Safari△#1
iOS8 (iPad mini 3G)Safari
Android2.3.5 (SBM101SH)Frefox 31.0○?#2
Android3.1 (L-06C)Frefox -?
Android4.1.1 (Dospara)Chrome 44.0
Opera 32.0
Frefox 41.0
  1. はじめて再生する音声の音が割れる、リロードすると解消する
  2. Android2.x に対しても最新の機能を提供する Mozilla 偉い!でもたまに不安定なときがあるような…

1.の音割れについては解決策が phina.js で紹介されていました。ハイレベルな開発陣が集まっている凄いプロジェクトです(汗

オーディオ API 性能表

表3:HTML Audio の性能

OS(端末)ブラウザスコアタッチ不要2音以上再生裏に回った検出最後に無音不要ボリュームメモ
iOS4.2.1 (iPod 2G)Safari2xx○#11?-
iOS6.1.5 (iPod 4G)Safari1xxx#12?-
Android2.3.5 (SBM101SH)AOSP3◎#2x○#6?#5
Opera12.103◎#2x?#3 #4
Firefox31.03x?#13 #16
Android3.1 (L-06C)AOSP3x△#7?-
Android4.0.4 (ST15i)AOSP2xx??-
Android4.1.1 (Dospara)AOSP2xx○#8?#10
Android4.2.2 (F-10D)AOSP2xxx○#8?#10
Chrome282xx○#9?-
Android4.4.4Chrome WebView1xxx○#9?-
WP7.8 (IS12)IEM91xxxx#14#15
WP8.1 (lumia925)IEM113x○#1?-
  1. 2音以上の場合、シークが不安定かもしれない
  2. 4音以上は現実的ではないかも
  3. audio.currentTime が更新され無いため Date.now() 等を使って現在時間を決定する
  4. 可変ビットレートな音声ファイルでシークが不安定になる。mp3 CBR を使用する
  5. シークが若干(0.5秒程度)遅れる
  6. ended に達すると音が鳴らなくなる -> リロードで解決 audio.loop を使って解決
  7. ended に達すると音が鳴らなくなる -> リロードで解決、但し 2.x 4.x より遅延が大きく1秒弱程度ある
  8. ended に達した際にしばしば currentTime が変更できなくなる -> リロードで解決、また audio.play() を currentTime の更新前に実施しておくと頻度が低下する audio.loop を使って解決
  9. ended に達した際に audio.play() が必要
  10. ogg のシークが不安定、mp3 m4a は安定する
  11. ended に達すると音が鳴らなくなる、audio.play() で解決、ended イベントは発しないので timeupdate イベント時に currentTime で判断する
  12. ended に達すると音が鳴らなくなる、audio.play() で頻度が改善、ended イベントは発しないので timeupdate イベント時に currentTime で判断する
  13. Web Audio をサポートしているが、ブラウザが不安定になるため使用しない
  14. audio.volume に 1 以外を設定すると音が聞こえない
  15. 最後の曲が再生できない、シークが不安定
  16. シーク時に再生がしばしば止まる問題、audio.play() で解決、Windows8.1 + Gecko40 でも発生。

タッチイベントで audio.play() するタイミング

タッチイベントのコールバック内で audio.load() または audio.play() を実施する。

このタッチを行う適切なタイミングは各環境で異なる。ついでにいうと audio.src = src 以降に発生するイベントも各環境で異なる。

次の表のタッチトリガーイベント列のイベントをきっかけとして(ボタン等で)ユーザーにタッチを促すのが良かった。

表4:タッチ要求のトリガーイベントと audio.duration の変化

環境タッチトリガーイベントdurationの変化durationFix
Chrome18 (HTL21)stalled0 -> 100s -> n #1
Chrome28 (F-10D)stalled0 -> 100s -> n #2
Chrome32 (ドスパラ)canplaythrought #30 -> n
Chrome36 (ドスパラ)canplaythrought0 -> n-#4
AOSP4.2.2 (F-10D)canplaythroughtNaN -> n-
Chrome WebView 4.4.4canplaythroughtNaN -> 0 -> n
iOS4.2.1suspendNaN -> n
iOS6.1.5suspendNaN -> n
iOS7.1(シミュレータ)suspend0 -> n
iOS8.1(シミュレータ)loadedmetadata--
WP7.8 IEM9canplayNaN -> n
WP8.1 IE11canplaythrought--
XP Opera12.17タッチ不要NaN -> Infinity -> NaN -> n
  1. しばらくの間 100s というダミーの値が duration に入っている、その間はシークができない。
  2. Chrome 18 から 28 の間のいつから 100s のときもシークができるのか?は不明。
  3. Chrome 32以降 stalled が発生しなくなる、31 では 100s が出現しない。
  4. Chrome 36以降 durationFix が不要になる、45 でもタッチは必要。

インスタンス生成~準備完了までのステップ

おおまかにいってタッチによる再生の有無に durationFix の有無をかけた計4パターンが必要です。

表5:タッチと durationFix のマトリクス

durationFix が必要durationFix が不要
タッチが必要12
タッチが不要34

計4種類の生成~準備プロセスはそれぞれ次の段階を通過する。

表6:タッチトリガーイベント発生から再開始生完了まで

経路1.タッチトリガーイベント発生(表4参照)2.タッチイベント発生3.duration取得4.Ready イベント発生
1タッチ要求play()currentTimeにセットtimeupdate
2タッチ要求play()timeupdate
3play()currentTimeにセットtimeupdate
4canplaythrought等

1. タッチ:要, durationFix:要な環境

例: iOS6, iOS4, WP7.8, Chrome WebView 4.4.4

  1. audio.src のセット
  2. タッチトリガーイベント発生(表4参照)を待って再生開始ボタン等の表示
  3. タッチイベント内で audio.play() する、ここでは currentTime のセットは行えない
  4. duration が取れたら currentTime に触ることができる、currentTime をセットする
  5. timeupdate イベントを待って再生開始完了

2. タッチが必要な環境

例: AOSP 4.2

  1. audio.src のセット
  2. タッチトリガーイベント発生(表4参照)を待って再生開始ボタン等の表示
  3. タッチイベント内で audio.play() と currentTime のセットを行う
  4. timeupdate イベントを待って再生開始完了

3. durationFix が必要な環境

例: WindowsXP + Opera12

  1. audio.src のセット
  2. canplaythrought 等を待って audio.play() する、ここでは currentTime のセットは行えない
  3. duration が取れたら currentTime に触ることができる、currentTime をセットする
  4. timeupdate イベントを待って再生開始完了

4. もっとも制限の少ない環境

例: PCブラウザ

  1. audio.src のセット
  2. canplaythrought 等を待って audio.play() と currentTime のセットを行う、再生開始完了

Audio インスタンスを触るポイント

生成~ロード~play()

  1. audio = new Audio( url ) または new HTMLAudioElement( url ); Safari3.2- では document.createElement( 'audio' );
  2. audio.preload = 'auto'; autoplay が優先されるけど一応

    autoplay 属性はこの属性(preload)より優先します。これは、音声を自動的に再生したい場合は明らかにブラウザが音声をダウンロードしなければならないためです。

    仕様書では autoplay 属性と preload 属性を両方とも指定することを認めています。()内は僕

  3. audio.autoplay = true; Android 4.0 - 4.1 で必要
  4. audio.src = src;
  5. audio.load() 省略できる環境が多い。必要な環境:AOSP4.1.1 HTL21
  6. audio.play()

シーク

  1. timeupdate イベント内でないとシークできない iOS
  2. play() 前にセットするとエラーになる
  3. シークの際に play() も読んでおかないと再生が停止する

破棄

audio.src = '';audio.load() を実施すると安定する?

emded イベント時にシークが無効になるブラウザと対策

解決法の無いものもあるので音声の最後に無音部分を追加して回避する。

また同じ問題は Android5.0 + Web Audio でも起きるかもしれない。

AOSP2.x, AOSP4.x

audio.loop = true を設定する。

但し ended イベントが起きなくなるため、timeupdate の際に currentTime が 0 付近に戻るのを検出して ended を代替する。

AOSP3.1

リロードを実施する、次の再生まで遅延が大きく1秒弱程度待つことになる

audio.src = '';
audio.src = src;

Chrome WebView 4.4.4

ended イベントの際のシークには audio.play() を呼んでおく。

iOS4

ended に達すると音が鳴らなくなる。(ended イベントは発しないので timeupdate イベント時に audio.currentTime === audio.duration で判断する)

ended イベントの際のシークには audio.play() で解決する。

シークの際に常に audio.play() を行う。

ちなみに シークの際の audio.play() は iOS8.3 でも必要な模様。

iOS6, iOS7.1

ended に達すると音が鳴らなくなる。(ended イベントは発しないので timeupdate イベント時に audio.currentTime === audio.duration で判断する)

解決せず…audio.play() で頻度が改善する模様。

ちなみに iOS7.1 iPhone 4s(シュミレータ)では、そのまま1分弱待つと再生を再開する。iOS8.1 iPhone 4s(シュミレータ)では遭遇していない。

iOS7.1 は pause -> ended となる場合と seeked -> pause -> progress -> stalled となる時がある。この時は duration が 0 になっている。

その他の問題

iOS8.3でtimeupdateのcurrentTimeのズレ

play(シーク)後に seeking と seekend に挟まれて発生する timeupdate イベントの際の currentTime の値が正しくない。

意図せず再生開始値より低い値になるのでループした等を誤検出しないように注意する。

参考

Web Audio

Qiita > Mobile Safari, Chrome for Android での Web Audio API 覚え書き

iOS 7.1 で decodeAudioData に処理が入った瞬間にスクリーンを長押しする(スクロールを繰り返す)と decoeAudioData の処理がキャンセルされることがある

因果日記+ >【javascript】モバイル向けブラウザでも音を鳴らしたい【WebAudio】

■Android標準ブラウザではWebAudiAPIは使えない
→使える機種もあるので厄介。(SH-02Eとか)

  1. 使い終わったインスタンスはload()しておくとやや安定
  2. (iOS)srcの差し替えはタッチイベントの中でやらなくていいので、(略)、GMの切り替えはsrcを代入しなおせばいい

おっぱいは正義。

HTML5Experts.jp > ネットワークのないところでも使え、サクサク動く。これからのWebゲームアプリが備えるべき機能とは「HTML5 Conference 2013」

・Chrome for Android31 までは HE-AAC が低速再生される不具合があった
・ver32 で fix 済み(【セッション映像】8:19)

HTML Audio

無職のプログラミング > HTML5 Audio オブジェクトを JavaScript で制御する方法

引数なしで new Audio() とすると、Operaでエラーになるそうなので注意。

Firefox3.6では再生終了後のpause()は無視されて、最初からの再生処理が実行されてしまう

Firefox3.6では一度も play() していない状態で currentTime = 0 を実行するとエラーになる。

GoogleChrome7 では currentTime = 0 直後に play() すると、pause()した位置前後の音が混ざることがある。

ケンタテクブロ > IE9 の HTML5 Audio について

IE9の動作

  1. JavaScriptでAudioオブジェクトを作ることができないので、Audioタグを使う必要がある
  2. (略)「clientaccesspolicy.xml」か「crossdomain.xml」が必要となる。
C# と VB.NET の質問掲示板 > IE9でHTML5 autio タグが無効になる

IE9でも動かす場合は、JavaScriptでAudioオブジェクトを作るのではなく、 HTML内にAudioタグを作成して、そのAudioに対してJavaScriptで操作

IEのバージョン9.0.8112.16421では、Audioオブジェクトのnewも対応してました。

とみぞーノート > HTML5 audioタグ ブラウザ間の違い

HTML5で使えるaudioタグをJavaScriptから扱う場合の、ブラウザ間での動作の違いのまとめ。とりあえず、autoplay,loopプロパティについて。

FireFox3.6, Android 2.3.6については、src変更後、load()を呼び出さないと切り替わらなかった。

AndroidのHTML Audioでerrorイベントが送出されないので、Androidのソースコードを調べてみた

Android 4.3までのブラウザで HTML Audio の error イベントが送出されない問題は、(少なくとも今回調べたAndroid 4.2では)特定端末の問題ではなくAndroid自体の問題であることがわかりました。また、error イベントの代替となる方法を見つけることもできました。

おわりに、モバイルオーディオは終わらない

DHTML の枯れてしまったことに比してモバイルブラウザのメディア周りはまだまだ悩みが尽きることはありません。

そのような背景もあり記事はとくに断り無く最新の調査結果を加筆修正していきます。Enjoy!