Riproduzione di video sul Web mobile

François Beaufort
François Beaufort

Come creare la migliore esperienza multimediale mobile sul web? Facile! Tutto dipende dal coinvolgimento degli utenti e dall'importanza che dai ai contenuti multimediali su una pagina web. Penso che tutti concordino sul fatto che se il video è il motivo della visita di un utente, l'esperienza dell'utente deve essere immersiva e coinvolgente.

Riproduzione di video sul 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. Per questo motivo, creeremo un'esperienza di visualizzazione semplice per i dispositivi mobili con controlli personalizzati, riproduzione a schermo intero e in background. Puoi provare l'esempio subito 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 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 di riproduzione/pausa, un pulsante per la visualizzazione a schermo intero, pulsanti per scorrere avanti e indietro e alcuni elementi per l'ora corrente, la durata e il monitoraggio del tempo.

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

Leggere i metadati dei video

Innanzitutto, attendiamo il caricamento dei metadati del video per impostarne la durata, l'ora corrente e inizializzare la barra di avanzamento. Tieni presente che la funzione secondsToTimeCode() è una funzione di utilità personalizzata che ho scritto e che converte un numero di secondi in una stringa nel formato "hh:mm:ss", più adatta 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 dei video

Riproduci/Metti in pausa video

Ora che i metadati del video sono stati caricati, aggiungiamo il primo pulsante che consente all'utente di riprodurre e mettere in pausa il video con video.play() e video.pause(), a seconda dello 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. Fare in modo che i controlli siano basati su eventi contribuisce alla flessibilità (come vedremo in seguito con l'API Media Session) e ci consentirà di mantenere i controlli sincronizzati se il browser interviene nella riproduzione. Quando inizia la riproduzione, cambia lo stato del pulsante in "Metti in pausa" e nascondiamo i controlli video. Quando il video viene messo in pausa, abbiamo semplicemente modificato lo stato del pulsante in &quot;Riproduci&quot; e mostrato i controlli video.

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

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

Quando l'ora indicata dall'attributo currentTime del video è cambiata tramite l'evento video timeupdate, aggiorniamo anche i controlli personalizzati, se visibili.

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

Quando il video termina, cambiamo semplicemente lo stato del pulsante in "Riproduci", reimpostamo il valore currentTime del video su 0 e mostriamo i controlli video per il momento. Tieni presente che potremmo anche scegliere di caricare automaticamente un altro video se l'utente ha attivato un qualche tipo di funzionalità di "riproduzione automatica".

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

Andare avanti e indietro

Continuiamo aggiungendo i pulsanti "Avanti" e "Indietro" 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 negli ascoltatori di eventi click di questi pulsanti, utilizzeremo gli eventi video seeking e seeked attivati per regolare la luminosità del video. La mia classe CSS personalizzata seeking è semplice come filter: brightness(0);.

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

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

Di seguito è riportato ciò che abbiamo creato finora. Nella sezione successiva implementeremo il pulsante per lo schermo intero.

Schermo intero

Sfrutteremo diverse API web per creare un'esperienza a schermo intero perfetta e senza interruzioni. Per vederlo in azione, dai un'occhiata al esempio.

Ovviamente, non devi utilizzarli tutti. Scegli quelli più pertinenti per te e combinali per creare il tuo flusso personalizzato.

Impedire lo schermo intero automatico

Su iOS, gli elementi video attivano automaticamente la modalità a schermo intero all'avvio della riproduzione dei contenuti multimediali. Poiché stiamo cercando di personalizzare e controllare il più possibile la nostra esperienza multimediale sui browser mobile, ti consiglio di impostare l'attributo playsinline dell'elemento video per forzare la riproduzione in linea su iPhone e non entrare in modalità a schermo intero all'inizio della riproduzione. Tieni presente che questa operazione 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 del pulsante

Ora che abbiamo impedito lo schermo intero automatico, dobbiamo gestire autonomamente la modalità schermo intero per il video con l'API Fullscreen. Quando l'utente fa clic sul"pulsante a schermo intero", esci dalla modalità a schermo intero con document.exitFullscreen() se la modalità a schermo intero è attualmente in uso dal documento. In caso contrario, richiedi lo schermo intero nel contenitore video con il metodo requestFullscreen(), se disponibile, oppure utilizza 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 schermo intero al cambio dell'orientamento dello schermo

Quando l'utente ruota il dispositivo in modalità Orizzontale, facciamoci furbi e richiediamo automaticamente lo schermo intero per creare un'esperienza immersiva. Per farlo, abbiamo bisogno dell'API Screen Orientation, che non è ancora supportata ovunque e che al momento ha ancora un prefisso in alcuni browser. Pertanto, questo sarà il nostro primo miglioramento progressivo.

