iOS Safariの音声再生における制限とその回避策の検証
iOS Safari の音声周りで不可解な現象に遭遇したため、調査を行った際の内容をまとめる。 なお、その現状自体は Safari のバグであることが分かり無駄足になった。
注意
この記事は、2024 年 8 月に調査した内容を元に書かれている。 2023 年に書かれた記事でも再現性が確認出来なかったため、この記事も同様に数ヶ月後には再現性が確認出来ないものになる可能性がある。
また調査は不十分であり間違いが多分に含まれている可能性があるため、自身でも確認することをお勧めする。 なお、このページには実際に動作を確認できる要素を埋め込んでいるため、そのまま動作を確認できる。
ブラウザにおける音声再生の制限
Audio 要素や Video 要素を用いて音声を自動再生する場合、ブラウザによる制限が掛かる。
<audio src="sample.mp3" autoplay></audio>
この制限は Autoplay policy と呼ばれるポリシーによるが、実質 JavaScript による音声の再生そのものへの制限にもなっている。 JavaScript つまり Web Audio API や Web Video API が自由に使えれば、自動再生が出来てしまうので妥当な制限と言える。
const audio = new Audio("sample.mp3");audio.play();
従って、ブラウザで音声を扱おうとする場合、Autoplay policy への理解はほぼ必須になっている。
以降、Web Audio API 及び audio 要素などを AudioContext と総称する。この記事における AudioContext は Web Audio API の AudioContext オブジェクトのみを指すものではない。
一般的な制限
Safari ほどではないものの、現在は Chrome のAutoplay policy in Chrome相当の制限がどのブラウザでも適用されている。
条件を満たさなければ自動再生することは出来ない
次のいずれかを満たさない場合、音声を自動的に再生することは出来ない。
- 音声がミュートされている。
- ユーザーがクリックやタップなどを行っている。
- ユーザーがモバイルでサイトをホーム画面に追加するか、PWA をインストールしている。
- ユーザーがデスクトップにおいて、そのサイトで今までに十分音声を再生している。
- これはメディアエンゲージメント指数(MEI)で測定される。
iOS Safari における制限
iOS Safari における制限に付いて詳細にまとめられている公式のドキュメントがないのか、調べると経験則のみに基づいた情報が多く見つかる。
実際、少し調べただけでは 2017 年に書かれたAuto-play policy changes for macOS程度しか公式の情報は見つからなかった。 だが、これは iOS に限定した話でもなければ、ここに書かれている制限は macOS Safari では既に適用されなくなっているものが含まれているように思う。現在では macOS Safari でも、Chrome などとほぼ同じ規則が適用されているように思われる。
iOS Safari における制限や回避策の検証
iOS Safari の音声再生に関する制限について、不確かな情報がとても多かったため、実際に試すことにした。 以下に検証した内容をまとめる。
AudioContext はユーザーのクリックやタップ毎に生成する必要がある → 再現なし
iOS Safari では、クリックやタップをトリガーにして Audio オブジェクトを生成しなければ、最初の 1 回しか表示されないという記述があったが、再現しなかった。何度押しても音声を再生することが出来る。
const audio = new Audio("/sample0.mp3");
document.getElementById("a1").addEventListener("click", () => { audio.currentTime = 0; audio.play();});
クリックやタップ 1 回に付き 1 回しか音声を再生できない → 再現なし
iOS Safari では、クリックやタップ 1 回に付き 1 回しか音声を再生できないという記述があったが、再現しなかった。一度のクリックで何度も音声を再生することが出来た。
let audio = new Audio("/sample0.mp3");document.getElementById("a2").addEventListener("click", () => { const audio1 = new Audio("/sample0.mp3");}window.setInterval(() => { if (audio1) { audio1.currentTime = 0; audio1.play(); }}, 1000);
非同期処理を利用すると音声が再生されなくなる → 再現なし
非同期処理を挟むだけでクリックなどを行っても音声が再生されなくなるという記述があったが、再現しなかった。
document.getElementById("a3").addEventListener("click", async (event) => { const btn = event.currentTarget; btn.classList.add("loading"); await sleep(4000); const audio1 = new Audio("/sample0.mp3"); audio1.play(); btn.classList.remove("loading");});
仕様が変わったわけでなければ、これは非同期処理であることが悪いのではなく、クリックやタップを行ってから時間が経過しすぎていたことが原因ではないかと次のセクションの内容から推測できる。
クリックやタップから 5 秒程度経過してから AudioContext を生成すると再生できない → 再現あり
これは記述を見つけたわけではないが、非同期処理の確認をしている際に発生を確認した。 macOS Safari では発生せず、iOS Safari でのみ再現した。
document.getElementById("a4").addEventListener("click", async (event) => { const btn = event.currentTarget; btn.classList.add("loading"); await sleep(5000); const audio1 = new Audio("/sample0.mp3"); audio1.play(); btn.classList.remove("loading");});
クリックやタップをされてすぐに AudioContext を生成すれば時間が経過しても再生できる → 再現あり
見つけた記述では、クリックやタップをされてすぐに AudioContext を生成して再生 → 停止を行えば、その後時間が経過しても再生できるとあった。
document.getElementById("a5").addEventListener("click", () => { const audio = new Audio("sample.mp3"); audio.play().catch(() => { // playを待たずにpauseを呼ぶため、Abort Errorが発生する。 // それをcatchを呼ぶことで握り潰している。 }); audio.pause();});
これは正しいが、再生 → 停止を行わなくても AudioContext を生成するだけで問題はない。
document.getElementById("a5").addEventListener("click", async (event) => { const audio = new Audio("/sample0.mp3"); const btn = event.currentTarget; btn.classList.add("loading"); await sleep(10000); audio.play(); btn.classList.remove("loading");});