【JavaScript】Safari だけじゃないオーディオ再生の制約と再生開始遅延の解決方法

オーディオの自動再生に大きな制約があるといえば、以前は iOS Safari に限った話でした。しかし、現在は PC も含め、あらゆるブラウザーでオーディオの自動再生に大きな制約があります。もし再生できたとしても、次に立ちはだかるのが再生開始遅延です。とりわけスマートフォンでタップ音や通知音を再生したい場合は違和感を感じてしまいます。

今回は、HTML マークアップおよび JavaScript によるオーディオ自動再生にどのような制約があるのか、その制約をどう解決するのか、そして、再生開始遅延を解消する方法について、詳しく見ていきます。

Chrome の自動再生ポリシー

オーディオの自動再生ができないという制約は、当初は iOS Safari に限った話でした。ところが、Google が自動再生ポリシーを発表してから、メジャーブラウザーすべてがそれに従うようになりました。英語ですが「Autoplay policy in Chrome」という記事に詳細が書かれていますので、興味がある人は読むと良いでしょう。

以前よりゲーム業界では大きな話題になりました。しかしほとんどのウェブ開発者はゲーム業界にいるわけではありませんので、他人事だと思っていた人も多いことでしょう。しかし、近年はウェブアプリの開発が増え、ゲームほど派手ではありませんが、タップ音や通知音を再生したいというニーズが増えているのは事実ではないでしょうか。

audio 要素におけるオーディオ再生の制約とは

次のマークアップをご覧ください。

<audio src="sound.mp3" autoplay></audio>

以前は iOS Safari では自動的にオーディオが再生されませんでした。しかし、とりわけ PC では問題がなかったため、スマートフォン向けでない場合は、あまり気にすることもなかったと言えます。しかし、現在は、PC 向けであっても、メジャーなブラウザーでは自動的にオーディオが再生されません。

これは audio 要素に限らず video 要素でも同様です。

<video src="video.mp4" autoplay></video>

これを自動再生させるためには、PC であればミュートする必要があります。つまり無音なら自動再生できるということです。

<video src="video.mp4" autoplay muted></video>

audio 要素の場合、muted 属性をマークアップしたら意味がありませんね。

JavaScript による自動再生の制約

audio 要素の autoplay 属性の制約と同様に、JavaScript によるオーディオの自動再生も禁止されています。

const audio = new Audio('sound.mp3');
audio.play();

自動再生を目的に上記のコードは再生されないはずです。では、どうすれば再生するのかというと、クリックやタップといったユーザーのアクションをトリガーにする必要があります。

<button id="like-btn">いいね</button>
const audio = new Audio('sound.mp3');

document.getElementById('like-btn').addEventListener('click', () => {
  audio.play();
});

ここではユーザーのクリックやタップを受け取るために button 要素を用意しましたが、どんな要素でも構いません。極端な例を挙げれば、body 要素でも機能します。もちろん、iOS Safari でも機能します。

const audio = new Audio('sound.mp3');

document.body.addEventListener('click', () => {
  audio.play();
});

このコードでは、body 要素をクリックすれば、何度でもオーディオが再生されると期待するでしょう。PC 版のブラウザーなら期待通りに動作します。ところが、iOS Safari では最初の一回しか再生されません。これを解決するには、Audio オブジェクトの生成もクリックをトリガーに行います。

document.body.addEventListener('click', () => {
  const audio = new Audio('sound.mp3');
  audio.play();
});

オーディオの元となる Audio オブジェクトがいつ作られたのかがポイントになります。ユーザーアクションのお墨付きを得られた Audio オブジェクトは晴れて再生可能なオブジェクトになります。

ユーザーのアクションなしにオーディオを再生する方法

ユーザーのクリックやタップが必要となると、自分がやりたかったことは無理だと諦めるのはまだ早いかもしれません。

ユーザーのクリックやタップが必要なことに変わりはありませんが、必ずしも、クリックやタップした瞬間に再生する必要はないからです。ユーザーのアクションさえ得られれば、オーディオの再生を後にすることができるからです。次のコードをご覧ください。

let audio = null;
document.body.addEventListener('click', () => {
  audio = new Audio('sound.mp3');
});

setInterval(() => {
  if(audio) {
    audio.play();
  }
}, 5000);

このコードでは、クリックをトリガーに Audio オブジェクトを生成しています。そして、クリックとは関係なしに何度でもそのオーディオを再生することができます。このように、ユーザーのアクションをトリガーに生成された Audio オブジェクトは、それ以降であれば、いつでも何度でも自由に再生できるようになります。

