iPhone「低電力モード」で再生ボタンを出さずに背景動画を実現するNext.jsで使えるReact実装
はじめに
こんにちは、株式会社TERASSのかんです。
Webサイトで動画を表示する際、最も厄介なのがブラウザやデバイス設定による自動再生のブロックです。特にiPhoneの「低電力モード」が有効になっている場合、ユーザーの操作(再生ボタンのタップ)なしに動画を再生することはできません。
本記事では、この課題に対し、再生ボタンを一切表示せず、再生がブロックされたら静止画へシームレスに切り替える、モダンなReactコンポーネントの実装方法を解説します。
自動再生ブロックを検知する核心ロジック
この実装の鍵は、HTMLMediaElement.play()が返すPromiseを利用し、再生の成否を正確に検知すること、そして、再生を試みる前に動画の準備が整っているかを確認することです。
コンポーネントでは、動画の再生状態をplaybackStatusというStateで管理します。
| playbackStatus | 意味 | 動作 |
|---|---|---|
| pending | 初期状態/ロード中 | 動画は非表示 |
| playing | 再生成功 | 動画を表示 |
| blocked | 再生失敗(ブロック) | 静止画を表示 |
// ----------------------- 一部省略 ----------------------------
const [playbackStatus, setPlaybackStatus] = useState<
"pending" | "playing" | "blocked"
>("pending")
const videoRef = useRef<HTMLVideoElement>(null)
useEffect(() => {
const detectVideo = videoRef.current
if (!detectVideo) return
const tryPlay = () => {
detectVideo
.play()
// Promiseが解決: 再生に成功
.then(() => setPlaybackStatus("playing"))
// Promiseが拒否: 再生ブロックを検知し、"blocked" に設定
.catch(() => setPlaybackStatus("blocked"));
}
// 動画のデータが準備できてから再生を試みるためのロジック
if (detectVideo.readyState >= 2) {
// 状態が 2 (HAVE_CURRENT_DATA) 以上なら、すぐに再生を試みる
tryPlay()
} else {
// データが不十分なら、イベントを待機
const handleCanPlay = () => {
tryPlay()
// 再生試行後、イベントリスナーを解除して重複実行を防ぐ
detectVideo.removeEventListener("canplay", handleCanPlay)
detectVideo.removeEventListener("loadedmetadata", handleCanPlay)
}
detectVideo.addEventListener("canplay", handleCanPlay)
detectVideo.addEventListener("loadedmetadata", handleCanPlay)
}
}, [videoSrc])
useEffect(() => {
if (playbackStatus !== "playing") return
videoRef.current?.play().catch(() => {
setPlaybackStatus("blocked")
})
}, [playbackStatus, videoSrc])
再生準備を確実にするための3要素
コードの後半では、tryPlay()を実行する前に、動画のデータがブラウザに読み込まれているかを確認しています。これには、readyState、canplay、loadedmetadataの3つが密接に関わっています。
readyState(動画の読み込み状況)
readyStateは、ブラウザがメディアファイル(動画)のどの程度までデータを読み込んでいるかを示す数値プロパティです。
video.play()を実行する際、データが全く読み込まれていない状態だと、ブラウザはエラーを返す可能性があります。それを防ぐため、readyStateが特定の数値に達するのを待ちます。
| readyStateの値 | 定数名 | 意味 |
|---|---|---|
| 0 | HAVE_NOTHING | データなし。ロード開始前。 |
| 1 | HAVE_METADATA | メタデータ(長さ、サイズなど)のみ利用可能。 |
| 2 | HAVE_CURRENT_DATA | 現在のフレームのデータまで利用可能。再生を開始できる。 |
| 3 | HAVE_FUTURE_DATA | 続けて再生できるだけのデータがバッファされている。 |
| 4 | HAVE_ENOUGH_DATA | 最後まで再生できると判断できる十分なデータがある。 |
コード内の if (detectVideo.readyState >= 2) は、最低限、動画の現在のフレームのデータが読み込まれており、再生を開始できる状態になってからtryPlay()を実行するための安全策です。まだデータがない場合は、canplayやloadedmetadataといったイベントを待ってから再生を試みます。
loadedmetadata イベント
動画ファイルからメタデータ(ファイルの長さ、サイズ、トラック情報など)の読み込みが完了したときに発火します。
このイベントは、動画が「存在し、サイズや時間がわかる」状態になったことを示しますが、まだ再生に必要なデータは揃っていない可能性があります。このタイミングでplay()を試みることもできますが、より確実なのは次のcanplayイベントを待つことです。
canplay イベント
動画の再生に最低限必要なデータがバッファされたときに発火します。
readyStateが2(HAVE_CURRENT_DATA)に達したことを示唆します。このイベントが発生した時点が、play()メソッドを呼び出すのに最も確実で適切なタイミングとされています。
なぜ両方のイベントを待つのか?
動画のデータロードはブラウザやネットワーク状況によって非同期で進みます。
loadedmetadataやcanplayのどちらのイベントが先に発火するかは保証されません。そのため、両方を待ち受けることで、どちらが来てもすぐにtryPlay()を実行し、イベントリスナーを解除するという、堅牢な処理を実現しています。
JSXによる最終的な表示制御
return (
<video
ref={videoRef}
// "playing"時のみ動画を可視化するCSSクラスを適用
className={`${styles.video} ${playbackStatus === "playing" ? styles.videoVisible : ""}`}
key={videoSrc} // ソース変更時に動画を再マウントし、確実なリロードを促す
src={videoSrc || undefined}
muted // 必須:モバイルでの自動再生を許可
playsInline // 必須:iOS Safariでインライン再生を許可
controls={false} // 最重要:再生ボタンを表示しない
>
お使いのブラウザは動画をサポートしていません。
</video>
{/* 再生失敗時("blocked")にのみ静止画を表示 */}
{playbackStatus === "blocked" && (
<div
style={{
backgroundImage: // 静止画指定
}}
aria-label="動画再生不可のため代替画像"
/>
)}
</Link>
</section>
)
この実装により、ユーザーが低電力モードであっても、動画が強制されることなく、静止画が表示されます。再生ボタンを表示しないという要件もcontrols={false}と"blocked"時の静止画フォールバックで満たされています。
まとめ
本記事で解説した実装は、単に動画を再生するだけでなく、ブラウザとデバイスの設定に柔軟に対応する現代的なWeb開発のベストプラクティスです。
このロジックを採用することで、以下のメリットが得られます。
-
再生ボタンの排除: controls={false}とplaybackStatusによる制御により、再生ボタンを一切表示しないというデザイン要件を達成しました。
-
確実なフォールバック: video.play().catch()と"blocked"状態の組み合わせにより、iPhoneの低電力モードによるブロックを含め、再生失敗を正確に検知し、静止画へ自動で切り替えます。
-
パフォーマンスと安定性: readyStateやcanplay/loadedmetadataイベントを待つことで、動画が完全に準備できてから再生を試みるため、不要なエラーやちらつきを防ぎます。
この堅牢なコンポーネントを導入することで、ユーザーがどの環境からアクセスしても、動画再生ボタンが表示されることなく意図通りの画が提供できるでしょう。ぜひご自身のNext.jsプロジェクトで活用してみてください。
Discussion