このエラーはChromeがバックグラウンドタブの映像のみ(音声なし)の動画を省電力のために一時停止したときに発生します。HTMLMediaElementのplay()はPromiseを返しますが、そのPromiseがresolveされる前にブラウザが再生を中断すると、AbortErrorでrejectされます。
ポイントは、これが「バグ」ではなくChromeの意図的な省電力挙動だということです。Chrome 66以降で導入されたAutoplay Policyの一部として、ユーザーが見ていないタブで電力を浪費する動画再生を抑制する設計になっています。
| 条件 | 詳細 |
|---|---|
| ブラウザ | Chrome(Chromium系ブラウザ全般) |
| メディア種別 | 映像のみ(video-only)。音声トラックがない、またはmuted状態 |
| タブの状態 | バックグラウンド(ユーザーが別タブに移動した状態) |
| 再生状態 | play()が呼ばれたが、Promiseがまだ解決していない |
根本原因を理解するには、HTMLMediaElement.play()がPromiseを返すようになった経緯を知る必要があります。
以前のplay()は戻り値がundefinedでした。Chrome 50でplay()がPromiseを返すようになり、再生の成功・失敗をハンドリングできるようになりました。
// Chrome 50以降: play()はPromiseを返す
const video = document.querySelector('video');
const playPromise = video.play();
// playPromise は Promise<void> | undefined
if (playPromise !== undefined) {
playPromise
.then(() => {
// 再生成功
})
.catch((error) => {
// 再生失敗(AbortError or NotAllowedError)
});
}
問題はplay()を呼んでからPromiseが解決するまでの間に、以下のいずれかが起きた場合です。
video.play() を呼び出し — Promiseが返されるpause()を実行AbortErrorでreject → コンソールにエラー表示もう一つの典型的なケースは、play()とpause()を短い間隔で連続して呼んだ場合です。
// play() の Promise が解決する前に pause() を呼ぶと AbortError
video.play(); // Promise が pending のまま...
video.pause(); // ← ここで中断が発生
Chrome 63以降、バックグラウンドタブの映像のみのメディアは自動的に一時停止されます。これはバッテリー消費を抑えるための仕様であり、disableする設定は提供されていません。音声トラックがあるメディアは、ユーザーが意図的に聞いている可能性があるため、この制限の対象外です。
最も基本的な対応です。play()がPromiseを返すことを前提に、catchでAbortErrorを適切に処理します。
async function safePlay(video) {
try {
await video.play();
} catch (error) {
if (error.name === 'AbortError') {
// 省電力による中断 — 通常は無視して問題ない
console.log('Playback was interrupted by the browser.');
} else if (error.name === 'NotAllowedError') {
// Autoplay が許可されていない — ユーザー操作が必要
showPlayButton();
} else {
throw error;
}
}
}
はい、多くのケースでは無視して安全です。ユーザーがタブを離れた(見ていない)状態で動画が停止しているだけなので、ユーザー体験には影響しません。タブに戻ったときにvisibilitychangeイベントで再生を再開すれば十分です。
play()のPromiseが解決する前にpause()を呼ぶとAbortErrorになります。Promiseの完了を待ってから次のアクションを実行する設計にします。
class VideoController {
#playPromise = null;
async play() {
this.#playPromise = this.video.play();
try {
await this.#playPromise;
} catch (e) {
if (e.name !== 'AbortError') throw e;
} finally {
this.#playPromise = null;
}
}
async pause() {
// play() の Promise が pending なら完了を待つ
if (this.#playPromise) {
try {
await this.#playPromise;
} catch {
// すでに中断されている場合は無視
}
}
this.video.pause();
}
}
バックグラウンドタブでの再生中断を前提とした設計にするなら、Page Visibility APIを使ってタブの表示状態に応じて再生を制御します。
function setupVisibilityHandler(video) {
document.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible') {
// タブがフォアグラウンドに戻った — 再生を再開
try {
await video.play();
} catch (e) {
if (e.name !== 'AbortError') throw e;
}
} else {
// タブがバックグラウンドに移った
// 映像のみの場合、Chromeが自動で停止するので
// 明示的にpause()しておくのも一つの手
video.pause();
}
});
}
ランディングページのヒーロー動画など、自動再生が必須のケースではmuted属性を活用します。muted動画はAutoplay Policyの制限が緩和されますが、バックグラウンドタブでの省電力停止は依然として発生します。
<!-- muted autoplay は多くのブラウザで許可される -->
<video id="hero-video" muted autoplay playsinline loop>
<source src="hero.mp4" type="video/mp4">
</video>
<script>
const video = document.getElementById('hero-video');
// autoplay が失敗した場合のフォールバック
video.play().catch(() => {
// 静止画フォールバック or 再生ボタン表示
video.poster = 'hero-poster.jpg';
});
</script>
ReactではuseRefとuseEffectで動画要素を管理するパターンが一般的です。クリーンアップでPromiseの競合を防ぎます。
function useVideoPlayer(src: string) {
const videoRef = useRef<HTMLVideoElement>(null);
const playPromiseRef = useRef<Promise<void> | null>(null);
const play = useCallback(async () => {
const video = videoRef.current;
if (!video) return;
try {
playPromiseRef.current = video.play();
await playPromiseRef.current;
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') {
// ブラウザによる中断 — 安全に無視
return;
}
throw error;
} finally {
playPromiseRef.current = null;
}
}, []);
const pause = useCallback(async () => {
if (playPromiseRef.current) {
try { await playPromiseRef.current; } catch {}
}
videoRef.current?.pause();
}, []);
return { videoRef, play, pause };
}
function useVideoPlayer() {
const videoEl = ref<HTMLVideoElement>();
let playPromise: Promise<void> | null = null;
async function play() {
if (!videoEl.value) return;
try {
playPromise = videoEl.value.play();
await playPromise;
} catch (e: any) {
if (e.name !== 'AbortError') throw e;
} finally {
playPromise = null;
}
}
async function pause() {
if (playPromise) {
try { await playPromise; } catch {}
}
videoEl.value?.pause();
}
return { videoEl, play, pause };
}
play()に関連するエラーは複数あり、混同しやすいので整理します。
| エラー名 | 原因 | 対処 |
|---|---|---|
AbortError(background media paused) |
バックグラウンドタブで省電力のため中断 | catchで無視 + visibilitychangeで再開 |
AbortError(play/pause race) |
play()のPromise解決前にpause()を呼んだ |
Promise chainで順序を保証 |
NotAllowedError |
Autoplay Policyによりユーザー操作なしの再生が拒否 | muted autoplayまたはユーザー操作トリガー |
NotSupportedError |
メディア形式がブラウザでサポートされていない | 対応フォーマット(MP4/WebM)を使用 |
Chrome DevToolsのMediaパネル(Chrome 91+)で、動画要素の再生状態、エラー、イベントの時系列を確認できます。
F12)テスト時にこのエラーを再現するには以下の手順で確認できます。
<video>を含むページを開くvideo.play()を実行Ctrl+Tab)chrome://media-internals にアクセスすると、すべてのタブのメディアプレイヤーの詳細な内部状態(パイプラインの状態、デコーダ情報、イベントログ)を確認できます。本番環境での調査時に有用です。
このエラーへの対応を一言でまとめると、play()がPromiseを返すことを前提にコードを書くということです。
play()の戻り値を常にcatchする — AbortErrorは安全に無視できるplay()とpause()の競合をPromise chainで防ぐ — 再生中にpauseを呼ばないvisibilitychangeでフォアグラウンド復帰時に再開する