sue@blog ~ /posts/video-play-request-interrupted
$ cd ../
$ cat post.metadata

AbortError: play()リクエストが中断される
問題の原因と対処法

date: 2026-04-04 JavaScript Chrome Web API Video
Webアプリで動画を自動再生しようとすると突然コンソールに現れる「AbortError: The play() request was interrupted」。Chrome特有の省電力機能とHTMLMediaElementのPromiseベースAPIが絡み合うこのエラーの発生メカニズム、再現条件、実装パターン別の解決策を整理する。

エラーの正体

$ cat error.log
AbortError: The play() request was interrupted because video-only background media was paused to save power. https://goo.gl/LdLk22

このエラーはChromeがバックグラウンドタブの映像のみ(音声なし)の動画を省電力のために一時停止したときに発生します。HTMLMediaElementのplay()はPromiseを返しますが、そのPromiseがresolveされる前にブラウザが再生を中断すると、AbortErrorでrejectされます。

ポイントは、これが「バグ」ではなくChromeの意図的な省電力挙動だということです。Chrome 66以降で導入されたAutoplay Policyの一部として、ユーザーが見ていないタブで電力を浪費する動画再生を抑制する設計になっています。

発生条件の整理

条件 詳細
ブラウザ Chrome(Chromium系ブラウザ全般)
メディア種別 映像のみ(video-only)。音声トラックがない、またはmuted状態
タブの状態 バックグラウンド(ユーザーが別タブに移動した状態)
再生状態 play()が呼ばれたが、Promiseがまだ解決していない

なぜ起きるのか — play() の非同期性

$ cat mechanism.md

根本原因を理解するには、HTMLMediaElement.play()がPromiseを返すようになった経緯を知る必要があります。

play() の歴史

以前のplay()は戻り値がundefinedでした。Chrome 50でplay()がPromiseを返すようになり、再生の成功・失敗をハンドリングできるようになりました。

JavaScriptplay() returns 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が解決するまでの間に、以下のいずれかが起きた場合です。

T0
video.play() を呼び出し — Promiseが返される
T1
ブラウザが再生準備を開始(デコーダ初期化、バッファリング)
T2 (中断)
ユーザーがタブを切り替え → Chromeが省電力でpause()を実行
T3
PromiseがAbortErrorでreject → コンソールにエラー表示

もう一つの典型的なケースは、play()pause()を短い間隔で連続して呼んだ場合です。

JavaScriptrace condition
// play() の Promise が解決する前に pause() を呼ぶと AbortError
video.play(); // Promise が pending のまま...
video.pause(); // ← ここで中断が発生
Chromeの省電力ポリシー

Chrome 63以降、バックグラウンドタブの映像のみのメディアは自動的に一時停止されます。これはバッテリー消費を抑えるための仕様であり、disableする設定は提供されていません。音声トラックがあるメディアは、ユーザーが意図的に聞いている可能性があるため、この制限の対象外です。


解決パターン

$ cat solutions.md

パターン1: play() の Promise を正しくハンドリングする

最も基本的な対応です。play()がPromiseを返すことを前提に、catchAbortErrorを適切に処理します。

JavaScript基本パターン
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;
    }
  }
}
AbortError は本当に無視していいのか

はい、多くのケースでは無視して安全です。ユーザーがタブを離れた(見ていない)状態で動画が停止しているだけなので、ユーザー体験には影響しません。タブに戻ったときにvisibilitychangeイベントで再生を再開すれば十分です。

パターン2: play() と pause() の競合を防ぐ

play()のPromiseが解決する前にpause()を呼ぶとAbortErrorになります。Promiseの完了を待ってから次のアクションを実行する設計にします。

JavaScript競合防止パターン
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();
  }
}

パターン3: Page Visibility API と連携する

バックグラウンドタブでの再生中断を前提とした設計にするなら、Page Visibility APIを使ってタブの表示状態に応じて再生を制御します。

JavaScriptPage 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();
    }
  });
}

パターン4: muted autoplay + ユーザー操作でunmute

ランディングページのヒーロー動画など、自動再生が必須のケースではmuted属性を活用します。muted動画はAutoplay Policyの制限が緩和されますが、バックグラウンドタブでの省電力停止は依然として発生します。

HTML + JavaScriptmuted autoplay
<!-- 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>

フレームワーク別の対応

$ cat frameworks.md

React

ReactではuseRefuseEffectで動画要素を管理するパターンが一般的です。クリーンアップでPromiseの競合を防ぎます。

React (TypeScript)useVideoPlayer.ts
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 };
}

Vue 3

Vue 3 (Composition API)useVideoPlayer.ts
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 };
}

関連するエラーとの違い

$ diff errors.md

play()に関連するエラーは複数あり、混同しやすいので整理します。

エラー名 原因 対処
AbortError
(background media paused)
バックグラウンドタブで省電力のため中断 catchで無視 + visibilitychangeで再開
AbortError
(play/pause race)
play()のPromise解決前にpause()を呼んだ Promise chainで順序を保証
NotAllowedError Autoplay Policyによりユーザー操作なしの再生が拒否 muted autoplayまたはユーザー操作トリガー
NotSupportedError メディア形式がブラウザでサポートされていない 対応フォーマット(MP4/WebM)を使用

デバッグのポイント

$ cat debug-tips.md

Chrome DevToolsでの確認

Chrome DevToolsのMediaパネル(Chrome 91+)で、動画要素の再生状態、エラー、イベントの時系列を確認できます。

  1. DevTools を開く(F12
  2. 三点メニュー → More toolsMedia
  3. 動画を再生し、Player PropertiesPlayer Events を確認

再現方法

テスト時にこのエラーを再現するには以下の手順で確認できます。

  1. muted状態の<video>を含むページを開く
  2. video.play()を実行
  3. すぐに別タブに切り替える(Ctrl+Tab
  4. 元のタブに戻り、コンソールを確認
chrome://media-internals

chrome://media-internals にアクセスすると、すべてのタブのメディアプレイヤーの詳細な内部状態(パイプラインの状態、デコーダ情報、イベントログ)を確認できます。本番環境での調査時に有用です。


まとめ

$ cat summary.md

このエラーへの対応を一言でまとめると、play()がPromiseを返すことを前提にコードを書くということです。

参考リンク

Related Posts

$ _