Riproduzione di video sul Web mobile

François Beaufort
François Beaufort

Come si fa a creare la migliore esperienza multimediale mobile sul Web? Non c'è problema! Tutto dipende dal coinvolgimento degli utenti e dall'importanza che attribuisci ai media su una pagina web. Penso che siamo tutti d'accordo sul fatto che, se il video è il motivo della visita di un utente, l'esperienza dell'utente deve essere immersiva e ricoinvolgente.

riproduzione di video per web mobile

In questo articolo ti mostrerò come migliorare in modo progressivo la tua esperienza multimediale e renderla più immersiva grazie a una miriade di API web. Ecco perché vogliamo creare un'esperienza semplice per il player mobile con controlli personalizzati, schermo intero e riproduzione in background. Puoi provare subito l'esempio e trovare il codice nel nostro repository GitHub.

Controlli personalizzati

Layout HTML
Figura 1.Layout HTML

Come puoi vedere, il layout HTML che utilizzeremo per il nostro media player è piuttosto semplice: un elemento principale <div> contiene un elemento multimediale <video> e un elemento secondario <div> dedicato ai controlli video.

I controlli video che tratteremo più avanti includono: un pulsante Riproduci/Pausa, un pulsante per la modalità a schermo intero, i pulsanti per andare avanti e indietro e alcuni elementi per il monitoraggio dell'ora attuale, della durata e del tempo.

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

Lettura dei metadati dei video

Prima di tutto, attendi che vengano caricati i metadati del video per impostare la durata del video e l'ora corrente e inizializzare la barra di avanzamento. Tieni presente che la funzione secondsToTimeCode() è una funzione di utilità personalizzata che ho scritto e converte un numero di secondi in una stringa nel formato "hh:mm:ss", il che è più adatto nel nostro caso.

<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
  })`;
});
solo metadati video
Figura 2. Media Player che mostra i metadati del video

Riproduci/Metti in pausa video

Ora che i metadati del video sono stati caricati, aggiungiamo il nostro primo pulsante che consente all'utente di riprodurre e mettere in pausa il video con video.play() e video.pause(), a seconda del relativo stato di riproduzione.

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

Anziché modificare i controlli video nel listener di eventi click, utilizziamo gli eventi video play e pause. Rendere i nostri controlli basati sugli eventi è utile in termini di flessibilità (come vedremo più avanti con l'API Media Session) e ci permetterà di mantenere i nostri controlli sincronizzati se il browser interviene nella riproduzione. Quando inizia la riproduzione del video, modifichiamo lo stato del pulsante in "Metti in pausa" e nascondiamo i controlli video. Quando il video viene messo in pausa, semplicemente modifichiamo lo stato del pulsante in "Riproduci" e mostriamo i controlli video.

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

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

Quando il tempo indicato dall'attributo video currentTime viene modificato tramite l'evento video timeupdate, aggiorniamo anche i nostri controlli personalizzati, se sono visibili.

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

Al termine del video, modifichiamo semplicemente lo stato del pulsante in "riproduci", impostiamo il video currentTime su 0 e mostriamo per il momento i controlli video. Tieni presente che potremmo anche decidere di caricare automaticamente un altro video se l'utente ha attivato una qualche tipo di funzionalità di "riproduzione automatica".

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

Vai avanti e indietro

Proseguiamo aggiungendo i pulsanti "Vai indietro " e"Vai avanti " in modo che l'utente possa saltare facilmente alcuni contenuti.

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

Come in precedenza, anziché modificare lo stile del video nei listener di eventi click di questi pulsanti, utilizzeremo gli eventi video seeking e seeked attivati per regolare la luminosità del video. La mia classe CSS seeking personalizzata è semplice come filter: brightness(0);.

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

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

Ecco di seguito ciò che abbiamo creato finora. Nella prossima sezione implementeremo il pulsante per la modalità a schermo intero.

Schermo intero

Qui sfrutteremo diverse API web per creare un'esperienza a schermo intero perfetta e senza interruzioni. Per vedere come funziona, guarda questo esempio.

Ovviamente, non è necessario utilizzarli tutti. Devi solo scegliere quelle più adatte alle tue esigenze e combinarle per creare il tuo flusso personalizzato.

Impedisci schermo intero automatico

Su iOS, gli elementi video entrano automaticamente in modalità a schermo intero quando inizia la riproduzione dei contenuti multimediali. Poiché cerchiamo di personalizzare e controllare il più possibile la nostra esperienza multimediale sui browser per dispositivi mobili, ti consiglio di impostare l'attributo playsinline dell'elemento video in modo da forzarne la riproduzione in linea su iPhone e di non attivare la modalità a schermo intero quando inizia la riproduzione. Tieni presente che questo non ha effetti collaterali su altri browser.

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

Attiva/disattiva schermo intero al clic sul pulsante

Ora che impediamo la modalità a schermo intero automatico, dobbiamo gestire la modalità a schermo intero per il video con l'API Fullscreen. Quando l'utente fa clic sul pulsante"Schermo intero", usciamo dalla modalità a schermo intero con document.exitFullscreen() se questa modalità è attualmente in uso per il documento. In caso contrario, richiedi la modalità a schermo intero nel contenitore video con il metodo requestFullscreen(), se disponibile, oppure utilizza il metodo di riserva webkitEnterFullscreen() per l'elemento video solo su 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);
});

Attiva/disattiva la modifica dell'orientamento dello schermo a schermo intero

Mentre l'utente ruota il dispositivo in modalità Orizzontale, cerchiamo di fare chiarezza su questo aspetto e richiediamo automaticamente lo schermo intero per creare un'esperienza coinvolgente. A questo scopo, è necessaria l'API Screen Orientation, che non è ancora supportata ovunque e che all'epoca era ancora prefisso in alcuni browser. Questo sarà quindi il nostro primo miglioramento progressivo.

Come funziona? Non appena rileviamo i cambiamenti di orientamento dello schermo, richiediamo lo schermo intero se la finestra del browser è in modalità Orizzontale, ovvero se la larghezza è maggiore della sua altezza. In caso contrario usciamo dalla modalità a schermo intero. È tutto.

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

Blocca schermo in orizzontale al clic sul pulsante

Poiché la visualizzazione del video è migliore in modalità Orizzontale, ti consigliamo di bloccare lo schermo in orizzontale quando l'utente fa clic sul "pulsante Schermo intero". Combineremo l'API Screen Orientation utilizzata in precedenza con alcune query media per assicurarci che questa esperienza sia la migliore.

Bloccare lo schermo in orizzontale è facile come chiamare screen.orientation.lock('landscape'). Tuttavia, consigliamo di eseguire questa operazione solo quando il dispositivo è in modalità verticale con matchMedia('(orientation: portrait)') e può essere tenuto in una mano con matchMedia('(max-device-width: 768px)'), poiché non sarebbe un'esperienza ottimale per gli utenti di tablet.

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

Sblocca schermo quando cambia l'orientamento del dispositivo

Anche se avrai notato che l'esperienza della schermata di blocco appena creata non è perfetta, in quanto non riceviamo cambiamenti di orientamento dello schermo quando questo è bloccato.

Per risolvere il problema, utilizziamo l'API Device Orientation, se disponibile. Questa API fornisce informazioni provenienti dall'hardware che misura la posizione e il movimento di un dispositivo nello spazio: giroscopio e bussola digitale per il suo orientamento e accelerometro per la sua velocità. Quando rileviamo un cambiamento di orientamento del dispositivo, sblocca lo schermo con screen.orientation.unlock() se l'utente tiene il dispositivo in modalità verticale e lo schermo è bloccato in modalità orizzontale.

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

Come si può vedere, si tratta dell'esperienza a schermo intero che stavamo cercando. Per vedere un esempio pratico, guarda questo esempio.

Riproduzione in background

Quando rilevi che una pagina web o un video non è più visibile, ti consigliamo di aggiornare i dati e le analisi di conseguenza. Questo potrebbe influenzare anche la riproduzione corrente, ad esempio per scegliere una traccia diversa, metterla in pausa o persino mostrare pulsanti personalizzati all'utente.

Metti in pausa la modifica della visibilità del video sulla pagina

Con l'API Page Visibility possiamo determinare la visibilità attuale di una pagina e ricevere notifiche relative alle modifiche della visibilità. Il codice riportato di seguito mette in pausa il video quando la pagina è nascosta. Questo accade quando è attivo il blocco schermo o quando passi da una scheda all'altra.

Poiché la maggior parte dei browser per dispositivi mobili ora offre controlli esterni al browser che consentono di riprendere un video in pausa, ti consiglio di impostare questo comportamento solo se l'utente può riprodurre in background.

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

Mostra/nascondi il tasto di disattivazione audio in caso di modifica della visibilità video

Se utilizzi la nuova API Intersection eventr, puoi essere ancora più granulare senza costi. Questa API consente di sapere quando un elemento osservato entra o esce dall'area visibile del browser.

Mostra/nascondi un pulsante di disattivazione audio in base alla visibilità del video nella pagina. Se il video è in riproduzione ma non è attualmente visibile, nell'angolo in basso a destra della pagina verrà visualizzato un mini pulsante di disattivazione audio per consentire all'utente di controllare l'audio del video. L'evento video volumechange viene utilizzato per aggiornare lo stile del pulsante di disattivazione audio.

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

Riproduci un solo video alla volta

Se ci sono più video in una pagina, ti suggerisco di riprodurre un solo video e di mettere in pausa gli altri automaticamente in modo che l'utente non debba ascoltare più tracce audio contemporaneamente.

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

Personalizza le notifiche di contenuti multimediali

Con l'API Media Session, puoi anche personalizzare le notifiche multimediali fornendo i metadati per il video in riproduzione. Ti consente inoltre di gestire eventi relativi ai contenuti multimediali, come la ricerca o il cambio di traccia, che possono provenire dalle notifiche o dai tasti multimediali. Per vedere come funziona, guarda questo esempio.

Quando l'app web sta riproducendo audio o video, puoi già vedere una notifica multimediale nella barra delle notifiche. Su Android, Chrome fa del suo meglio per mostrare le informazioni appropriate usando il titolo del documento e l'immagine dell'icona più grande che riesce a trovare.

Vediamo come personalizzare questa notifica di contenuti multimediali impostando alcuni metadati della sessione multimediale, come il titolo, l'artista, il nome dell'album e l'artwork con l'API Media Session.

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

Al termine della riproduzione, non è necessario "rilasciare" la sessione multimediale, perché la notifica scompare automaticamente. Tieni presente che all'avvio della riproduzione verrà usata l'attuale navigator.mediaSession.metadata. Per questo motivo, devi aggiornarla per assicurarti di visualizzare sempre informazioni pertinenti nella notifica per i contenuti multimediali.

Se la tua app web fornisce una playlist, puoi consentire all'utente di sfogliare la playlist direttamente dalla notifica relativa ai contenuti multimediali con alcune icone "Traccia precedente" e "Traccia successiva".

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

Tieni presente che i gestori delle azioni multimediali continueranno a essere disponibili. È molto simile al pattern di ascolto di eventi, tranne per il fatto che, per gestire un evento, il browser smette di eseguire qualsiasi comportamento predefinito e lo utilizza per indicare che l'app web supporta l'azione multimediale. Di conseguenza, i controlli delle azioni multimediali non verranno mostrati a meno che non imposti il gestore delle azioni corretto.

A proposito, annullare l'impostazione di un gestore delle azioni multimediali è facile come assegnarlo a null.

L'API Media Session ti consente di mostrare le icone di notifica multimediali "Vai indietro" e "Vai avanti" se vuoi controllare il tempo saltato.

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

L'icona "Riproduci/Metti in pausa" viene sempre mostrata nella notifica dei contenuti multimediali e i relativi eventi vengono gestiti automaticamente dal browser. Se per qualche motivo il comportamento predefinito non funziona, puoi comunque gestire gli eventi multimediali "Riproduci" e "Metti in pausa".

L'aspetto interessante dell'API Media Session è che la barra delle notifiche non è l'unico punto in cui sono visibili i metadati e i controlli dei contenuti multimediali. La notifica multimediale viene sincronizzata automaticamente con qualsiasi dispositivo indossabile accoppiato. Inoltre, viene visualizzato nelle schermate di blocco.

Feedback