移动网页视频播放

[TOC]

对原文有一定删简。

我认为我们都同意,如果视频是用户访问的原因,用户的体验必须是沉浸式和重新吸引。

在这篇文章中,展示了如何以渐进的方式增强您的媒体体验,并通过大量的Web API使其更加身临其境。这就是为什么我们要用自定义控件,全屏幕和背景播放来构建一个简单的移动播放器体验。您现在可以尝试该示例,并在我们的GitHub存储库中找到代码

自定义控件

您可以看到,我们将用于我们的媒体播放器的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” > 
    <div id = “videoCurrentTime” > </ div>  
    <div id = “videoDuration” > </ div> 
    <div id = “videoProgressBar” > </ div> 
  </ 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">
    <button id="playPauseButton"></button>
    <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();
  }
});

注意:我调用event.stopPropagation()阻止父处理程序(例如视频控件)被通知点击事件。

而不是在click事件监听器中调整我们的视频控件,我们使用playpause视频事件。使我们的控件事件有助于灵活性(稍后我们将在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>
    <button id="seekForwardButton"></button>
    <button id="seekBackwardButton"></button>
    <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视频事件来调整视频亮度。我自定义的seekingCSS类与filter: brightness(0);一样简单。

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

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

Here's below what we have created so far. In the next section, we'll implement the fullscreen button.

全屏

在这里,我们将利用多个Web API来创建完美无缝的全屏体验。要查看它,请查看 样品

防止自动全屏

在iOS上,video当媒体播放开始时,元素会自动进入全屏模式。当我们尝试在移动浏览器上定制和控制我们的媒体体验时,我建议您设置元素的playsinline 属性,video以强制其在iPhone上内嵌播放,而在播放开始时不进入全屏模式。请注意,这对其他浏览器没有任何副作用。

<div id="videoContainer">
  <video id="video" src="file.mp4" playsinline></video>
  <div id="videoControls">...</div>
</div>

注意:仅当您提供自己的媒体控件或显示本机控件<video controls>才设置playsinline

点击按钮切换全屏

现在我们阻止自动全屏,我们需要使用全屏API来处理视频的全屏模式。当用户单击“全屏按钮”时,如果文档当前正在使用全屏模式,则使用document.exitFullscreen()退出全屏模式。否则,请在视频容器上使用requestFullscreen()方法请求全屏,或者仅在iOS上的视频元素上回溯到webkitEnterFullscreen()。

注意:我将在下面的代码片段中使用一个微小的垫片作为全屏API,因为当时API并不是含有前缀的,因此需要处理前缀。您也可以使用screenfull.js包装器。

<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>
    <button id="fullscreenButton"></button>
    <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);
});

译者注:

toggle ( String [, force] )

当只有一个参数时:切换 class value; 即如果类存在,则删除它并返回false,如果不存在,则添加它并返回true。

当存在第二个参数时:如果第二个参数的计算结果为true,则添加指定的类值,如果计算结果为false,则删除它。

https://developer.mozilla.org/zh-CN/docs/Web/API/Element/classList

屏幕方向改变时切换全屏

当用户以横向模式旋转设备时,让我们对此进行自动化,并自动请求全屏来创建身临其境的体验。为此,我们将需要屏幕定向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();
    lockScreenInLandscape();
  }
});
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')
    .then(function() {
      listenToDeviceOrientationChanges();
    });
  }
}
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);
      }
    }
  });
}

如您所见,这是我们正在寻找的无缝全屏体验。要查看此操作,请查看样品

背景播放

当您检测到网页或网页中的视频不再可见时,您可能需要更新分析以反映这一点。这也可能会影响当前播放,例如选择不同的轨道,暂停播放,甚至显示自定义按钮。

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

注意:Chrome for Android已经在页面被隐藏时暂停了视频。

在视频可见度变化时,显示/隐藏静音按钮

如果您使用新的Intersection Observer API,则可以免费更精细地进行。此API可让您知道观察元素何时进入或退出浏览器的视口。

让我们根据页面中的视频可见性显示/隐藏静音按钮。如果视频播放但当前不可见,则迷你静音按钮将显示在页面的右下角,以便用户控制视频声音。该 volumechange视频事件被用于更新静音按钮造型。

注意:如果网页上有大量视频,并且正在使用Intersection Observer API 来暂停/静音屏幕视频,则可能需要重置视频源,video.src = null。因为它会在无限滚动情况下释放大量资源。

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

一次只播放一个视频

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

// Note: 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,您还可以通过为当前播放的视频提供元数据来自定义媒体通知。它还允许您处理与媒体相关的事件,例如寻找或跟踪可能来自通知或媒体密钥的更改。要查看此操作,请查看样品

当您的网络应用程序播放音频或视频时,您可以看到通知托盘中的媒体通知。在Android上,Chrome会尽可能通过使用文档的标题和最大的图标图像来显示适当的信息。

我们来看看如何通过媒体会话API设置一些媒体会话元数据(如标题,艺术家,专辑名称和图稿)来自定义此媒体通知 。

playPauseButton.addEventListener('click', function(event) {
  event.stopPropagation();
  if (video.paused) {
    video.play()
    .then(function() {
      setMediaSession();
    });
  } 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
  });
}

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

顺便说一下,取消设置媒体操作处理程序很简单:将其分配给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的酷炫之处在于,通知托盘不是媒体元数据和控件可见的唯一位置。媒体通知会自动同步到任何配对的可穿戴设备。它也显示在锁定屏幕上。

Last updated