上記のコードは PC 版のブラウザーと Android の Chrome と Firefox で期待通りに動作します。しかし iOS Safari では、最初の 1 回目しか再生されません。残念ながら、筆者が知る限り、iOS Safari ではこれが限界です。1 再生ごとに 1 タップが必要になります。

とはいえ、このような制約の中でも、役に立つケースはあります。たとえば何かしらのイベントをキャッチしたらオーディオを再生してユーザーに通知したいとしましょう。通知を行ったら、ユーザーに何かしらタップするよう、ウェブアプリケーション側で誘導するのはどうでしょう。もしユーザーがタップしたら、次の通知のために Audio オブジェクトを作っておいて、次の通知まで待機させるのです。

もちろん、iOS Safari ユーザーが期待通りにタップしてくれるとは限りませんが、アプリケーションの設計次第で違和感のないオーディオ再生が実現できるはずです。

意外に気になる再生開始遅延

PC の場合、小さなオーディオファイルの再生であれば、再生開始遅延は気にならないでしょう。ところが、スマートフォンの場合、状況によってはとても遅延が気になります。再掲しますが、次のコードを iOS Safari で試すと、再生開始遅延を感じます。

document.body.addEventListener('click', () => {
  const audio = new Audio('sound.mp3');
  audio.play();
});

遅延の理由は、クリック後に Audio オブジェクトを生成しているからです。かといって、前述の通り、事前に Audio オブジェクトを生成してしまうと、iOS Safari では初回しか再生されない問題が発生してしまいます。

Web Audio API で解決

これを解決するには、Web Audio API を使います。Audio オブジェクトを使う方法と比べて、かなり複雑になりますが、iOS Safari でも遅延を最小限に抑えられます。一気にコードをご覧ください。

const audio_ctx = new AudioContext();
let audio_buffer = null;
let audio_buffer_node = null;

(async () => {
  // fetch で sound.mp3 ファイルをダウンロードして ArrayBuffer を取得
  const response = await fetch('sound.mp3')
  const response_buffer = await response.arrayBuffer();

  // ArrayBuffer をデコードして AudioBuffer オブジェクトを取得
  audio_buffer = await audio_ctx.decodeAudioData(response_buffer);
  // 初回再生のために準備を行う
  prepareAudioBufferNode();

  // body をクリックしたときの処理を定義
  document.body.addEventListener('click', () => {
    // オーディオを再生
    audio_buffer_node.start(0);
    // 次回の再生のために準備を行う
    prepareAudioBufferNode();
  });
})();

// 音源再生のために準備を行う
function prepareAudioBufferNode() {
  audio_buffer_node = audio_ctx.createBufferSource();
  audio_buffer_node.buffer = audio_buffer;
  audio_buffer_node.connect(audio_ctx.destination);
}

ポイントとしては、fetch を使って音源ファイルをダウンロードして ArrayBuffer を取得します。そして、その音源を Web Audio API で再生するまでの準備を事前に行っておきます。

body がクリックされたらオーディオの再生を行い、その直後に、次のクリックに備えて、また再生の準備を事前に行っておくのです。こうすることで、iOS Safari でも再生開始遅延がかなり低減されます。

事前準備といっても、オーディオファイルのダウンロードもデコードも、最初に一回行うだけです。この点が前述の Audio オブジェクトを使う方法と違う点です。

まとめ

昔と比べてオーディオの自動再生は難しくなりました。この記事で紹介してきた通り、オーディオの自動再生をかつてのように解決する方法はありません。

利用者の立場になると、自動再生の制約は正しい方向です。かつて、ページを表示したとたんに大きな広告が表示され、かつ、動画の自動再生とともに宣伝文句の音声が流れた時代もありました。いきなり音声が流れ始めるとビックリしてしまいます。また、アクセスしたページの内容によっては、そういった音声付きの広告がいきなり流れることで、いろんな意味でビクッとした人もいるのではないでしょうか。

そういった自動再生の乱用の禁止は、健全なウェブサーフィンを実現するためにも必要不可欠なのでしょう。そういった制約を受け入れ、その制約の中で如何に利用者に違和感のないウェブアプリを設計できるのかが、エンジニアの腕の見せ所なのでしょう。

制約の抜け道を探すハックの技術力ではなく、制約を受け入れた設計力や提案力(受託開発の場合)が重要だと、この記事を書きながら考えさせられました。多くの場合、抜け道のハックが見つかったとしても、永遠に使える技術ではありませんしね。

今回は以上で終わりです。最後まで読んでくださりありがとうございました。それでは次回の記事までごきげんよう。

Share