移动网站视频播放

François Beaufort
François Beaufort

如何在 Web 上打造最佳移动媒体体验?这很简单!这完全取决于用户互动度以及您对网页上媒体的重视程度。我想大家都会同意,如果用户访问网站的主要目的是观看视频, 那么用户体验必须具有沉浸感和吸引力。

移动网站视频播放

在本文中,我将向您展示如何通过各种 Web 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 事件监听器来调整视频控件,而是使用 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 视频事件来调整视频亮度。我的自定义 seeking CSS 类非常简单,如下所示:filter: brightness(0);。

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

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

以下是到目前为止我们创建的内容。在下一部分中,我们将实现全屏按钮。

全屏

在这里,我们将利用多个 Web 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);
});

一次只播放一个视频

如果一个页面上有多个视频,我建议您只播放一个视频,并自动暂停其他视频,这样用户就不必同时听到多个音轨。

// 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,您还可以通过为当前播放的视频提供元数据来自定义媒体通知。它还允许您处理与媒体相关的事件,例如可能来自通知或媒体键的跳转或曲目更改。如需查看实际效果,请参阅示例。

当您的 Web 应用播放音频或视频时,您已经可以在通知栏中看到媒体通知。在 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。因此,您需要更新它,以确保始终在媒体通知中显示相关信息。

如果您的 Web 应用提供播放列表,您可能希望允许用户通过一些“上一曲目”和“下一曲目”图标直接从媒体通知中浏览播放列表。

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

请注意,媒体操作处理程序会保留。这与事件监听器模式非常相似,不同之处在于,处理事件意味着浏览器会停止执行任何默认行为,并将其用作 Web 应用支持媒体操作的信号。因此,除非您设置了适当的操作处理程序,否则系统不会显示媒体操作控件。

顺便提一下,取消设置媒体操作处理程序就像将其分配给 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 的一个亮点在于,通知栏并非唯一显示媒体元数据和控件的地方。媒体通知会自动同步到任何已配对的穿戴式设备。它还会显示在锁屏界面上

反馈