モバイルウェブ動画の再生

フランソワ ボーフォール
François Beaufort

ウェブに最適なモバイル メディア エクスペリエンスを作成する方法簡単に行えます。すべては、ユーザー エンゲージメントと、ウェブページ上のメディアに与える重要性によって異なります。もし動画がユーザーの訪問の目的である場合 ユーザーエクスペリエンスは没入感のあるものでなければなりません

モバイルウェブ動画再生

この記事では、数多くのウェブ API を活用してメディア エクスペリエンスを段階的に強化し、臨場感あふれるものにする方法について説明します。そのため、カスタム コントロール、全画面、バックグラウンド再生を備えたシンプルなモバイル プレーヤー エクスペリエンスを構築します。今すぐサンプルを試すと、GitHub リポジトリでコードを確認できます。

カスタム コントロール

HTML レイアウト
図 1. HTML レイアウト

ご覧のとおり、メディア プレーヤーに使用する HTML レイアウトは非常にシンプルです。<div> ルート要素には、<video> メディア要素と、動画コントロール専用の <div> 子要素が含まれます。

動画コントロールについては、後ほど説明します。再生/一時停止ボタン、全画面ボタン、早送りボタン、早送りボタンのほか、現在時刻、継続時間、時刻のトラッキングに関する要素などがあります。

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls"></div>
</div>

動画のメタデータを読み取る

まず、動画のメタデータが読み込まれるまで待ってから、動画の長さと現在の時刻を設定し、進行状況バーを初期化します。secondsToTimeCode() 関数は、私が作成したカスタム ユーティリティ関数で、秒数をこのケースに適した「hh:mm:ss」形式の文字列に変換します。

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <strong>
      <div id="videoCurrentTime"></div>
      <div id="videoDuration"></div>
      <div id="videoProgressBar"></div>
    </strong>
  </div>
</div>
video.addEventListener('loadedmetadata', function () {
  videoDuration.textContent = secondsToTimeCode(video.duration);
  videoCurrentTime.textContent = secondsToTimeCode(video.currentTime);
  videoProgressBar.style.transform = `scaleX(${
    video.currentTime / video.duration
  })`;
});
動画のメタデータのみ
図 2.動画のメタデータを表示するメディア プレーヤー

動画を再生、一時停止

動画のメタデータが読み込まれたら、video.play()video.pause() を使用して、再生状態に応じてユーザーが動画を再生および一時停止できる最初のボタンを追加します。

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <strong><button id="playPauseButton"></button></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
playPauseButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (video.paused) {
    video.play();
  } else {
    video.pause();
  }
});

ここでは、click イベント リスナーで動画コントロールを調整するのではなく、playpause の動画イベントを使用します。コントロール イベントをベースにすると、柔軟性が向上します(後ほど Media Session API で取り上げます)。これにより、ブラウザが再生に介入した場合でもコントロールを同期できます。動画の再生が開始すると、ボタンの状態が「一時停止」に変更され、動画コントロールが非表示になります。動画が一時停止すると、ボタンの状態が「再生」に変更され、動画コントロールが表示されます。

video.addEventListener('play', function () {
  playPauseButton.classList.add('playing');
});

video.addEventListener('pause', function () {
  playPauseButton.classList.remove('playing');
});

動画 currentTime 属性で示される時間が動画イベント timeupdate によって変更されたとき、カスタム コントロールが表示されている場合、コントロールも更新されます。

video.addEventListener('timeupdate', function () {
  if (videoControls.classList.contains('visible')) {
    videoCurrentTime.textContent = secondsToTimeCode(video.currentTime);
    videoProgressBar.style.transform = `scaleX(${
      video.currentTime / video.duration
    })`;
  }
});

動画が終了したら、ボタンの状態を「再生」に変更し、動画 currentTime を 0 に戻して、動画コントロールを表示します。なお、ユーザーがなんらかの「自動再生」機能を有効にしている場合は、別の動画を自動的に読み込むこともできます。

video.addEventListener('ended', function () {
  playPauseButton.classList.remove('playing');
  video.currentTime = 0;
});

巻き戻し / 早送り

次に、「前方シーク」ボタンと「早送り」ボタンを追加して、ユーザーがコンテンツを簡単にスキップできるようにしましょう。

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <button id="playPauseButton"></button>
    <strong
      ><button id="seekForwardButton"></button>
      <button id="seekBackwardButton"></button
    ></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
var skipTime = 10; // Time to skip in seconds

seekForwardButton.addEventListener('click', function (event) {
  event.stopPropagation();
  video.currentTime = Math.min(video.currentTime + skipTime, video.duration);
});

