AndroidのHTML Audioでerrorイベントが送出されないので、Androidのソースコードを調べてみた
腕時計をする習慣が無いので、Apple Watchを買っても鞄の中に入れたままになりそうで買うのをためらっている、フロントエンジニアのらくさんです。いつものように Will it Blend? で粉にされてますが、これIon-XガラスのApple Watch Sportですね。サファイアガラスは粉にならなかったんでしょうか?
error イベントが送出されない
HTML Audio の src に指定したオーディオファイルが存在しない場合、error イベントが送出されるはずですが、どういうわけかAndroid 4.3までのブラウザ(Chromiumベースになる前のブラウザ)では送出されないようです。
1 2 3 4 5 6 7 8 9 10 11 |
[sourcecode lang="javascript"]<input type="button" value="load" onclick="loadAudio()"> <script type="text/javascript"> function loadAudio() { var audio = new Audio(); audio.addEventListener("error", function() { console.log("audio error"); }, false); audio.src = "notfound.m4a"; audio.load(); } </script>[/sourcecode] |
error イベントを受け取れないと、代替処理などをしたい場合には困ってしまいます。また、特定の端末のみで起きる問題なのかどうかを確認しなければならないのも頭の痛い点です。そこで、Androidのソースコードではどうなっているのかを追ってみることにしました。
Androidソースコード検索
Androidのソースコードは、次のサイトで検索することができます。
Androidソースコード検索サービス
ここにはAndroidのバージョン毎のリンクがあります。今回は手元にあった端末がAndroid4.2だったので http://tools.oesf.biz/android-4.2.0_r1.0/ で検索しました。
ここでは、全文検索(Full Search)、関数や変数等の定義(Definition)、関数や変数等の使用箇所(Symbol)、ファイルパス(File Path)、コミットログ(History)による検索ができます。
HTML Audio の load メソッドを探す
先ほどのソースコードの9行目、audio.load() の呼び出しから順に辿っていきます。まずは load メソッドを探します。
HTML Audio のクラス名は HTMLAudioElement ですが、load メソッドは親クラスの HTMLMediaElement で定義されているので、HTMLMediaElement を探します。これはクラス名なので、Definition に入力して検索するのがよいでしょう。
すると、HTMLMediaElement.cpp というファイルが見つかるので、このファイルを開いて load メソッドを探します。
ブラウザのページ内検索で load を検索語にしてファイル内を適当に検索すると、511行目に次の関数が見つかります。
最初の引数が isUserGesture という名前になっています。スマートフォンの HTML Audio はユーザーの操作によって呼び出されないと動作しないことから、どうやらこれが探しているもと考えてよいでしょう。
そこから深く掘り下げていく
次に、この関数から呼ばれている prepareForLoad を見てみます。prepareForLoad の呼び出し部分にハイパーリンクが貼られているので、クリックすると移動できます。
prepareForLoad の中身を眺めると m_player という変数が気になります。これは全くの勘なのですが、この m_player が指しているオブジェクトがオーディオ再生の実際の処理をしているのではないか? という気がします。今回のようにしてソースコードを追っていく場合は、このような勘の働かせ方も重要になってきます。
HTMLMediaElement.cpp 内で m_player を使っているところを見ていくと、739行目に次のような箇所が有りました。どうやらロードの実際の処理は、この先で行われていそうです。
では、m_player の型が何かを調べます。
29行目でヘッダファイル HTMLMediaElement.h をインクルードしています。ここもハイパーリンクになっているのでクリックすると、File Path に HTMLMediaElement.h が入った状態で検索されます。HTMLMediaElement.h が見つかるので開きます。
HTMLMediaElement.h の368行目に m_player の宣言がありました。どうやら m_player は MediaPlayer(のスマートポインタ)のようです。
さらに深く…
長くなるので、ここからしばらくは箇条書きにします。
- MediaPlayer を探す
- MediaPlayer::load を探す
- src のURLが m_url に代入されている
- m_url が使用されているところを見ていく
- m_private->load(m_url);
- m_private の型は? → MediaPlayerPrivateInterface (MediaPlayer.hの319行目)
- MediaPlayerPrivateInterfaceで検索 → MediaPlayerPrivateAndroid.cpp
- static const char* g_ProxyJavaClassAudio = “android/webkit/HTML5Audio”; とか jclass clazz = env->FindClass(g_ProxyJavaClassAudio); とか → android.webkit.HTML5Audio というJavaクラスがあるんだな…
- private MediaPlayer mMediaPlayer; → Android SDK の MediaPlayer か!
というわけで、Android SDK の MediaPlayer を使用していることが分かりました。ここで、HTML5Audio.java の onError を見てみます。
false を返しています。これは android.media.MediaPlayer.OnErrorListener の onError を実装したもので、リファレンスには次のように書かれています。
True if the method handled the error, false if it didn’t. Returning false, or not having an OnErrorListener at all, will cause the OnCompletionListener to be called.
エラーを処理した場合は true を、そうでない場合は false を返します。false を返すか、あるいは OnErrorListener を設定していない場合は OnCompletionListener が呼ばれます。
あっ…(察し)
結局エラーはここで握り潰され、false を返すことで OnCompletionListener が呼ばれて正常に処理されたことになっているようです。(160行目の resetMediaPlayer() を見ても、JavaScript側への error イベント送出に繋がるような処理はされてないように見えます。)
error イベントの代わりになる方法を探す
詳細は省きますが OnCompletionListener の149行目、nativeOnEnded の中をしばらく掘り進んでいくと、timeupdate イベントが送出されていることがわかります。
また、この timeupdate イベントが送られるときの currentTime と duration の値が次のように特徴的です。
- load() した場合
- currentTime: 1
- duration: NaN
- load() せず play() した場合
- currentTime: 1
- duration: 1
これの特徴を利用することで、エラーかどうかを判別することができます。
まとめ
Android 4.3までのブラウザで HTML Audio の error イベントが送出されない問題は、(少なくとも今回調べたAndroid 4.2では)特定端末の問題ではなくAndroid自体の問題であることがわかりました。また、error イベントの代替となる方法を見つけることもできました。
Androidのソースコードを調査して問題の原因と回避方法を見つけるには、C/C++やJavaの知識も多少必要になりますが、今回の調査では関数名や変数名を大雑把に検索しながら斜め読みする程度で問題の原因にたどり着くことができました。困ったときは駄目元で一度Androidのソースコードを調査してみてはいかがでしょうか。