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

François Beaufort
François Beaufort

Como você cria a melhor experiência de mídia móvel na Web? Isso é fácil! Tudo depende do engajamento do usuário e da importância que você dá à mídia em uma página da Web. Acho que todos concordamos que, se o vídeo é o motivo da visita, a experiência do usuário precisa ser imersiva e envolvente.

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

Neste artigo, vou mostrar como melhorar progressivamente sua experiência de mídia e torná-la mais imersiva graças a uma infinidade de APIs da Web. É por isso que vamos criar uma experiência simples para o player de dispositivos móveis com controles personalizados, tela cheia e reprodução em segundo plano. É possível testar o exemplo agora e encontrar o código em nosso repositório do GitHub.

Controles personalizados

Layout HTML
Figura 1.Layout HTML

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

Os controles de vídeo que abordaremos mais tarde incluem: um botão de reproduzir/pausar, um botão de tela cheia, botões de retroceder e avançar, e alguns elementos para rastreamento de tempo, duração e tempo atual.

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

Ler metadados do 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. Observe que a função secondsToTimeCode() é uma função utilitária personalizada que escrevi que converte um número de segundos em uma string no formato "hh:mm:ss", que é mais adequado neste 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. Player de mídia mostrando metadados de vídeo

Assistir/pausar o vídeo

Agora que os metadados do vídeo estão carregados, vamos adicionar nosso primeiro botão que permite que o usuário reproduza e pause 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 nossos controles de vídeo no listener de eventos click, usamos os eventos de vídeo play e pause. Tornar nossos eventos de controle baseados em eventos de controle ajuda na flexibilidade (como veremos mais adiante com a API Media Session) e permitirá manter nossos controles sincronizados se o navegador intervir na reprodução. Quando o vídeo começa a ser reproduzido, mudamos o estado do botão para “pausar” e ocultamos os controles. Quando o vídeo é pausado, apenas mudamos o estado do botão para "reproduzir" e mostramos os controles.

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

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

Quando o horário indicado pelo atributo currentTime do vídeo muda pelo evento de vídeo timeupdate, também atualizamos nossos 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, basta mudar o estado do botão para "reproduzir", definir o currentTime de volta como 0 e mostrar os controles do vídeo por enquanto. Também poderíamos optar por carregar automaticamente outro vídeo se o usuário tiver ativado algum tipo de recurso "Reprodução automática".

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

Voltar e avançar

Vamos continuar adicionando os botões "voltar" e "avançar" para que o usuário possa pular algum 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 de 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');
});

Veja abaixo 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 de tela cheia perfeita e contínua. Confira o exemplo para saber como isso funciona.

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

Impedir a tela cheia automática

No iOS, os elementos video entram automaticamente no modo de tela cheia quando a reprodução de mídia começa. Como estamos tentando personalizar 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>

Ativar/desativar tela cheia ao clicar no botão

Agora que evitamos a tela cheia automática, precisamos processar o modo de tela cheia do vídeo com a API Fullscreen. Quando o usuário clica no "botão de tela cheia", o document.exitFullscreen() é usado para sair do modo de tela cheia, caso o documento esteja em uso no momento. Caso contrário, solicite a 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);
});

Ativar/desativar a tela cheia na orientação da tela

Quando o usuário gira o dispositivo no modo paisagem, considere isso e solicite 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 está prefixada em alguns navegadores na época. Assim, este será nosso primeiro aprimoramento progressivo.

Como isso funciona? Assim que detectarmos as mudanças na orientação da tela, solicitaremos a tela cheia se a janela do navegador estiver no modo paisagem (ou seja, a largura for maior que a altura). Caso contrário, vamos sair do modo de 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 em paisagem ao clicar no botão

Como o vídeo pode ter uma visualização melhor no modo paisagem, recomendamos bloquear a tela nesse modo 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 que essa experiência seja a melhor.

Bloquear a tela no modo paisagem é tão fácil quanto chamar screen.orientation.lock('landscape'). No entanto, precisamos fazer isso somente quando o dispositivo está no modo retrato com matchMedia('(orientation: portrait)') e pode ser segurado com matchMedia('(max-device-width: 768px)') na mão, já que isso não seria uma ótima experiência para usuários de 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 tela quando a orientação do dispositivo é alterada

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

Para corrigir isso, vamos usar a API Device Orientation, se disponível. Essa API fornece informações do hardware que mede a posição e o movimento de um dispositivo no espaço: giroscópio e compasso digital para a orientação dele e o acelerômetro para a velocidade. Quando detectarmos uma mudança na orientação do dispositivo, desbloquearemos 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, esta é a experiência perfeita de tela cheia que estávamos procurando. Confira o exemplo para entender isso na prática.

Reprodução em segundo plano

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

Pausar vídeo na mudança na visibilidade da página

Com a API Page Visibility, podemos determinar a visibilidade atual de uma página e receber avisos sobre mudanças. 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 de guia, 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 somente 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 quando a visibilidade do vídeo mudar

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

Vamos mostrar/ocultar um botão de desativar som com base na visibilidade do vídeo na página. Se o vídeo estiver sendo reproduzido, mas não estiver visível no momento, um minibotão de desativar microfone 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 desativar som.

<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, sugerimos que você reproduza apenas um e pause os outros automaticamente para que o usuário não precise ouvir várias faixas de áudio tocando 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, também é possível personalizar as notificações de mídia fornecendo metadados para o vídeo que está sendo reproduzido. Ela também permite processar eventos relacionados a mídia, como busca ou mudanças de rastreamento, que podem ser provenientes de notificações ou teclas de mídia. Para ver isso na prática, 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 pode encontrar.

Vamos ver como personalizar essa notificação de mídia definindo alguns metadados de 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 vai ser necessário "liberar" a sessão de mídia, porque a notificação vai desaparecer automaticamente. Não esqueça que o navigator.mediaSession.metadata atual será usado quando qualquer reprodução for iniciada. É por isso que você precisa atualizá-lo para garantir que sempre mostre informações relevantes na notificação de mídia.

Se o app da Web oferece uma playlist, você pode permitir que o usuário navegue pela playlist diretamente da notificação de mídia, com alguns ícones "Faixa anterior" e "Próxima faixa".

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 gerenciadores de ações de mídia serão mantidos. Ele é muito semelhante ao padrão de listener de eventos, exceto pelo fato de que processar 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 exibidos, a menos que você defina o gerenciador de ações adequado.

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

A API Media Session permite mostrar os ícones de notificação de mídia "Procurar para trás" e "Avançar" se você quiser controlar a quantidade de tempo ignorada.

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, você ainda poderá processar os eventos de mídia "Reproduzir" e "Pausar".

O legal da API Media Session é que a bandeja de notificações não é o único lugar em que os metadados e os controles de mídia ficam visíveis. A notificação de mídia é sincronizada automaticamente com qualquer dispositivo wearable pareado. E também aparece nas telas de bloqueio.

Feedback