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.
Neste artigo, mostro 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, 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 nosso repositório do GitHub.
Controles personalizados
Como você pode ver, o layout HTML que vamos usar para 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 aos controles de vídeo.
Os controles de vídeo que vamos abordar mais adiante incluem: um botão de reproduzir/pausar, um botão de tela cheia, botões de retroceder e avançar e alguns elementos para o tempo atual, a duração e o controle 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 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
})`;
});
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. Basear nossos controles em eventos ajuda
na flexibilidade (como veremos mais adiante com a API Media Session) e permite
manter os 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 do vídeo. Quando o vídeo é pausado, basta mudar o estado do botão para "reproduzir" e mostrar 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 de vídeo currentTime é alterado pelo evento de vídeo timeupdate, também atualizamos os controles personalizados, se 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, mudamos o estado do botão para "reproduzir", definimos o
currentTime do vídeo de volta como 0 e mostramos os controles do vídeo por enquanto. Também é possível
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;
});
Voltar e avançar
Vamos continuar e adicionar os botões "retroceder" e "avançar" para que o usuário possa pular 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 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 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 integrada. Para conferir isso em ação, confira o exemplo.
Obviamente, você não precisa usar todos eles. Basta escolher as que fazem sentido para você e combiná-las para criar seu fluxo personalizado.
Impedir 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, recomendo 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 ele estiver sendo usado pelo
documento. Caso contrário, solicite o modo de tela cheia no contêiner de vídeo com o método
requestFullscreen(), se disponível, ou use webkitEnterFullscreen() como substituto no
elemento de vídeo apenas 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 tela cheia ao mudar a orientação da tela
Quando o usuário girar 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 é compatível com todos os navegadores e ainda tem prefixo em alguns deles. Assim, esse será nosso primeiro aprimoramento progressivo.
Como funciona? Assim que detectarmos as mudanças na orientação da tela, vamos solicitar a tela cheia se a janela do navegador estiver no modo paisagem (ou seja, se a largura for maior que a altura). Se não estiver, saia 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 de bloqueio no modo paisagem ao clicar no botão
Como os vídeos podem ser melhor visualizados no modo paisagem, talvez seja interessante bloquear a tela em 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 que essa experiência seja a melhor possível.
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 estiver no modo retrato com matchMedia('(orientation: portrait)') e puder
ser segurado com uma mão com matchMedia('(max-device-width: 768px)'), já que essa
não seria uma boa experiência para usuários de 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
No entanto, você deve ter percebido que a experiência de tela de bloqueio que acabamos de criar não é perfeita, já que não recebemos mudanças na 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 orientação e acelerômetro para velocidade. Quando detectamos uma mudança na orientação do dispositivo, desbloqueamos 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 de tela cheia que estávamos procurando. Para ver isso em ação, confira o exemplo.
Tocar em segundo plano
Quando você detectar que uma página da Web ou um vídeo nela não está mais visível, atualize suas análises para refletir isso. Isso também pode afetar a reprodução atual, como 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ê troca 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, recomendo definir esse comportamento apenas se o usuário tiver permissão para tocar 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 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 permite que você saiba quando um elemento observado entra ou sai da janela de visualização do navegador.
Vamos mostrar/ocultar um botão de desativar microfone com base na visibilidade do vídeo na página. Se
o vídeo estiver sendo reproduzido, mas não estiver visível, um mini botão de desativar som será mostrado no
canto inferior direito da página para que o usuário controle o som do vídeo. O evento de vídeo volumechange é usado para atualizar o estilo do botão de desativar microfone.
<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 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. Ele também permite processar eventos relacionados à mídia, como busca ou mudança de faixa, que podem vir de notificações ou teclas de mídia. Para ver isso em ação, confira 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 maior imagem de ícone que 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',
},
],
});
}
Depois que a reprodução for concluída, não será necessário "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 for iniciada. Por
isso, é necessário atualizá-lo para garantir que você sempre mostre informações
relevantes na notificação de mídia.
Se o app da Web tiver uma playlist, você pode permitir que o usuário navegue por ela diretamente na notificação de mídia com os í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 vão persistir. Isso é muito semelhante ao padrão de listener de eventos, exceto que o processamento de um evento significa que o navegador interrompe qualquer comportamento padrão e usa isso como um sinal de que seu app da Web é compatível com a 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ções adequado.
Aliás, para cancelar a definição de um gerenciador de ações de mídia, basta atribuí-lo a null.
A API Media Session permite mostrar os ícones de notificação de mídia "Voltar" e "Avançar" se você quiser controlar o 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, 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.