Come funziona? Non appena rileviamo le variazioni dell'orientamento dello schermo, dobbiamo richiedere lo schermo intero se la finestra del browser è in modalità Orizzontale (ovvero se la larghezza è maggiore dell'altezza). In caso contrario, esci 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();
    }
  });
}

Schermata di blocco in orizzontale al clic sul pulsante

Poiché il video può essere visualizzato meglio in modalità Orizzontale, potremmo voler bloccare la schermata in modalità Orizzontale quando l'utente fa clic sul "pulsante a schermo intero". Combineremo l'API Screen Orientation utilizzata in precedenza e alcune query media per garantire la migliore esperienza possibile.

Bloccare la schermata in orizzontale è facile come chiamarescreen.orientation.lock('landscape'). Tuttavia, dobbiamo farlo solo quando il dispositivo è in modalità Ritratto con matchMedia('(orientation: portrait)') e può essere tenuto in una mano con matchMedia('(max-device-width: 768px)'), in quanto non sarebbe un'esperienza ottimale per gli utenti su 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');
  }
}

Sbloccare lo schermo in caso di modifica dell'orientamento del dispositivo

Potresti aver notato, però, che l'esperienza di blocco dello schermo che abbiamo appena creato non è perfetta, dato che non riceviamo modifiche dell'orientamento dello schermo quando lo schermo è bloccato.

Per risolvere il problema, utilizziamo l'API Device Orientation, se disponibile. Questa API fornisce informazioni 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 cambio di orientamento del dispositivo, sblocchiamo lo schermo con screen.orientation.unlock() se l'utente tiene il dispositivo in modalità Ritratto 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 puoi vedere, questa è l'esperienza a schermo intero senza interruzioni che cercavamo. Per vedere come funziona, guarda questo esempio.

Riproduzione in background

Quando rilevi che una pagina web o un video al suo interno non è più visibile, potrebbe essere opportuno aggiornare i dati e le analisi in modo che riflettano questa situazione. Questo potrebbe influire sulla riproduzione corrente, ad esempio scegliere una traccia diversa, metterla in pausa o persino mostrare pulsanti personalizzati all'utente.

Mettere in pausa il video in caso di modifica della visibilità della pagina

Con l'API Page Visibility, possiamo determinare la visibilità attuale di una pagina e ricevere una notifica in caso di modifiche alla visibilità. Il codice riportato di seguito mette in pausa il video quando la pagina viene nascosta. Ciò accade quando il blocco schermo è attivo o quando, ad esempio, cambi scheda.

Poiché la maggior parte dei browser mobile ora offre controlli esterni che consentono di riprendere un video in pausa, ti consiglio di impostare questo comportamento solo se l'utente è autorizzato a riprodurre i video in background.

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

Mostrare/nascondere il pulsante di disattivazione audio in caso di modifica della visibilità del video

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

Mostriamo/nascondiamo un pulsante di disattivazione dell'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à mostrato un mini pulsante di disattivazione dell'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 dell'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 in una pagina sono presenti più video, ti consiglio di riprodurne solo uno e mettere in pausa automaticamente gli altri 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();
  });
}

Personalizzazione delle notifiche per i contenuti multimediali

Con l'API Media Session, puoi anche personalizzare le notifiche multimediali fornendo i metadati del video in riproduzione. Inoltre, consente di gestire eventi relativi ai contenuti multimediali, come la ricerca o la modifica dei canali, che possono provenire da notifiche o tasti multimediali. Per vedere come funziona, consulta il campione.

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

Vediamo come personalizzare questa notifica multimediale impostando alcuni metadati della sessione multimediale, come titolo, artista, nome dell'album e 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 devi "rilasciare" la sessione multimediale perché la notifica scompare automaticamente. Tieni presente che verrà utilizzato il valore corrente di navigator.mediaSession.metadata all'avvio di qualsiasi riproduzione. Per questo motivo, devi aggiornarlo per assicurarti di mostrare sempre informazioni pertinenti nella notifica multimediale.

Se la tua app web fornisce una playlist, ti consigliamo di consentire all'utente di navigare tra i brani direttamente dalla notifica multimediale con alcune icone "Brano precedente" e "Brano successivo".

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 rimarranno invariati. È molto simile al pattern di ascoltatore di eventi, tranne per il fatto che la gestione di un evento comporta l'interruzione del comportamento predefinito del browser e viene utilizzato come indicatore del fatto che la tua app web supporta l'azione multimediale. Pertanto, i controlli delle azioni multimediali non verranno visualizzati a meno che non imposti il gestore delle azioni appropriato.

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

L'API Media Session ti consente di mostrare le icone di notifica multimediale "Avanti veloce" e "Indietro veloce" se vuoi controllare la quantità di tempo saltata.

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 visualizzata nella notifica multimediale e gli eventi correlati vengono gestiti automaticamente dal browser. Se per qualche motivo il comportamento predefinito non funziona, puoi comunque gestire gli eventi media "Riproduci" e "Metti in pausa".

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

Feedback