seekBackwardButton.addEventListener('click', function (event) {
  event.stopPropagation();
  video.currentTime = Math.max(video.currentTime - skipTime, 0);
});

以前と同様に、これらのボタンの click イベント リスナーで動画のスタイル設定を調整するのではなく、発生した seekingseeked の動画イベントを使用して動画の明るさを調整します。カスタムの seeking CSS クラスは filter: brightness(0); と同じくらいシンプルです。

video.addEventListener('seeking', function () {
  video.classList.add('seeking');
});

video.addEventListener('seeked', function () {
  video.classList.remove('seeking');
});

これまでに作成したものを以下に示します。次のセクションでは全画面ボタンを実装します

全画面表示

ここでは、いくつかのウェブ API を利用して、完全でシームレスな全画面エクスペリエンスを作成します。実際の動作を確認するには、サンプルをご覧ください。

もちろん、それらすべてを使用する必要はありません。適切なものを選択して組み合わせるだけで、カスタムフローを作成できます。

自動全画面表示を無効にする

iOS では、メディアの再生が開始されると video 要素は自動的に全画面モードになります。モバイル ブラウザ全体でメディア エクスペリエンスを可能な限り調整して制御しようとしているため、video 要素の playsinline 属性を設定して、iPhone で強制的にインライン再生し、再生開始時に全画面モードに移行しないようにすることをおすすめします。なお、他のブラウザへの副作用はありません。

<div id="videoContainer"></div>
  <video id="video" src="file.mp4"></video><strong>playsinline</strong></video>
  <div id="videoControls">...</div>
</div>

ボタンのクリックで全画面表示の切り替え

自動全画面を防止したので、Fullscreen API を使用して動画の全画面モードを処理する必要があります。ユーザーが「全画面ボタン」をクリックしたときに、ドキュメントで全画面モードが使用されている場合は、document.exitFullscreen() を使用して全画面モードを終了しましょう。それ以外の場合は、requestFullscreen() メソッド(使用可能な場合)を使用して動画コンテナの全画面表示をリクエストし、iOS でのみ動画要素で webkitEnterFullscreen() にフォールバックします。

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <button id="playPauseButton"></button>
    <button id="seekForwardButton"></button>
    <button id="seekBackwardButton"></button>
    <strong><button id="fullscreenButton"></button></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
fullscreenButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (document.fullscreenElement) {
    document.exitFullscreen();
  } else {
    requestFullscreenVideo();
  }
});

function requestFullscreenVideo() {
  if (videoContainer.requestFullscreen) {
    videoContainer.requestFullscreen();
  } else {
    video.webkitEnterFullscreen();
  }
}

document.addEventListener('fullscreenchange', function () {
  fullscreenButton.classList.toggle('active', document.fullscreenElement);
});

画面の向きの変更で全画面表示を切り替える

ユーザーが横表示でデバイスを回転させた場合、この点を考慮し、自動的に全画面表示をリクエストして没入感のあるエクスペリエンスを実現します。そのためには Screen Orientation API が必要です。この API は、まだ場所を問わずサポートされているわけではなく、現時点では一部のブラウザで接頭辞付きとなっています。したがって、これは最初の段階的な拡張になります。

このプログラムの仕組みを見てみましょう。画面の向きの変化が検出されたらすぐに、ブラウザ ウィンドウが横表示(つまり、幅が高さより大きい)であれば、全画面表示をリクエストします。そうでない場合は、全画面表示を終了します。以上です。

if ('orientation' in screen) {
  screen.orientation.addEventListener('change', function () {
    // Let's request fullscreen if user switches device in landscape mode.
    if (screen.orientation.type.startsWith('landscape')) {
      requestFullscreenVideo();
    } else if (document.fullscreenElement) {
      document.exitFullscreen();
    }
  });
}

ボタンのクリック時に横向きのロック画面

横表示にすると動画が見やすくなります。ユーザーが「全画面ボタン」をクリックしたときに画面を横向きでロックすることをおすすめします。これまで使用してきた Screen Orientation APIメディアクエリを組み合わせて、最適な表示を実現します。

横向きでの画面のロックは、screen.orientation.lock('landscape') を呼び出すだけで簡単に行えます。ただし、これを行うのは、デバイスが matchMedia('(orientation: portrait)') でポートレート モードになっていて、matchMedia('(max-device-width: 768px)') で片手で持つことができる場合のみにするべきです。これは、タブレットを使用するユーザーにとっては最適なエクスペリエンスとは言えません。

fullscreenButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (document.fullscreenElement) {
    document.exitFullscreen();
  } else {
    requestFullscreenVideo();
    <strong>lockScreenInLandscape();</strong>;
  }
});
function lockScreenInLandscape() {
  if (!('orientation' in screen)) {
    return;
  }
  // Let's force landscape mode only if device is in portrait mode and can be held in one hand.
  if (
    matchMedia('(orientation: portrait) and (max-device-width: 768px)').matches
  ) {
    screen.orientation.lock('landscape');
  }
}

デバイスの向きが変わったときに画面のロックを解除する

先ほど作成したロック画面のエクスペリエンスが不完全であることにお気づきかもしれませんが、画面がロックされているときは画面の向きの変更が行われないためです。

この問題を解決するには、Device Orientation API を使用します(利用可能な場合)。この API は、空間内でのデバイスの位置と動きを測定しているハードウェアからの情報を提供します。つまり、デバイスの向きを示すジャイロスコープとデジタル コンパス、速度を示す加速度計の情報を提供します。デバイスの向きの変化を検出し、ユーザーがデバイスを縦表示で持ち、横表示で画面がロックされていれば、screen.orientation.unlock() で画面をロック解除します。

function lockScreenInLandscape() {
  if (!('orientation' in screen)) {
    return;
  }
  // Let's force landscape mode only if device is in portrait mode and can be held in one hand.
  if (matchMedia('(orientation: portrait) and (max-device-width: 768px)').matches) {
    screen.orientation.lock('landscape')
    <strong>.then(function() {
      listenToDeviceOrientationChanges();
    })</strong>;
  }
}
function listenToDeviceOrientationChanges() {
  if (!('DeviceOrientationEvent' in window)) {
    return;
  }
  var previousDeviceOrientation, currentDeviceOrientation;
  window.addEventListener(
    'deviceorientation',
    function onDeviceOrientationChange(event) {
      // event.beta represents a front to back motion of the device and
      // event.gamma a left to right motion.
      if (Math.abs(event.gamma) > 10 || Math.abs(event.beta) < 10) {
        previousDeviceOrientation = currentDeviceOrientation;
        currentDeviceOrientation = 'landscape';
        return;
      }
      if (Math.abs(event.gamma) < 10 || Math.abs(event.beta) > 10) {
        previousDeviceOrientation = currentDeviceOrientation;
        // When device is rotated back to portrait, let's unlock screen orientation.
        if (previousDeviceOrientation == 'landscape') {
          screen.orientation.unlock();
          window.removeEventListener(
            'deviceorientation',
            onDeviceOrientationChange,
          );
        }
      }
    },
  );
}

ご覧のとおり、これは私たちが求めていたシームレスな全画面エクスペリエンスです。 実際の動作を確認するには、サンプルをご覧ください。

バックグラウンド再生

ウェブページやウェブページ内の動画が表示されなくなったことが検出された場合は、アナリティクスを更新してこれを反映することをおすすめします。また、別のトラックの選択や一時停止、ユーザーへのカスタムボタンの表示など、現在の再生にも影響する可能性があります。

ページの公開設定の変更で動画を一時停止する

Page Visibility API を使用すると、ページの現在の公開設定を判断し、公開設定の変更について通知を受け取ることができます。以下のコードでは、ページが非表示のときに動画を一時停止しています。これは、画面ロックが有効になっているとき、またはタブを切り替えたときに発生します。

現在、ほとんどのモバイル ブラウザでは、一時停止した動画を再開できるブラウザの外部でコントロールが提供されているため、この動作はユーザーにバックグラウンドでの再生が許可されている場合にのみ設定することをおすすめします。

document.addEventListener('visibilitychange', function () {
  // Pause video when page is hidden.
  if (document.hidden) {
    video.pause();
  }
});

動画の公開設定の変更時にミュートボタンを表示/非表示

新しい Intersection Observer API を使用すると、費用をかけずにさらにきめ細かく制御できます。この API を使用すると、監視対象の要素がブラウザのビューポートを出入りするタイミングを把握できます。

ページでの動画の表示に基づいて、ミュートボタンの表示/非表示を切り替えてみましょう。動画が再生されているものの、現在表示されない場合は、ページの右下にミニミュートボタンが表示され、ユーザーが動画の音声をコントロールできるようになります。volumechange 動画イベントは、ミュートボタンのスタイルを更新するために使用されます。

<button id="muteButton"></button>
if ('IntersectionObserver' in window) {
  // Show/hide mute button based on video visibility in the page.
  function onIntersection(entries) {
    entries.forEach(function (entry) {
      muteButton.hidden = video.paused || entry.isIntersecting;
    });
  }
  var observer = new IntersectionObserver(onIntersection);
  observer.observe(video);
}

muteButton.addEventListener('click', function () {
  // Mute/unmute video on button click.
  video.muted = !video.muted;
});

video.addEventListener('volumechange', function () {
  muteButton.classList.toggle('active', video.muted);
});

