iOSの低電力モードにはめられた話 —— Videoタグのautoplayが効かず、最後は「諦めを実装」した

学苑祭の公式ホームページのスクリーンショット 学苑祭 developers blog
学苑祭の公式ホームページのスクリーンショット

学苑祭公式サイトを作る中で、地味に、でもかなり強烈に苦しめられたのが「iOS / iPadOS の低電力モードで Video タグの autoplay が効かない」問題でした。しかもただ再生されないだけではなく、こちらが意図していない四角い再生ボタンのマークまで出てきて、ローディング演出の見た目がきれいに崩れます。

今回の記事は、第78回水戸一高・附属中学校学苑祭「天翔る」公式サイトを作る中で、実際にこの問題にどうはまり、どう調べ、どう割り切って、最終的にどう実装したかをまとめたものです。「同じことにハマる人を減らしたい」という気持ちで、できるだけ真似しやすい形で残しておきます。

学苑祭公開サイト

学苑祭サイトのNow Loading静止画
実際のサイトで用意されている Now Loading 用の静止画。最終的に、この「静止画へのフォールバック」が重要になりました。

まず、このプロジェクトがどんなサイトだったか

今回の学苑祭サイトは、ただの1ページものではありません。PHP で共通ヘッダー・フッターを分けつつ、トップ、コンセプト、企画、タイムテーブル、喫茶、グッズ、アクセスなど複数ページを持ち、さらに WordPress REST API からブログやお知らせを取得し、PWA 化まで行っている構成でした。管理画面ではSquare 連携による在庫管理まで扱えるようになっています。

WordPress REST APIの連携については過去記事を参考にしてみてください

WordPress REST APIで最新の投稿をサイトに表示する方法
学苑祭BlogではWordPressを使用して管理を行なっています。今日は皆さんも目にしているこのような実装の仕方を解説します。そもそもWordPressは、強力なコンテンツ管理機能だけでなく、REST APIを通じて外部アプリケーションと…

手前味噌ではありますが文化祭サイトとしてはかなり本格的だと思っています。

その中でトップページは、最初にローディング演出を出す設計になっていました。動画を一瞬流してから本編へ遷移する、いわばサイトの“つかみ”です。演出的にもかなり大事だったので、ここが崩れるとトップ体験そのものが弱くなります。

発端 —— 「autoplay の定番セット」を全部入れても止まる

トップページのローディング部分には、実際に次のような video タグが置かれていました。autoplaymutedplaysinlinewebkit-playsinlinepreload="auto" と、iOS 系でよく言われる“定番セット”はちゃんと入っています。

<video
  class="loading_video"
  id="loading_video"
  autoplay
  muted
  playsinline
  webkit-playsinline
  preload="auto"
  aria-hidden="true">
</video>

しかも JavaScript 側でも、ソースを動的に差し込み、video.load() した上で video.play() まで呼び出しています。つまり、「属性を付けただけ」ではなく、「再生開始のトリガーも明示的にかけている」状態でした。それでも、iPhone / iPad の低電力モード時だけ autoplay が通らない。ここが今回のやっかいなポイントでした。

症状 —— Macでは平和、iPhone / iPadだけ急に裏切る

こういう問題がつらいのは、全部の環境で壊れるわけではないことです。Mac 側では普通に動いているのに、iPhone や iPad の低電力モードに入った瞬間だけ、動画が自動再生されず、あの四角い再生ボタンが出てきます。しかも「コードが間違っている」感じではなく、「条件が揃った時だけ OS 側に止められている」ような挙動をします。

つまり開発中の感覚としては、「え、Mac では動いてるのに?」「mutedplaysinline もあるのに?」「play() まで呼んでるのに?」という、かなり精神に悪いタイプの不具合でした。

再現条件

少なくとも今回の問題は、次の条件で再現しやすいものでした。

  1. iPhone または iPad で Safari を使う
  2. 端末の低電力モードを ON にする
  3. 対象ページを開く / リロードする
  4. autoplay を期待している video が再生されない
  5. 代わりに再生ボタンの UI が見えてしまう

Apple のサポート文書でも、低電力モードはバッテリー消費を抑えるために各種の自動処理やバックグラウンド動作を制限すると説明されています。その文脈で autoplay が止められるのは、かなり自然な挙動です。 Apple Support

最初に試したこと

