Reprodução de vídeo da Web para dispositivos móveis

François Beaufort
François Beaufort

Como criar a melhor experiência de mídia para dispositivos móveis na Web? Fácil! Tudo depende do engajamento do usuário e da importância que você dá à mídia em uma página da Web. Todos concordamos que, se o vídeo é O motivo da visita de um usuário, a experiência deve ser imersiva e reengajada.

reprodução de vídeo na Web para dispositivos móveis

Neste artigo, mostro como melhorar de forma progressiva sua experiência de mídia e torná-la mais imersiva graças a uma infinidade de APIs da Web. Por isso, vamos criar uma experiência simples de player para dispositivos móveis com controles personalizados, tela cheia e reprodução em segundo plano. Teste o exemplo agora e encontre o código no repositório do GitHub.

Controles personalizados

Layout HTML
Figura 1.Layout HTML

Como você pode ver, o layout HTML que vamos usar para o player de mídia é bem simples: um elemento raiz <div> contém um elemento de mídia <video> e um elemento filho <div> dedicado aos controles de vídeo.

Os controles de vídeo que vamos abordar mais adiante incluem: um botão de reprodução/pausa, um botão de tela cheia, botões de avanço e recuo e alguns elementos para o tempo atual, a duração e o acompanhamento do tempo.

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

Ler metadados de vídeo