一度に 1 つの動画のみを再生する

1 ページに複数の動画がある場合は、1 つの動画のみを再生し、他の動画を自動的に一時停止することをおすすめします。これにより、複数の音声トラックが同時に再生される必要がなくなります。

// This array should be initialized once all videos have been added.
var videos = Array.from(document.querySelectorAll('video'));

videos.forEach(function (video) {
  video.addEventListener('play', pauseOtherVideosPlaying);
});

function pauseOtherVideosPlaying(event) {
  var videosToPause = videos.filter(function (video) {
    return !video.paused && video != event.target;
  });
  // Pause all other videos currently playing.
  videosToPause.forEach(function (video) {
    video.pause();
  });
}

メディア通知のカスタマイズ

Media Session API を使用すると、現在再生中の動画のメタデータを提供して、メディア通知をカスタマイズすることもできます。また、通知やメディアキーから発生するシークやトラック変更など、メディア関連のイベントを処理することもできます。実際の動作を確認するには、サンプルをご覧ください。

ウェブアプリが音声または動画を再生しているときは、すでに通知トレイにメディア通知が表示されています。Android では、Chrome はドキュメントのタイトルと、確認可能な最大のアイコン画像を使用して、適切な情報を表示するために最善を尽くします。

Media Session API を使用して、タイトル、アーティスト、アルバム名、アートワークなどのメディア セッション メタデータを設定して、このメディア通知をカスタマイズする方法を見てみましょう。

playPauseButton.addEventListener('click', function(event) {
  event.stopPropagation();
  if (video.paused) {
    video.play()
    <strong>.then(function() {
      setMediaSession();
    });</strong>
  } else {
    video.pause();
  }
});
function setMediaSession() {
  if (!('mediaSession' in navigator)) {
    return;
  }
  navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Never Gonna Give You Up',
    artist: 'Rick Astley',
    album: 'Whenever You Need Somebody',
    artwork: [
      {src: 'https://dummyimage.com/96x96', sizes: '96x96', type: 'image/png'},
      {
        src: 'https://dummyimage.com/128x128',
        sizes: '128x128',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/192x192',
        sizes: '192x192',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/256x256',
        sizes: '256x256',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/384x384',
        sizes: '384x384',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/512x512',
        sizes: '512x512',
        type: 'image/png',
      },
    ],
  });
}

再生が完了したら、通知は自動的に消えるため、メディア セッションを「解放」する必要はありません。再生が開始されると、現在の navigator.mediaSession.metadata が使用されます。そのため、メディア通知に常に関連情報が表示されるよう更新する必要があります。

ウェブアプリでプレイリストを提供している場合、「前のトラック」アイコンと「次のトラック」アイコンを使って、メディア通知からユーザーが直接プレイリスト内を移動できるようにすることをおすすめします。

if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('previoustrack', function () {
    // User clicked "Previous Track" media notification icon.
    playPreviousVideo(); // load and play previous video
  });
  navigator.mediaSession.setActionHandler('nexttrack', function () {
    // User clicked "Next Track" media notification icon.
    playNextVideo(); // load and play next video
  });
}

なお、メディア アクション ハンドラは存続します。これはイベント リスナーのパターンとよく似ていますが、イベントを処理するとブラウザがデフォルトの動作を停止し、ウェブアプリがメディア アクションをサポートしていることを示すシグナルとして使用されます。そのため、適切なアクション ハンドラを設定しない限り、メディア アクション コントロールは表示されません。

なお、メディア アクション ハンドラの設定解除は、null に割り当てるだけで簡単です。

Media Session API を使用すると、「巻き戻し」と「早送り」のメディア通知アイコンを表示して、スキップする時間を制御できます。

if ('mediaSession' in navigator) {
  let skipTime = 10; // Time to skip in seconds

  navigator.mediaSession.setActionHandler('seekbackward', function () {
    // User clicked "Seek Backward" media notification icon.
    video.currentTime = Math.max(video.currentTime - skipTime, 0);
  });
  navigator.mediaSession.setActionHandler('seekforward', function () {
    // User clicked "Seek Forward" media notification icon.
    video.currentTime = Math.min(video.currentTime + skipTime, video.duration);
  });
}

メディア通知には「再生/一時停止」アイコンが常に表示され、関連するイベントはブラウザによって自動的に処理されます。なんらかの理由でデフォルトの動作が機能しない場合でも、「再生」と「一時停止」のメディア イベントを処理できます。

Media Session API の優れている点は、メディアのメタデータとコントロールが表示される場所は通知トレイだけではないことです。メディア通知は、ペア設定されているウェアラブル デバイスと自動的に同期されます。ロック画面にも表示されます

フィードバック