2012-02-27

AndroidのWebViewで音声を再生する

Webページに音声を再生する仕組みが組み込まれている場合があるのですが、AndroidのWebViewは、そのままではそれを再生してくれません。何かプログラムを書いてやらなければいけないようです。

参考にすべきは、Androidの公式ドキュメントのこの記事でしょう。

音声を再生する場合、利用する主要なクラスはMediaPlayerとAudioManagerです。これらのクラスの機能を利用するために、マニフェストに記述しなければならないことは特になさそうですが、インターネット経由で音声ファイルにアクセスする場合や、音声の再生中にデバイスがスリープしてしまうのを防ぎたい場合は、それぞれINTERNETとWAKE_LOCKパーミッションを許可する必要があります。 ネット上の音声ファイルを再生するには、次のようにしてUriを指定します。

try {
    String url = "http://........"; // your URL here
    MediaPlayer mediaPlayer = new MediaPlayer();
    mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    mediaPlayer.setDataSource(url);
    mediaPlayer.prepare(); // might take long! (for buffering, etc)
    mediaPlayer.start();
} catch(IllegalArgumentException ex1) {
    // handle error
} catch (IOException ex2) {
    // handle error
}
prepare()メソッドは時間がかかる場合があるので、UIスレッドで呼んでユーザを待たせてはいけません。あまり待たせるとアプリケーションがハングしたとシステムに判断されてしまうこともあるようです。「never call it from your application's UI thread」と太字でドキュメントに書かれています。 しかし、必ずしも開発者が別スレッドを用意しなくていけないわけではなくて、代わりにprepareAsync()メソッドを呼ぶという手もあります。この場合は、MediaPlayerの準備が整った後で実行するべき処理をsetOnPreparedListener()で登録しておく必要があります。 MediaPlayerは状態管理を行っていて、準備が整わないのにいきなり音声を再生することはできません。次のような状態遷移図を頭に入れておけとのことです。

MediaPlayerはリソースを消費するので、使い終わったらちゃんと解放してやらなければなりません。ActivityにMediaPlayerを保持している場合は、onStop()が呼ばれたら解放しないとダメです。解放するときは、
this.mediaPlayer.release();
this.mediaPlayer = null;
という具合にreleaseした後でnullを代入します。


状態遷移図を見ると、一度setDataSource()を実行してInitializedされたMediaPlayerに対して、もう一度setDataSource()を実行することはできないようです。同じMediaPlayerは、同じデータを繰り返し再生できますが、別のデータを再生する場合は、新たにインスタンスをつくらないといけないということでしょう。
MediaPlayerをActivityではなくService利用する場合は、毎回解放する必要はないみたいですが、とりあえず今回はServiceを利用しないので関係のない話です。

後は、ちょっとプログラムをいじってちゃんと音声を再生できるか試してみます。
つぎのようなコードを書いてみました。わずか数秒の音声データの再生が目的なので、最後まで再生して終了というだけのシンプルなロジックです。これを、WebViewClientのonLoadResource(WebView view, String url) の中で実行しています。URLをみて音声データだと判定できた場合にのみ実行する処理です。レスポンスのMimeTypeをチェックできたら、もっといいんですが、それっぽいイベントが見つかりませんでした。
try {
    Log.d(TAG, "Initialize MediaPlayer");
    MediaPlayer mediaPlayer = new MediaPlayer(); //idle
    mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    mediaPlayer.setLooping(false);
    mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
        @Override
        public void onPrepared(MediaPlayer mp) { //prepared
            Log.d(TAG, "MediaPlayer prepared");
            mp.start();
        }
    });
    mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
        @Override
        public void onCompletion(MediaPlayer mp) { // playback completed
            Log.d(TAG, "MediaPlayer completed");
            mp.stop(); //stopped
            mp.release();
        }
    });
    mediaPlayer.setDataSource(url); // initialized
    mediaPlayer.prepareAsync(); //preparing
} 
catch (Exception ex) {
    Log.w(TAG, ex.toString(), ex);
}

軽くテストしてみたところ、いろいろと問題が発覚しました。
まず、Android1.6のエミュレータだと音声がループしてしまいます。ログを見たところonCompletionが呼ばれていないです。代わりに、「E/PlayerDriver(31): Invalid percentage value ####」というログがずっと出力され続けています。####の部分は数値で、この数値がだんだん増加していきます。
Android 2.1では、再生ができないです。「W/MediaPlayer(223): info/warning (1, 44) 」というログが出力されています。OnInfoListenerでは、MEDIA_INFO_UNKNOWN == 1が通知されます。
Android2.3だとループなく再生できますが、音声の最後が少しだけ途切れてしまいます。
上のコードでは、OnErrorListenerがセットされていませんが、onErrorListenerが定義されていなければonCompletionが呼ばれるということなので、これは問題ないはずです。
さらに調査していたら、ちゃんと再生できるデータとそうでないデータがあるということが分かりました。インターネット上のデータに依存する問題ということであきらめざる得ない部分も多そうです。リピートに関してはバックボタンで再生を中止できるようにしてお茶をにごし、再生できないデータについては潔くあきらめる、というあたりで手を打ちます。音声の最後が途切れるという問題は、ちょっとスリープしてからstop()を実行することで対処できるといえばできますが、こんなことでいいのかどうか考えものです。onCompletion()はUIスレッドらしく、その中でスリープするとその間ユーザの操作をアプリが受け付けなくなってしまうので、やるとしたら別スレッドを用意しなくてはいけません。

あと、複数のMediaPlayerを同時に起動できてしまうという問題にも対処しておいた方がよさそうですね。イベントハンドラの中でtry/catchすることも必要そうです。


0 件のコメント: