Odtwarzanie wideo w internecie mobilnym

François Beaufort
François Beaufort

Jak zapewnić najlepsze wrażenia z korzystania z multimediów na urządzeniach mobilnych w internecie? To proste. Wszystko zależy od zaangażowania użytkowników i znaczenia, jakie przypisujesz multimediom na stronie internetowej. Chyba wszyscy się zgodzimy, że jeśli film jest głównym powodem wizyty użytkownika, to wrażenia użytkownika muszą być wciągające i zachęcające do ponownego zaangażowania.

odtwarzanie filmów w internecie mobilnym

W tym artykule pokazuję, jak stopniowo ulepszać odtwarzanie multimediów i zwiększać immersyjność dzięki licznym interfejsom Web API. Dlatego utworzymy prosty odtwarzacz mobilny z niestandardowymi elementami sterującymi, pełnym ekranem i odtwarzaniem w tle. Możesz teraz wypróbować przykład i znaleźć kod w naszym repozytorium na GitHubie.

Ustawienia niestandardowe

Układ HTML
Rysunek 1. Układ HTML

Jak widać, układ HTML, którego użyjemy w naszym odtwarzaczu multimedialnym, jest dość prosty: element główny <div> zawiera element multimedialny <video> i element podrzędny <div> przeznaczony do sterowania odtwarzaniem.

Elementy sterujące odtwarzaniem, które omówimy później, obejmują: przycisk odtwarzania/wstrzymania, przycisk pełnego ekranu, przyciski przewijania do tyłu i do przodu oraz elementy do śledzenia bieżącego czasu, czasu trwania i czasu.

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

Odczytywanie metadanych filmu

Najpierw poczekajmy na wczytanie metadanych filmu, aby ustawić czas trwania filmu i bieżący czas oraz zainicjować pasek postępu. Zwróć uwagę, że funkcja secondsToTimeCode() to niestandardowa funkcja użytkowa, którą napisałem, aby konwertować liczbę sekund na ciąg znaków w formacie „gg:mm:ss”, który w naszym przypadku jest bardziej odpowiedni.

