2015年4月12日日曜日

audio要素のplayイベント、endedイベント、readyStateのおかしな動きに対応す るための足掻き

※この記事の筆者は趣味レベルでしかjavascriptをいじっていないので、以下にある方法は最適解ではないと思う。もっと良い方法があれば教えてほしいです。

html5のaudioを使って音声を再生する際、playイベントやendedイベントがうまく発生しなかったり、Android版ChromeにおいてreadyStateが正常に判断されないことがあって困った。

play、endedイベントのバグだが、コントローラーを使ってる時はいいのかもしれないが、別の要素のクリックイベントから.play()した場合、IE(10, 11で確認)やAndroid版Chromeで発生する。

まず、IEでは2度目の再生時にはplayイベントが発生しないらしい。また、IEやAndroid版Chromeではたまにendedイベントが発生しないことがある。endedイベントが発生しない原因はよくわからない。わたしが作っているサイトでは、ひとつのページに複数のaudioを動的に生成しているので、その弊害なのかもしれない。

とりあえずそういうバグ(ブラウザの仕様の違い?)をある程度は解消できる処置の例を以下に。

playイベントが発生しないことがある問題

再生開始をtimeupdateで監視する。timeupdateイベントは再生位置の変更時に発生する。音声の再生位置をcurrentTimeで取得すると1/1000秒の位まで表示されるのだが、timeupdateイベントはだいたい1/4秒毎に発生するっぽい(PC版Chrome、PC版Firefox31、IE11で確認)。

ちなみに再生直後、最初のtimeupdateイベント発生時のcurrentTimeは、IE11では0、Chromeではだいたい0.16~0.198の間、Firefoxではだいたい0.03だった。
なので、このようにイベントを設定する。

audio.addEventListener("timeupdate", function(){
if(Math.floor(this.currentTime*10)<=1 && !this.paused){
playStart(this); //再生開始時の処理
}
}, false);

jQueryを使うなら、

$("audio").bind("timeupdate", function(){
if(Math.floor(this.currentTime*10)<=1 && !this.paused){
playStart(this); //再生開始時の処理
}
});

これで再生開始判定ができる。
「Math.floor(this.currentTime*10)<=1(現在の再生位置の1/10の位が1以下か否か)」で最初のtimeupdateであるかどうかを判定している。

これだけでも一応は動くのだが、「!this.paused(一時停止中でない)」も判断している。たとえば再生終了時処理などで音声停止時に「audio.currentTime = 0(再生位置を冒頭に戻す)」をすると、その時にもtimeupdateが発生する。それを弾くためだ。

わたしの場合はこれだけでちゃんと判定できたが、ループ再生する場合などはもっと条件を加えなければならないだろう。

endedイベントが発生しないことがある問題

これは保険をかけておくことで解消した。とりあえずendedイベントに終了時処理を設定しておいて、再生開始時処理にもaudio.duration(+遊び)秒後に終了時処理を行うようにしておくのだ。

再生開始時の処理を

function playStart(playingAudio){
endTimer = setTimeout(
function(){
playEnd(playingAudio); //再生終了時の処理
},
playingAudio.duration * 1100
);
}

などのように設定する。

setTimeoutの時間設定はミリ秒単位、audio.durationで取得される音声再生時間は秒単位なので1000倍しないといけない。また、この処理は保険なので、endedイベントが問題なく発生すれば必要ない。よってタイマーは「再生時間*1000*1.1 ミリ秒後」に設定している。

そしてendedイベントは次のように設定する。

audio.addEventListener("ended", function(){
clearTimeout(endTimer);
playEnd(this); //再生終了時の処理
}, false);

jQueryを使うなら、

$("audio").bind("ended", function(){
clearTimeout(endTimer);
playEnd(this); //再生終了時の処理
});

clearTimeoutで保険のタイマーを解除するのを忘れずに。

audio.readyStateがまともに判定されない問題

PCであればpreloadされたり、audio.play()すれば読み込みもしてくれるので問題ないのだけど、スマフォやタブレットではそうも行かない。スマフォ、タブレットではユーザーの行動を伴わなければ音声データの読み込みが行われないのだ(そのへんの挙動はこのページに詳しい)。

そこで、読み込みが完了していない音声についてはclickイベント時にaudio.load();を行わねばならぬのだが、読み込みが完了したか否かを教えてくれるはずのaudio.readyStateが嘘をつく事がある……。

例えば、clickイベントに以下のような設定をすれば、「読み込まれていない音声は読み込んでから再生する」ことができるはずなのだ。

if(audio.readyState != 4){
audio.load();
}
audio.play();

しかしAndroid4.1.1, Chrome 28.0の環境で、読み込まれてもいないのにaudio.readyStateが「4(読み込み完了)」を返してきやがった。当然ifの中は実行されず、読み込みはされず、読み込まれていないので再生もできない。

そこで、「読み込まれていないならaudio.durationがNaNになるはずだから、isNaN(audio.duration)で判定すればいいんじゃないか?」と思ったのだが、それでもダメだった。読み込みが完了していない音声のdurationが「100」を返したのだ!

しかし、読み込まれていない音声は一律で100を返してくれるので、まだ親切だ。それで判定すれば良い。

次のように変えよう。

var adD = audio.duration;
if(isNaN(adD)||adD==100){
audio.load();
}
audio.play();

極稀に音声の長さがきっかり100秒というデータもあり得るだろうが、そんなのは無視していい例外だろう。また、もしそういうのがあったとしても、再生のたびにロードされるだけなのでそこまで問題はない。

あと、Androidデフォルトのブラウザだとさらによくわからないことになっているらしく、これでも読み込んでくれないことがある模様。今後の課題かなあ。

0 件のコメント:

コメントを投稿