Phát video trên web dành cho thiết bị di động

[Tên người]
François Beaufort

Làm cách nào để tạo trải nghiệm tốt nhất khi lướt web đối với nội dung nghe nhìn dành cho thiết bị di động? Thật dễ dàng! Tất cả đều phụ thuộc vào mức độ tương tác của người dùng và tầm quan trọng mà bạn coi trọng đối với nội dung đa phương tiện trên trang web. Tôi nghĩ tất cả chúng ta đều đồng ý rằng nếu video là lý do truy cập của người dùng, thì trải nghiệm của người dùng phải sống động và tương tác lại.

phát video trên web dành cho thiết bị di động

Trong bài viết này, tôi sẽ chỉ cho bạn cách cải thiện dần trải nghiệm nội dung đa phương tiện và làm cho trải nghiệm đó trở nên sống động hơn nhờ vào số lượng lớn API web. Đó là lý do chúng tôi sẽ xây dựng một trải nghiệm đơn giản cho trình phát trên thiết bị di động với các nút điều khiển tuỳ chỉnh, chế độ toàn màn hình và tính năng phát trong nền. Bạn có thể thử ngay mẫu và tìm trong kho lưu trữ GitHub của chúng tôi.

Các chế độ điều khiển tuỳ chỉnh

Bố cục HTML
Hình 1. Bố cục HTML

Như bạn có thể thấy, bố cục HTML mà chúng ta sẽ sử dụng cho trình phát nội dung đa phương tiện khá đơn giản: thành phần gốc <div> chứa thành phần đa phương tiện <video> và thành phần con <div> dành riêng cho các chế độ điều khiển video.

Các nút điều khiển video mà chúng ta sẽ đề cập sau đó, bao gồm: nút phát/tạm dừng, nút toàn màn hình, nút tua lùi và tiến, cùng một số thành phần cho thời gian hiện tại, thời lượng và phần theo dõi thời gian.

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

Đọc siêu dữ liệu video

Trước tiên, hãy đợi siêu dữ liệu video được tải để đặt thời lượng video, thời gian hiện tại và khởi chạy thanh tiến trình. Xin lưu ý rằng hàm secondsToTimeCode() là một hàm tiện ích tuỳ chỉnh mà tôi đã viết để chuyển đổi số giây thành chuỗi ở định dạng "hh:mm:ss". Hàm này phù hợp hơn trong trường hợp của chúng ta.