Primeiro, vamos esperar que os metadados do vídeo sejam carregados para definir a duração do vídeo, o horário atual e inicializar a barra de progresso. A função secondsToTimeCode() é uma função utilitária personalizada que eu escrevi para converter um número de segundos em uma string no formato "hh:mm:ss", que é mais adequado para nosso 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
  })`;
});
somente metadados do vídeo
Figura 2. Media Player mostrando metadados do vídeo

Assistir/pausar o vídeo

Agora que os metadados do vídeo foram carregados, vamos adicionar o primeiro botão que permite ao usuário reproduzir e pausar o vídeo com video.play() e video.pause(), dependendo do estado de reprodução.

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

Em vez de ajustar os controles de vídeo no listener de eventos click, usamos os eventos de vídeo play e pause. Fazer nossos controles com base em eventos aumenta a flexibilidade (como veremos mais adiante com a API Media Session) e permite manter nossos controles sincronizados caso o navegador intervém na reprodução. Quando o vídeo começa a ser reproduzido, mudamos o estado do botão para "pausar" e ocultamos os controles do vídeo. Quando o vídeo é pausado, simplesmente mudamos o estado do botão para "reproduzir" e mostramos os controles do vídeo.

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

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

Quando o tempo indicado pelo atributo currentTime do vídeo muda pelo evento de vídeo timeupdate, também atualizamos os controles personalizados, se eles estiverem visíveis.

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

Quando o vídeo termina, simplesmente mudamos o estado do botão para "reproduzir", definimos o currentTime de volta para 0 e mostramos os controles do vídeo por enquanto. Também podemos optar por carregar automaticamente outro vídeo se o usuário tiver ativado algum tipo de recurso de "Reprodução automática".

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

Avançar e retroceder

Vamos continuar e adicionar os botões "seek backward" e "seek forward" para que o usuário possa pular o conteúdo com facilidade.

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

Como antes, em vez de ajustar o estilo do vídeo nos listeners de eventos click desses botões, usaremos os eventos de vídeo seeking e seeked disparados para ajustar o brilho do vídeo. Minha classe CSS seeking personalizada é tão simples quanto filter: brightness(0);.

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

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

Confira o que criamos até agora. Na próxima seção, vamos implementar o botão de tela cheia.

Tela cheia

Aqui, vamos aproveitar várias APIs da Web para criar uma experiência perfeita e integrada em tela cheia. Para vê-lo em ação, confira este exemplo.

Obviamente, você não precisa usar todos eles. Basta escolher os que fazem sentido para você e combiná-los para criar seu fluxo personalizado.

Impedir a tela cheia automática

No iOS, elementos video entram automaticamente no modo de tela cheia quando a reprodução da mídia é iniciada. Como estamos tentando adaptar e controlar o máximo possível nossa experiência de mídia em navegadores para dispositivos móveis, recomendamos que você defina o atributo playsinline do elemento video para forçar a reprodução inline no iPhone, e não entrar no modo de tela cheia quando a reprodução começar. Isso não tem efeitos colaterais em outros navegadores.

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

Alternar tela cheia ao clicar no botão

Agora que impedimos a tela cheia automática, precisamos lidar com o modo de tela cheia do vídeo com a API Fullscreen. Quando o usuário clicar no "botão de tela cheia", vamos sair do modo de tela cheia com document.exitFullscreen() se o modo de tela cheia estiver em uso pelo documento. Caso contrário, solicite tela cheia no contêiner de vídeo com o método requestFullscreen(), se disponível, ou use webkitEnterFullscreen() no elemento de vídeo somente no 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);
});

Alternar tela cheia na mudança de orientação da tela

À medida que o usuário gira o dispositivo no modo paisagem, vamos ser inteligentes e solicitar automaticamente a tela cheia para criar uma experiência imersiva. Para isso, precisamos da API Screen Orientation, que ainda não tem suporte em todos os lugares e ainda tem prefixo em alguns navegadores. Portanto, esse será nosso primeiro aprimoramento progressivo.

Como funciona? Assim que detectarmos as mudanças na orientação da tela, vamos pedir tela cheia se a janela do navegador estiver no modo paisagem (ou seja, se a largura for maior que a altura). Caso contrário, vamos sair da tela cheia. Isso é tudo.

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

Bloquear a tela no modo paisagem ao clicar no botão

Como o vídeo pode ser melhor visualizado no modo paisagem, talvez seja necessário bloquear a tela no modo paisagem quando o usuário clicar no "botão de tela cheia". Vamos combinar a API Screen Orientation usada anteriormente e algumas consultas de mídia para garantir a melhor experiência.

Bloquear a tela no modo paisagem é tão fácil quanto chamar screen.orientation.lock('landscape'). No entanto, só devemos fazer isso quando o dispositivo está no modo retrato com o matchMedia('(orientation: portrait)') e pode ser segurado em uma mão com o matchMedia('(max-device-width: 768px)'), já que essa não seria uma ótima experiência para usuários em tablets.

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

Desbloquear a tela ao mudar a orientação do dispositivo

Talvez você tenha notado que a experiência da tela de bloqueio que acabamos de criar não é perfeita, já que não recebemos mudanças de orientação da tela quando ela está bloqueada.

Para corrigir isso, vamos usar a API Device Orientation, se disponível. Essa API fornece informações do hardware medindo a posição e o movimento de um dispositivo no espaço: giroscópio e bússola digital para a orientação e acelerômetro para a velocidade. Quando detectarmos uma mudança na orientação do dispositivo, vamos desbloquear a tela com screen.orientation.unlock() se o usuário segurar o dispositivo no modo retrato e a tela estiver bloqueada no modo paisagem.

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

Como você pode ver, essa é a experiência em tela cheia que queríamos. Para ver como isso funciona, confira este exemplo.

Reprodução em segundo plano

Quando você detecta que uma página da Web ou um vídeo nela não está mais visível, atualize as análises para refletir isso. Isso também pode afetar a reprodução atual, como ao escolher uma faixa diferente, pausá-la ou até mesmo mostrar botões personalizados para o usuário, por exemplo.

Pausar o vídeo quando a visibilidade da página mudar

Com a API Page Visibility, podemos determinar a visibilidade atual de uma página e receber notificações sobre mudanças na visibilidade. O código abaixo pausa o vídeo quando a página está oculta. Isso acontece quando o bloqueio de tela está ativo ou quando você alterna entre guias, por exemplo.

Como a maioria dos navegadores para dispositivos móveis agora oferece controles fora do navegador que permitem retomar um vídeo pausado, recomendamos que você defina esse comportamento apenas se o usuário tiver permissão para reproduzir em segundo plano.

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

Mostrar/ocultar o botão de desativar microfone na mudança de visibilidade do vídeo

Se você usar a nova API Intersection Observer, poderá ser ainda mais granular sem custo financeiro. Essa API informa quando um elemento observado entra ou sai da janela de visualização do navegador.

Vamos mostrar/ocultar um botão de desativação do som com base na visibilidade do vídeo na página. Se o vídeo estiver sendo reproduzido, mas não estiver visível, um minibotão de silenciar será mostrado no canto inferior direito da página para dar ao usuário controle sobre o som do vídeo. O evento de vídeo volumechange é usado para atualizar o estilo do botão de silenciar.

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

Reproduzir apenas um vídeo por vez

Se houver mais de um vídeo em uma página, sugiro que você reproduza apenas um e pause os outros automaticamente para que o usuário não precise ouvir várias faixas de áudio ao mesmo tempo.

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

Personalizar notificações de mídia

Com a API Media Session, você também pode personalizar notificações de mídia fornecendo metadados para o vídeo que está sendo reproduzido. Ele também permite processar eventos relacionados à mídia, como a busca ou a mudança de faixa, que podem vir de notificações ou teclas de mídia. Para ver como isso funciona, confira este exemplo.

Quando seu app da Web está reproduzindo áudio ou vídeo, você já pode ver uma notificação de mídia na bandeja de notificações. No Android, o Chrome faz o possível para mostrar as informações adequadas usando o título do documento e a maior imagem de ícone que encontrar.

Vamos aprender a personalizar essa notificação de mídia definindo alguns metadados da sessão de mídia, como título, artista, nome do álbum e arte, com a 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',
      },
    ],
  });
}

Quando a reprodução for concluída, não será necessário "liberar" a sessão de mídia, já que a notificação desaparecerá automaticamente. O navigator.mediaSession.metadata atual será usado quando qualquer reprodução for iniciada. Por isso, é necessário fazer a atualização para garantir que as informações relevantes sejam sempre mostradas na notificação de mídia.

Se o seu app da Web oferecer uma playlist, permita que o usuário navegue por ela diretamente da notificação de mídia com alguns ícones de "Faixa anterior" e "Faixa seguinte".

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

Os manipuladores de ação de mídia vão persistir. Isso é muito semelhante ao padrão de listener de eventos, exceto que o processamento de um evento significa que o navegador para de fazer qualquer comportamento padrão e usa isso como um sinal de que seu app da Web oferece suporte à ação de mídia. Portanto, os controles de ação de mídia não serão mostrados, a menos que você defina o gerenciador de ação adequado.

Aliás, cancelar a configuração de um gerenciador de ações de mídia é tão fácil quanto atribuí-lo a null.

A API Media Session permite mostrar os ícones de notificação de mídia "Seek Backward" e "Seek Forward" se você quiser controlar o tempo ignorado.

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

O ícone "Reproduzir/pausar" é sempre mostrado na notificação de mídia, e os eventos relacionados são processados automaticamente pelo navegador. Se, por algum motivo, o comportamento padrão não funcionar, ainda será possível processar eventos de mídia "Play" e "Pause".

O legal da API MediaSession é que a bandeja de notificações não é o único lugar em que os metadados e controles de mídia ficam visíveis. A notificação de mídia é sincronizada automaticamente com qualquer dispositivo portátil pareado. Ele também aparece na tela de bloqueio.

Feedback