移动网站视频播放

François Beaufort
François Beaufort

如何在网络上打造最佳移动媒体体验?这很简单!这完全取决于用户互动度以及您对网页上媒体内容的重视程度。我想我们都同意,如果用户访问网站的主要原因是视频,那么用户体验必须具有沉浸感和吸引力,才能让用户再次光顾。

移动网站视频播放

在本文中,我将向您展示如何借助众多 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();
  }
});

我们使用 playpause 视频事件,而不是在 click 事件监听器中调整视频控件。使我们的控件事件基于事件有助于提高灵活性(我们稍后将在 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);
});

与之前一样,我们将使用触发的 seekingseeked 视频事件来调整视频亮度,而不是在这些按钮的 click 事件监听器中调整视频样式。我的自定义 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 很棒的一点是,通知栏不是显示媒体元数据和控件的唯一位置。媒体通知会自动同步到任何已配对的穿戴式设备。而且在锁定的屏幕上也会显示

反馈