<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
  })`;
});
chỉ siêu dữ liệu video
Hình 2. Trình phát nội dung đa phương tiện hiển thị siêu dữ liệu video

Phát/tạm dừng video

Bây giờ, siêu dữ liệu video đã được tải, hãy thêm nút đầu tiên để người dùng có thể phát và tạm dừng video bằng video.play()video.pause() tuỳ thuộc vào trạng thái phát.

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

Thay vì điều chỉnh các nút điều khiển video trong trình nghe sự kiện click, chúng ta sử dụng các sự kiện video playpause. Việc sử dụng các sự kiện điều khiển sẽ giúp tăng tính linh hoạt (như chúng ta sẽ thấy sau với API Phiên phát nội dung đa phương tiện) và sẽ cho phép chúng tôi duy trì các chế độ điều khiển ở chế độ đồng bộ nếu trình duyệt can thiệp vào quá trình phát. Khi video bắt đầu phát, chúng tôi thay đổi trạng thái của nút thành "tạm dừng" và ẩn các nút điều khiển video. Khi video tạm dừng, chúng ta chỉ cần thay đổi trạng thái của nút thành "phát" và hiển thị các nút điều khiển video.

video.addEventListener('play', function () {
  playPauseButton.classList.add('playing');
});

video.addEventListener('pause', function () {
  playPauseButton.classList.remove('playing');
});

Khi thời gian được biểu thị bằng thuộc tính currentTime của video thay đổi thông qua sự kiện video timeupdate, chúng tôi cũng cập nhật các chế độ điều khiển tuỳ chỉnh nếu các chế độ điều khiển đó hiển thị.

video.addEventListener('timeupdate', function () {
  if (videoControls.classList.contains('visible')) {
    videoCurrentTime.textContent = secondsToTimeCode(video.currentTime);
    videoProgressBar.style.transform = `scaleX(${
      video.currentTime / video.duration
    })`;
  }
});

Khi video kết thúc, chúng ta chỉ cần thay đổi trạng thái của nút thành "phát", đặt video currentTime về 0 và hiện các nút điều khiển video. Xin lưu ý rằng chúng ta cũng có thể chọn tự động tải một video khác nếu người dùng đã bật một số loại tính năng "Tự động phát".

video.addEventListener('ended', function () {
  playPauseButton.classList.remove('playing');
  video.currentTime = 0;
});

Tua lại và tua đi

Hãy tiếp tục và thêm các nút "tìm kiếm phía sau" và "tìm kiếm phía trước" để người dùng có thể dễ dàng bỏ qua một số nội dung.

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

Như trước đây, thay vì điều chỉnh kiểu video trong trình nghe sự kiện click của các nút này, chúng ta sẽ sử dụng sự kiện video seekingseeked được kích hoạt để điều chỉnh độ sáng của video. Lớp CSS seeking tuỳ chỉnh của tôi cũng đơn giản như filter: brightness(0);.

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

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

Dưới đây là những nội dung chúng tôi đã tạo cho đến thời điểm này. Trong phần tiếp theo, chúng ta sẽ triển khai nút toàn màn hình.

Toàn màn hình

Ở đây, chúng ta sẽ tận dụng một số API web để tạo ra trải nghiệm toàn màn hình hoàn hảo và liền mạch. Để xem ví dụ thực tế, hãy xem mẫu.

Rõ ràng là bạn không cần phải sử dụng tất cả. Chỉ cần chọn những luồng phù hợp với bạn và kết hợp chúng để tạo ra luồng tuỳ chỉnh của bạn.

Ngăn chế độ tự động chuyển sang chế độ toàn màn hình

Trên iOS, các phần tử video sẽ tự động chuyển sang chế độ toàn màn hình khi quá trình phát nội dung đa phương tiện bắt đầu. Vì chúng tôi đang cố gắng điều chỉnh và điều chỉnh nhiều nhất có thể trải nghiệm nội dung đa phương tiện trên các trình duyệt cho thiết bị di động, bạn nên đặt thuộc tính playsinline của phần tử video để buộc phần tử đó phát cùng dòng trên iPhone và không chuyển sang chế độ toàn màn hình khi bắt đầu phát. Xin lưu ý rằng điều này không có tác dụng phụ trên các trình duyệt khác.

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

Bật/tắt chế độ toàn màn hình khi nhấp vào nút

Bây giờ, chúng ta đã ngăn chặn chế độ toàn màn hình tự động, nên chúng ta cần xử lý chế độ toàn màn hình cho video bằng API toàn màn hình. Khi người dùng nhấp vào "nút toàn màn hình", hãy thoát khỏi chế độ toàn màn hình bằng document.exitFullscreen() nếu tài liệu hiện đang sử dụng chế độ toàn màn hình. Nếu không, hãy yêu cầu chế độ toàn màn hình trên vùng chứa video bằng phương thức requestFullscreen() nếu có hoặc chỉ dùng phương thức dự phòng cho webkitEnterFullscreen() trên phần tử video trong 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);
});

Bật/tắt chế độ toàn màn hình khi thay đổi hướng màn hình

Khi người dùng xoay thiết bị ở chế độ ngang, hãy tỉnh táo và tự động yêu cầu chế độ toàn màn hình để tạo ra trải nghiệm sống động. Để làm được điều này, chúng ta sẽ cần Screen Orientation API (API Hướng màn hình). API này chưa được hỗ trợ ở mọi nơi và vẫn có tiền tố trong một số trình duyệt tại thời điểm đó. Do đó, đây sẽ là tính năng nâng cao tiến bộ đầu tiên của chúng tôi.

Cơ chế này hoạt động như thế nào? Ngay khi chúng tôi phát hiện thấy hướng màn hình thay đổi, hãy yêu cầu cửa sổ trình duyệt toàn màn hình nếu cửa sổ trình duyệt ở chế độ ngang (nghĩa là chiều rộng của cửa sổ lớn hơn chiều cao). Nếu không, hãy thoát khỏi chế độ toàn màn hình. Chỉ có vậy thôi.

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

Màn hình khoá ở chế độ ngang khi nhấp vào nút

Vì video có thể được xem tốt hơn ở chế độ ngang, nên chúng ta có thể muốn khoá màn hình ở chế độ ngang khi người dùng nhấp vào "nút toàn màn hình". Chúng ta sẽ kết hợp API Hướng màn hình đã dùng trước đó và một số truy vấn nội dung đa phương tiện để đảm bảo bạn có trải nghiệm tốt nhất.

Việc khoá màn hình ở chế độ ngang cũng dễ dàng như gọi screen.orientation.lock('landscape'). Tuy nhiên, chúng ta chỉ nên làm việc này khi thiết bị ở chế độ dọc với matchMedia('(orientation: portrait)') và có thể cầm bằng một tay bằng matchMedia('(max-device-width: 768px)') vì đây sẽ không phải là trải nghiệm tuyệt vời cho người dùng trên máy tính bảng.

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

Thay đổi hướng màn hình khi thiết bị mở khoá

Bạn có thể nhận thấy trải nghiệm màn hình khoá mà chúng ta vừa tạo chưa hoàn hảo, vì chúng ta không nhận được các thay đổi về hướng màn hình khi màn hình đang khoá.

Để khắc phục vấn đề này, hãy sử dụng Device Orientation API (API Hướng thiết bị) nếu có. API này cung cấp thông tin từ phần cứng đo lường vị trí và chuyển động của thiết bị trong không gian: con quay hồi chuyển và la bàn kỹ thuật số cho hướng của thiết bị và gia tốc kế cho vận tốc của thiết bị. Khi chúng tôi phát hiện sự thay đổi về hướng của thiết bị, hãy mở khoá màn hình bằng screen.orientation.unlock() nếu người dùng giữ thiết bị ở chế độ dọc và màn hình bị khoá ở chế độ ngang.

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

Như bạn có thể thấy, đây là trải nghiệm toàn màn hình liền mạch mà chúng ta mong muốn. Để xem ví dụ thực tế, hãy xem mẫu.

Phát trong nền

Khi phát hiện một trang web hoặc một video trên trang web không còn hiển thị, bạn có thể cập nhật số liệu phân tích để phản ánh điều này. Điều này cũng có thể ảnh hưởng đến quá trình phát hiện tại, chẳng hạn như chọn một bản nhạc khác, tạm dừng hoặc thậm chí là hiển thị các nút tuỳ chỉnh cho người dùng.

Tạm dừng thay đổi chế độ hiển thị video trên trang

Với API Chế độ hiển thị trang, chúng ta có thể xác định chế độ hiển thị hiện tại của trang và nhận được thông báo về các thay đổi về chế độ hiển thị. Mã bên dưới sẽ tạm dừng video khi trang bị ẩn. Điều này xảy ra khi phương thức khoá màn hình đang hoạt động hoặc khi bạn chuyển đổi giữa các thẻ.

Hiện nay, hầu hết các trình duyệt trên thiết bị di động đều cung cấp các chế độ kiểm soát bên ngoài trình duyệt để cho phép tiếp tục phát video đã tạm dừng, vì vậy, bạn chỉ nên thiết lập hành vi này nếu người dùng được phép phát trong nền.

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

Hiện/ẩn nút tắt tiếng khi thay đổi chế độ hiển thị video

Nếu sử dụng API Intersection Observer mới, bạn có thể chi tiết hơn nữa mà không mất phí. API này cho bạn biết khi một phần tử được quan sát đi vào hoặc thoát khỏi khung nhìn của trình duyệt.

Hãy hiện/ẩn nút tắt tiếng dựa trên chế độ hiển thị video trên trang. Nếu video đang phát nhưng hiện không hiển thị, nút tắt tiếng mini sẽ hiển thị ở góc dưới cùng bên phải của trang để giúp người dùng kiểm soát âm thanh của video. Sự kiện video volumechange được dùng để cập nhật kiểu nút tắt tiếng.

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

Chỉ phát một video tại một thời điểm

Nếu có nhiều video trên một trang, bạn chỉ nên phát một video và tự động tạm dừng các video khác để người dùng không phải nghe nhiều bản âm thanh phát cùng lúc.

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

Tuỳ chỉnh thông báo về nội dung nghe nhìn

Với API Phiên nội dung nghe nhìn, bạn cũng có thể tuỳ chỉnh thông báo về nội dung nghe nhìn bằng cách cung cấp siêu dữ liệu cho video hiện đang phát. API này cũng cho phép bạn xử lý các sự kiện liên quan đến nội dung đa phương tiện như tìm kiếm hoặc theo dõi thay đổi có thể đến từ thông báo hoặc các phím nội dung đa phương tiện. Để xem ví dụ thực tế, hãy xem mẫu.

Khi ứng dụng web của bạn đang phát âm thanh hoặc video, bạn có thể thấy một thông báo nội dung nghe nhìn nằm trong khay thông báo. Trên Android, Chrome sẽ cố gắng hết sức để hiển thị thông tin phù hợp bằng cách sử dụng tiêu đề tài liệu và hình ảnh biểu tượng lớn nhất có thể tìm thấy.

Hãy xem cách tuỳ chỉnh thông báo về nội dung đa phương tiện này bằng cách thiết lập một số siêu dữ liệu của phiên phát nội dung đa phương tiện (chẳng hạn như tiêu đề, nghệ sĩ, tên đĩa nhạc và hình minh hoạ) bằng API Phiên nội dung đa phương tiện.

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',
      },
    ],
  });
}

Sau khi phát xong, bạn không cần phải "phát hành" phiên phát nội dung đa phương tiện vì thông báo sẽ tự động biến mất. Xin lưu ý rằng navigator.mediaSession.metadata hiện tại sẽ được dùng khi mọi lượt phát bắt đầu. Đây là lý do bạn cần cập nhật thông báo đó để đảm bảo bạn luôn hiển thị thông tin có liên quan trong thông báo về nội dung nghe nhìn.

Nếu ứng dụng web của bạn cung cấp một danh sách phát, thì bạn nên cho phép người dùng di chuyển qua danh sách phát ngay trong thông báo về nội dung nghe nhìn, với một số biểu tượng "Bản nhạc trước" và "Bản nhạc tiếp theo".

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

Xin lưu ý rằng các trình xử lý hành động đối với nội dung đa phương tiện sẽ vẫn tồn tại. Điều này rất giống với mẫu trình nghe sự kiện, ngoại trừ việc xử lý một sự kiện có nghĩa là trình duyệt ngừng thực hiện bất kỳ hành vi mặc định nào và sử dụng hành vi này làm tín hiệu cho biết ứng dụng web của bạn hỗ trợ hành động đối với nội dung đa phương tiện. Do đó, các chế độ kiểm soát thao tác đối với nội dung đa phương tiện sẽ không hiển thị trừ phi bạn đặt trình xử lý hành động thích hợp.

À, việc huỷ thiết lập một trình xử lý hành động đối với nội dung đa phương tiện cũng dễ dàng như việc chỉ định trình xử lý đó cho null.

API Phiên phát nội dung đa phương tiện cho phép bạn hiển thị các biểu tượng thông báo nội dung nghe nhìn "Tua lui" và "Tua đi" nếu bạn muốn kiểm soát khoảng thời gian bỏ qua.

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

Biểu tượng "Phát/Tạm dừng" luôn hiển thị trong thông báo về nội dung nghe nhìn và các sự kiện liên quan sẽ được trình duyệt xử lý tự động. Nếu vì lý do nào đó mà hành vi mặc định không hoạt động, bạn vẫn có thể xử lý các sự kiện phát nội dung đa phương tiện "Phát" và "Tạm dừng".

Điểm thú vị của API Phiên phát nội dung đa phương tiện là khay thông báo không phải là nơi duy nhất hiển thị siêu dữ liệu nội dung đa phương tiện và các chế độ điều khiển. Thông báo về nội dung nghe nhìn được đồng bộ hoá tự động với mọi thiết bị đeo đã ghép nối. Thông tin này cũng hiển thị trên màn hình khoá.

Ý kiến phản hồi