Воспроизведение мобильного веб-видео

Франсуа Бофор
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 видео. Создание наших элементов управления на основе событий обеспечивает гибкость (как мы увидим позже с помощью 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);
});

Переключить полноэкранный режим при изменении ориентации экрана

Поскольку пользователь поворачивает устройство в альбомном режиме, давайте будем осторожны и автоматически запросим полноэкранный режим, чтобы создать эффект присутствия. Для этого нам понадобится 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();
    }
  });
}

Блокировка экрана в альбомной ориентации при нажатии кнопки

Поскольку видео лучше просматривать в альбомном режиме, мы можем захотеть заблокировать экран в альбомной ориентации, когда пользователь нажимает кнопку «полноэкранный режим». Мы собираемся объединить ранее использовавшийся 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');
  }
}

Разблокировка экрана при изменении ориентации устройства

Возможно, вы заметили, что только что созданный нами экран блокировки не идеален, поскольку мы не получаем изменения ориентации экрана, когда экран заблокирован.

Чтобы это исправить, давайте воспользуемся 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,
          );
        }
      }
    },
  );
}

Как видите, это именно тот полноэкранный режим, который мы искали. Чтобы увидеть это в действии, посмотрите пример .

Фоновое воспроизведение

Когда вы обнаружите, что веб-страница или видео на веб-странице больше не отображается, вы можете обновить свою аналитику, чтобы отразить это. Это также может повлиять на текущее воспроизведение, например, при выборе другого трека, приостановке его или даже показе пользователю пользовательских кнопок.

Пауза видео при изменении видимости страницы

С помощью API видимости страницы мы можем определить текущую видимость страницы и получать уведомления об изменениях видимости. Код ниже приостанавливает видео, когда страница скрыта. Это происходит, когда активна блокировка экрана или, например, при переключении вкладок.

Поскольку большинство мобильных браузеров теперь предлагают элементы управления вне браузера, позволяющие возобновлять приостановленное видео, я рекомендую устанавливать это поведение только в том случае, если пользователю разрешено воспроизведение в фоновом режиме.

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

Показать/скрыть кнопку отключения звука при изменении видимости видео

Если вы используете новый API Intersection Observer , вы можете добиться еще большей детализации без каких-либо затрат. Этот 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();
  });
}

Настройте медиа-уведомления

С помощью API Media Session вы также можете настроить медиа-уведомления, предоставив метаданные для воспроизводимого в данный момент видео. Это также позволяет вам обрабатывать события, связанные с мультимедиа, такие как поиск или отслеживание изменений, которые могут поступать из уведомлений или медиа-ключей. Чтобы увидеть это в действии, посмотрите пример .

Когда ваше веб-приложение воспроизводит аудио или видео, вы уже можете видеть мультимедийное уведомление в области уведомлений. На 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 .

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

Значок «Воспроизведение/Пауза» всегда отображается в мультимедийном уведомлении, а соответствующие события обрабатываются браузером автоматически. Если по какой-то причине поведение по умолчанию не работает, вы все равно можете обрабатывать медиа-события «Воспроизведение» и «Пауза» .

Самое интересное в API сеанса мультимедиа то, что панель уведомлений — не единственное место, где видны метаданные мультимедиа и элементы управления. Мультимедийное уведомление автоматически синхронизируется с любым подключенным носимым устройством. И это также отображается на экранах блокировки.

Обратная связь