もちろん、最初から「無理だ」と思っていたわけではありません。まずは Web 側でできる王道の対応を一通りやりました。

  1. autoplay muted playsinline webkit-playsinline を付ける
  2. preload="auto" を付ける
  3. 動画ソースを webm / mp4 で用意する
  4. JavaScript から video.play() を明示的に呼ぶ
  5. 再生できなかったときのイベントや Promise の失敗を拾う

実際のコードでも、モバイル用・PC用で動画と静止画のパスを切り替えながら、video.play() の成功・失敗を見ています。ここまでやって動かないなら、「実装不足」ではなく「その条件ではブラウザ側の方針で止められている」と考えるしかありませんでした。

調べて分かったこと —— “強制再生” はほぼ期待しない方がいい

この件について調べると、かなり近い報告がいくつも見つかります。たとえば Stack Overflow でも、iPhone の低電力モードでは autoplay が効かないという相談があり、回答では「play() の失敗を拾ってフォールバック UI を出す」といった方向が示されています。つまり、強引に突破するというより、失敗した時にどう崩さないかが現実的な対処です。 Stack Overflow

また、「低電力モードのときに出る再生ボタンだけ消せないか」という話もコミュニティで繰り返し出ていますが、発想としてはかなり苦しいです。そもそも問題の本体は CSS ではなく、autoplay が許可されていないことだからです。再生ボタンを隠したところで、動画が勝手に動くわけではありません。 Stack Overflow

要するに、今回の調査範囲では「iOS / iPadOS の低電力モードでも、Web 側から確実に autoplay を強制できる決定打」は見つかりませんでした。少なくとも本番運用に耐える形で「これで絶対通る」と言えるものは、見つからなかったです。

結論 —— 解決方法はなかった。だから“壊れない代替”を実装した

ここが今回いちばん大事な結論です。「低電力モードでも autoplay させる」こと自体は、解決できませんでした。なので発想を変えて、再生できなかった時に見た目が破綻しない実装に切り替えました。

具体的には、動画が再生できない場合は、最初から用意していた WebP の静止画に切り替えるようにしました。これなら演出の情報量は少し落ちても、少なくとも「謎の再生ボタンが真ん中に出てくる」「ローディング画面として成立しない」という事故は避けられます。そして実際のプロジェクトでも、その方針で実装されています。

実際の実装方針

実装の考え方はシンプルです。「動画が再生できたら動画使う。再生できなかったら静止画を出す」。これだけです。ただし、ちゃんと運用に耐えるようにするには、いくつか細かいポイントがあります。

1. 最初から静止画を用意しておく

まず、動画の代わりになる静止画を同時に用意しておきます。今回のサイトでも、動画に対して NowLoading.webp をフォールバックとして持っています。これは「動画が使えなかった時にどうするか」を、後から考えるのではなく最初から設計に入れる、という意味でも大事でした。

2. video.play() の失敗を拾う

iOS の低電力モードでは、単に autoplay 属性を書いて終わりでは足りません。play() を呼び、その Promise が reject されたら「この端末では自動再生できない」と判断して静止画へ切り替える、という実装にしておくとかなり安定します。これは今回のコードでも実際に採っている方法です。Stack Overflow

<div id="loading_screen">
  <video id="loading_video" autoplay muted playsinline webkit-playsinline preload="auto"></video>
  <img id="loading_image" src="/materials/NowLoading/NowLoading.webp" alt="Now Loading">
</div>

<script>
const video = document.getElementById("loading_video");
const fallbackImage = document.getElementById("loading_image");

let switched = false;

function showFallback(reason) {
  if (switched) return;
  switched = true;
  console.log("fallback:", reason);
  video.style.display = "none";
  fallbackImage.style.display = "block";
}

video.muted = true;
video.defaultMuted = true;
video.setAttribute("muted", "");
video.setAttribute("playsinline", "");
video.setAttribute("webkit-playsinline", "");
video.setAttribute("autoplay", "");
video.preload = "auto";

video.innerHTML = `
  <source src="/materials/NowLoading/NowLoading.webm" type="video/webm">
  <source src="/materials/NowLoading/NowLoading.mp4" type="video/mp4">
`;

video.load();

try {
  const promise = video.play();
  if (promise && typeof promise.then === "function") {
    promise.catch((error) => {
      showFallback(error);
    });
  }
} catch (error) {
  showFallback(error);
}

video.addEventListener("error", () => {
  showFallback("video error");
});

