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 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

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

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.