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. Acho que todos concordamos que, se o vídeo é o motivo da visita de um usuário, a experiência dele precisa ser imersiva e envolvente.

reprodução de vídeo da 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, duração e rastreamento de 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
  })`;
});
apenas 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. Criar eventos de controle ajuda com a flexibilidade (como veremos mais adiante com a API MediaSession) e permitirá que mantenhamos nossos controles sincronizados se o navegador interferir 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 de vídeo por enquanto. Também podemos 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, vamos usar os eventos de vídeo seeking e seeked acionados 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 conferir em ação, confira o 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, 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 ao máximo 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 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

Quando o usuário gira o dispositivo no modo paisagem, é preciso ser inteligente 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 de 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 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 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, isso só deve ser feito quando o dispositivo estiver no modo retrato com matchMedia('(orientation: portrait)') e puder ser segurado em uma mão com matchMedia('(max-device-width: 768px)'), já que essa não seria uma ótima experiência para os usuários no 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');
  }
}

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, porque 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 que mede 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 de 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 conferir isso em ação, confira o exemplo.

Reprodução em segundo plano

Quando você detecta que uma página da Web ou um vídeo na página da Web não está mais visível, é recomendável atualizar a análise para refletir isso. Isso também pode afetar a reprodução atual, como escolher uma faixa diferente, pausar ou até mostrar botões personalizados ao usuário.

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

Com a API Page Visibility, podemos determinar a visibilidade atual de uma página e receber notificações sobre mudanças de visibilidade. O código abaixo pausa o vídeo quando a página é 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 o som ao mudar a 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 silenciar 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 conferir isso em ação, consulte o exemplo.

Quando o 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 imagem de ícone maior que puder encontrar.

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

Depois que a reprodução terminar, você não precisará "liberar" a sessão de mídia, porque a notificação vai desaparecer automaticamente. O navigator.mediaSession.metadata atual será usado quando qualquer reprodução começar. Por isso, é necessário fazer atualizações para garantir que as informações relevantes sejam sempre mostradas na notificação de mídia.

Se o 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.

A propósito, é fácil desativar um gerenciador de ação de mídia, basta atribuir a null.

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

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 Media Session é 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