setTimeout(() => {
  if (!switched && video.paused && video.readyState < 2) {
    showFallback("autoplay timeout");
  }
}, 2000);
</script>

3. 「再生できないまま固まる」も潰す

実際のコードでは、Promise の失敗だけではなく、error イベントや「2秒たっても再生が進んでいない」ケースまで見ています。このあたりを入れておくと、端末差や回線状況の違いがあっても、ローディング画面が中途半端に止まる事故を減らせます。

4. CSSは“見た目”だけに徹する

CSS 側では、動画も静止画も全画面にきれいに収まるようにしておけば十分です。実際のトップページ CSS でも、動画と静止画は同じように中央配置され、object-fit: cover; で画面いっぱいに見せる構成でした。一方で、iOS の低電力モード時に出る再生ボタンそのものを CSS だけでどうにかする専用実装は入っていません。それで正解だったと思います。本質は装飾ではなく、動画を諦めて静止画へ逃がすことだからです。

#loading_screen {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100dvh;
  overflow: hidden;
}

.loading_video,
.loading_image {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  min-width: 100%;
  min-height: 100%;
  object-fit: cover;
}

.loading_image {
  display: none;
}

実運用で地味に効いたポイント

今回の実装でよかったのは、「初回だけローディングを見せて、同一セッションの再訪時にはスキップする」工夫も入っていたことです。具体的には sessionStorage を使って、一度表示したら同じセッションではローディング画面を飛ばしています。これによって、動画が再生できる端末でも、何度も同じ演出を見せて煩わしくなるのを防げました。

さらに、モバイルと PC / タブレットで読み込む動画・静止画を分けているのも堅実です。画面サイズに応じて素材を切り替えておくと、表示負荷も演出の見え方もかなり安定します。単に「autoplay が通るか」だけではなく、「通らない時も含めてどう見せるか」を設計しておくのが大事でした。

逆に、やらなくてよかったこと

今振り返ると、やらなくてよかったなと思うのは、iOS の低電力モードをどうにか出し抜こうとする方向に時間を使いすぎることです。UA 判定を濃くしたり、無理に controls をいじったり、CSS で再生ボタンだけ隠そうとしたりしても、根本の「autoplay が許可されていない」問題は解消しません。

むしろ大事なのは、「この条件では動画が流れない」という前提を受け入れて、それでも見た目と導線を壊さないことでした。これは演出実装全般に言える話で、リッチ表現は成功したら加点、失敗しても減点にならない作りにしておくと、本番でかなり強いです。

今回の学び

今回いちばん学んだのは、autoplay は「使えたらラッキー」くらいに考えた方がいいということです。特に iOS / iPadOS では、端末状態や省電力設定の影響を受けやすく、開発機の一部環境でうまくいっていても、本番で同じように動くとは限りません。 Apple Support

そしてもうひとつ、文化祭サイトのように「初見の印象」が大事な場面ほど、動画そのものよりも失敗時の見え方の方が重要です。今回は結局、低電力モードそのものを攻略したわけではありません。でも、再生できない端末でも破綻しない導線に変えたことで、実運用としては十分に着地できました。

まとめ

iOS / iPadOS の低電力モード時に Video タグ of autoplay が効かない問題は、少なくとも今回の調査と実装の範囲では、「強制的に再生させる」形では解決できませんでした。だからこそ、解決策は「無理やり再生すること」ではなく、再生できなかったときに静止画へ切り替えることでした。

もし同じ問題にハマっているなら、まずは次の順番で考えるのがおすすめです。まず mutedplaysinlineautoplay を正しく付ける。次に video.play() の失敗を拾う。それでもだめなら、フォールバック画像を正として実装する。これが、今回の学苑祭サイトでたどり着いた、いちばん現実的な答えでした。Stack Overflow


参考リンク

学苑祭 developers blog
実行委員長 浅沼 琉音をフォローする
この記事を書いた人

第78回学苑祭実行委員長を務めている高校3年生です。

1年に1度のお祭りを最高のものにするため、現在チーム一丸となって奮闘しています。私の記事では、実行委員長という立場からしか見えない景色や視点で、学苑祭の魅力を発信していきます。

委員会の何気ない日常から、準備期間のちょっとした裏話までお届けします。ぜひ当日まで一緒にワクワクしていただけたら嬉しいです!

実行委員長 浅沼 琉音をフォローする

コメント

タイトルとURLをコピーしました