<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
  })`;
});
tylko metadane filmu
Rysunek 2. Odtwarzacz multimedialny wyświetlający metadane filmu

Odtwórz/wstrzymaj film

Metadane filmu zostały wczytane, więc dodajmy pierwszy przycisk, który pozwoli użytkownikowi odtwarzać i wstrzymywać film za pomocą video.play() i video.pause() w zależności od stanu odtwarzania.

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

Zamiast dostosowywać elementy sterujące odtwarzaniem wideo w detektorze zdarzeń click, używamy zdarzeń wideo play i pause. Oparcie elementów sterujących na zdarzeniach zwiększa elastyczność (jak zobaczymy później w przypadku interfejsu Media Session API) i pozwala nam synchronizować elementy sterujące, jeśli przeglądarka ingeruje w odtwarzanie. Gdy rozpoczyna się odtwarzanie filmu, zmieniamy stan przycisku na „wstrzymaj” i ukrywamy elementy sterujące odtwarzaniem. Gdy odtwarzanie filmu zostanie wstrzymane, zmieniamy stan przycisku na „Odtwórz” i wyświetlamy elementy sterujące odtwarzaniem.

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

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

Gdy czas wskazany przez atrybut currentTime filmu zmieni się w ramach zdarzenia wideo timeupdate, aktualizujemy też niestandardowe elementy sterujące, jeśli są widoczne.

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

Po zakończeniu filmu zmieniamy stan przycisku na „Odtwórz”, ustawiamy czas filmu currentTime na 0 i wyświetlamy elementy sterujące odtwarzaniem. Możemy też automatycznie wczytać kolejny film, jeśli użytkownik włączył funkcję autoodtwarzania.

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

Przewijanie do tyłu i do przodu

Przejdźmy dalej i dodajmy przyciski „Przewiń do tyłu” i „Przewiń do przodu”, aby użytkownik mógł łatwo pomijać niektóre treści.

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

Podobnie jak wcześniej, zamiast dostosowywać styl filmu w detektorach zdarzeń click tych przycisków, użyjemy wywołanych zdarzeń wideo seeking i seeked, aby dostosować jasność filmu. Moja niestandardowa klasa CSS seeking jest tak prosta jak filter: brightness(0);.

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

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

Poniżej znajdziesz informacje o tym, co do tej pory stworzyliśmy. W następnej sekcji zaimplementujemy przycisk pełnego ekranu.

Pełny ekran

W tym przypadku wykorzystamy kilka interfejsów Web API, aby stworzyć idealne i płynne działanie na pełnym ekranie. Aby zobaczyć, jak to działa, zapoznaj się z przykładem.

Oczywiście nie musisz używać wszystkich. Wybierz te, które Ci odpowiadają, i połącz je, aby utworzyć własny proces.

Blokowanie automatycznego wyświetlania na pełnym ekranie

W systemie iOS elementy video automatycznie przechodzą w tryb pełnoekranowy po rozpoczęciu odtwarzania multimediów. Staramy się jak najlepiej dostosować i kontrolować odtwarzanie multimediów w przeglądarkach mobilnych, dlatego zalecam ustawienie atrybutu playsinline elementu video, aby wymusić odtwarzanie w trybie inline na iPhonie i nie przechodzić w tryb pełnoekranowy po rozpoczęciu odtwarzania. Pamiętaj, że nie ma to efektów ubocznych na inne przeglądarki.

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

Włączanie i wyłączanie pełnego ekranu po kliknięciu przycisku

Skoro uniemożliwiamy automatyczne wyświetlanie na pełnym ekranie, musimy samodzielnie obsłużyć tryb pełnoekranowy filmu za pomocą interfejsu Fullscreen API. Gdy użytkownik kliknie przycisk pełnego ekranu, wyjdziemy z trybu pełnoekranowego za pomocą document.exitFullscreen(), jeśli tryb pełnoekranowy jest obecnie używany przez dokument. W przeciwnym razie poproś o pełny ekran w kontenerze wideo za pomocą metody requestFullscreen(), jeśli jest dostępna, lub użyj metody zastępczej webkitEnterFullscreen() w elemencie wideo tylko na 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);
});

Włączanie/wyłączanie pełnego ekranu po zmianie orientacji ekranu

Gdy użytkownik obróci urządzenie do trybu poziomego, możemy automatycznie poprosić o wyświetlanie na pełnym ekranie, aby zapewnić mu lepsze wrażenia. W tym celu potrzebujemy interfejsu Screen Orientation API, który nie jest jeszcze obsługiwany wszędzie i w niektórych przeglądarkach nadal ma prefiks. Będzie to więc nasze pierwsze progresywne ulepszenie.

Jak to działa? Gdy tylko wykryjemy zmianę orientacji ekranu, poprośmy o pełny ekran, jeśli okno przeglądarki jest w trybie poziomym (czyli jego szerokość jest większa niż wysokość). Jeśli nie, wyjdź z trybu pełnoekranowego. To wszystko.

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

Blokowanie ekranu w orientacji poziomej po kliknięciu przycisku

Filmy mogą być lepiej widoczne w trybie poziomym, dlatego gdy użytkownik kliknie przycisk „Pełny ekran”, możemy zablokować ekran w trybie poziomym. Połączymy używany wcześniej interfejs Screen Orientation API i niektóre zapytania o media, aby zapewnić jak najlepsze wrażenia.

Blokowanie ekranu w trybie poziomym jest tak proste, jak wywołanie funkcji screen.orientation.lock('landscape'). Powinniśmy to jednak robić tylko wtedy, gdy urządzenie jest w trybie portretowym z matchMedia('(orientation: portrait)') i można je trzymać w jednej ręce z matchMedia('(max-device-width: 768px)'), ponieważ nie byłoby to wygodne dla użytkowników tabletów.

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

Odblokuj ekran po zmianie orientacji urządzenia

Możesz zauważyć, że utworzony przez nas ekran blokady nie jest idealny, ponieważ nie otrzymujemy informacji o zmianach orientacji ekranu, gdy jest on zablokowany.

Aby rozwiązać ten problem, użyjemy interfejsu Device Orientation API, jeśli jest dostępny. Ten interfejs API dostarcza informacje ze sprzętu mierzącego pozycję i ruch urządzenia w przestrzeni: żyroskopu i cyfrowego kompasu w celu określenia orientacji oraz akcelerometru w celu określenia prędkości. Gdy wykryjemy zmianę orientacji urządzenia, odblokujemy ekran za pomocą screen.orientation.unlock(), jeśli użytkownik trzyma urządzenie w trybie portretowym, a ekran jest zablokowany w trybie poziomym.

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

Jak widać, jest to płynne działanie na pełnym ekranie, którego oczekiwaliśmy. Aby zobaczyć, jak to działa, zapoznaj się z przykładem.

Odtwarzanie w tle

Jeśli zauważysz, że strona internetowa lub film na niej nie są już widoczne, możesz zaktualizować statystyki, aby to odzwierciedlić. Może to też mieć wpływ na bieżące odtwarzanie, np. wybór innego utworu, wstrzymanie odtwarzania, a nawet wyświetlanie użytkownikowi niestandardowych przycisków.

Wstrzymaj odtwarzanie filmu po zmianie widoczności strony

Dzięki interfejsowi Page Visibility API możemy określić bieżącą widoczność strony i otrzymywać powiadomienia o zmianach widoczności. Kod poniżej wstrzymuje odtwarzanie filmu, gdy strona jest ukryta. Dzieje się tak na przykład, gdy blokada ekranu jest aktywna lub gdy przełączasz karty.

Większość przeglądarek mobilnych oferuje obecnie elementy sterujące poza przeglądarką, które umożliwiają wznowienie wstrzymanego filmu. Zalecam ustawienie tego zachowania tylko wtedy, gdy użytkownik może odtwarzać treści w tle.

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

Pokaż/ukryj przycisk wyciszenia przy zmianie widoczności filmu

Jeśli używasz nowego interfejsu Intersection Observer API, możesz bezpłatnie uzyskać jeszcze bardziej szczegółowe informacje. Ten interfejs API informuje, kiedy obserwowany element pojawia się w obszarze widocznym przeglądarki lub z niego znika.

Pokażmy/ukryjmy przycisk wyciszenia na podstawie widoczności filmu na stronie. Jeśli film jest odtwarzany, ale nie jest obecnie widoczny, w prawym dolnym rogu strony pojawi się mały przycisk wyciszania, który umożliwia użytkownikowi sterowanie dźwiękiem filmu. Zdarzenie wideo volumechange służy do aktualizowania stylu przycisku wyciszania.

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

Odtwarzanie tylko jednego filmu naraz

Jeśli na stronie jest więcej niż 1 film, zalecamy odtwarzanie tylko jednego z nich i automatyczne wstrzymywanie pozostałych, aby użytkownik nie musiał słuchać kilku ścieżek audio jednocześnie.

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

Dostosowywanie powiadomień o multimediach

Za pomocą interfejsu Media Session API możesz też dostosowywać powiadomienia o multimediach, podając metadane aktualnie odtwarzanego filmu. Umożliwia też obsługę zdarzeń związanych z multimediami, takich jak przewijanie czy zmiana ścieżki, które mogą pochodzić z powiadomień lub klawiszy multimedialnych. Aby zobaczyć, jak to działa, zapoznaj się z przykładem.

Gdy aplikacja internetowa odtwarza dźwięk lub obraz, w obszarze powiadomień pojawia się powiadomienie o multimediach. Na Androidzie Chrome stara się wyświetlać odpowiednie informacje, używając tytułu dokumentu i największego obrazu ikony, jaki uda się znaleźć.

Zobaczmy, jak dostosować to powiadomienie o multimediach, ustawiając niektóre metadane sesji multimediów, takie jak tytuł, wykonawca, nazwa albumu i grafika, za pomocą interfejsu 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',
      },
    ],
  });
}

Po zakończeniu odtwarzania nie musisz „zwalniać” sesji multimedialnej, ponieważ powiadomienie automatycznie zniknie. Pamiętaj, że podczas odtwarzania będzie używany bieżący navigator.mediaSession.metadata. Dlatego musisz go aktualizować, aby zawsze wyświetlać odpowiednie informacje w powiadomieniu o multimediach.

Jeśli Twoja aplikacja internetowa udostępnia playlistę, możesz umożliwić użytkownikowi poruszanie się po niej bezpośrednio z powiadomienia o multimediach za pomocą ikon „Poprzedni utwór” i „Następny utwór”.

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

Pamiętaj, że moduły obsługi działań multimedialnych pozostaną. Jest to bardzo podobne do wzorca detektora zdarzeń, z tą różnicą, że obsługa zdarzenia oznacza, że przeglądarka przestaje wykonywać działanie domyślne i używa tego jako sygnału, że aplikacja internetowa obsługuje działanie multimedialne. Dlatego elementy sterujące działaniami multimedialnymi nie będą wyświetlane, dopóki nie ustawisz odpowiedniego modułu obsługi działań.

Przy okazji, usunięcie modułu obsługi działań multimedialnych jest tak proste jak przypisanie go do null.

Interfejs Media Session API umożliwia wyświetlanie ikon powiadomień multimedialnych „Przewiń do tyłu” i „Przewiń do przodu”, jeśli chcesz kontrolować czas pomijania.

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

Ikona „Odtwórz/Wstrzymaj” jest zawsze wyświetlana w powiadomieniu o multimediach, a powiązane zdarzenia są automatycznie obsługiwane przez przeglądarkę. Jeśli z jakiegoś powodu działanie domyślne nie działa, możesz obsługiwać zdarzenia multimedialne „Odtwórz” i „Wstrzymaj”.

Fajne w interfejsie Media Session API jest to, że obszar powiadomień nie jest jedynym miejscem, w którym widoczne są metadane i elementy sterujące multimediów. Powiadomienie o multimediach jest automatycznie synchronizowane z każdym sparowanym urządzeniem do noszenia. Pojawia się też na ekranach blokady.

Prześlij opinię