移动网站视频播放

弗朗索瓦·博福特
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 很酷的一点是,通知栏并不是显示媒体元数据和控件的唯一位置。媒体通知会自动同步到任何已配对的穿戴式设备。而且会显示在锁定的屏幕上。

反馈