Воспроизведение видео на мобильных сайтах

François Beaufort
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() – это специальная служебная функция, которую я написал, чтобы преобразовывать количество секунд в строку в формате "чч:мм:сс", который лучше подходит для нашего случая.

<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, мы используем события видео play и pause. Использование событий для управления воспроизведением обеспечивает гибкость (как мы увидим позже при рассмотрении 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 этих кнопок, мы будем использовать активированные видеособытия seeking и seeked для настройки яркости видео. Мой собственный класс CSS seeking выглядит так: filter: brightness(0);.

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

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

Вот что мы уже создали. В следующем разделе мы реализуем кнопку полноэкранного режима.

Полноэкранный режим

Здесь мы воспользуемся несколькими веб-API, чтобы создать идеальный и бесшовный полноэкранный режим. Чтобы посмотреть, как это работает, ознакомьтесь с примером.

Конечно, не обязательно использовать их все. Просто выберите те, которые имеют смысл для вас, и объедините их, чтобы создать свой собственный поток.

Запретить автоматическое открытие в полноэкранном режиме

На устройствах iOS элементы video автоматически переходят в полноэкранный режим при начале воспроизведения медиаконтента. Поскольку мы стараемся максимально адаптировать и контролировать воспроизведение медиаконтента в мобильных браузерах, рекомендуем задать атрибут playsinline элемента video, чтобы принудительно воспроизводить видео на 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(), если он доступен, или используйте резервный вариант webkitEnterFullscreen() для элемента видео (только на устройствах iOS).

<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, который пока поддерживается не везде и в некоторых браузерах имеет префикс. Таким образом, это будет наше первое прогрессивное улучшение.

Как это работает? Как только мы обнаружим изменение ориентации экрана, давайте запросим полноэкранный режим, если окно браузера находится в горизонтальном режиме (то есть его ширина больше высоты). Если нет, выйдите из полноэкранного режима. Это всё.

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

Воспроизводить только одно видео за раз

Если на странице несколько видео, рекомендуем воспроизводить только одно из них, а остальные автоматически ставить на паузу, чтобы пользователю не приходилось слушать несколько звуковых дорожек одновременно.

// 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 заключается в том, что панель уведомлений – не единственное место, где отображаются метаданные и элементы управления. Уведомление о медиаконтенте автоматически синхронизируется с любым подключенным носимым устройством. Оно также появляется на заблокированных экранах.

Как оставить отзыв