Personalizar notificações de mídia e gerenciar playlists

François Beaufort
François Beaufort

Com a nova API Media Session, agora é possível personalizar as notificações de mídia fornecendo metadados para a mídia que seu app da Web está reproduzindo. Ele também permite que você processe eventos relacionados a mídia, como busca ou rastreamento de mudanças que podem ser provenientes de notificações ou chaves de mídia. Gostou? Teste os exemplos oficiais de sessão de mídia (link em inglês).

A API Media Session é compatível com o Chrome 57 (Beta em fevereiro de 2017 e estável em março de 2017).

Resumo da sessão de mídia;
Foto de Michael Alø-Nielsen / CC BY 2.0

Me dê o que eu quero

Você já conhece a API Media Session e está voltando para copiar e colar sem vergonha do código boilerplate? Aqui está ele.

if ('mediaSession' in navigator) {

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

    navigator.mediaSession.setActionHandler('play', function() {});
    navigator.mediaSession.setActionHandler('pause', function() {});
    navigator.mediaSession.setActionHandler('seekbackward', function() {});
    navigator.mediaSession.setActionHandler('seekforward', function() {});
    navigator.mediaSession.setActionHandler('previoustrack', function() {});
    navigator.mediaSession.setActionHandler('nexttrack', function() {});
}

Entrar no código

Vamos jogar 🎷

Adicione um elemento <audio> simples à sua página da Web e atribua várias fontes de mídia para que o navegador possa escolher qual funciona melhor.

<audio controls>
    <source src="audio.mp3" type="audio/mp3"/>
    <source src="audio.ogg" type="audio/ogg"/>
</audio>

Como você deve saber, o autoplay está desativado para elementos de áudio no Chrome para Android, o que significa que precisamos usar o método play() do elemento de áudio. Esse método precisa ser acionado por um gesto do usuário, como um toque ou um clique do mouse. Isso significa detectar eventos pointerup, click e touchend. Em outras palavras, o usuário precisa clicar em um botão antes que seu app da Web possa realmente fazer barulho.

playButton.addEventListener('pointerup', function(event) {
    let audio = document.querySelector('audio');

    // User interacted with the page. Let's play audio...
    audio.play()
    .then(_ => { /* Set up media session... */ })
    .catch(error => { console.log(error) });
});

Se você não quiser tocar áudio logo após a primeira interação, recomendamos usar o método load() do elemento de áudio. Essa é uma maneira do navegador saber se o usuário interagiu com o elemento. Isso também pode ajudar a suavizar a reprodução, porque o conteúdo já estará carregado.

let audio = document.querySelector('audio');

welcomeButton.addEventListener('pointerup', function(event) {
  // User interacted with the page. Let's load audio...
  <strong>audio.load()</strong>
  .then(_ => { /* Show play button for instance... */ })
  .catch(error => { console.log(error) });
});

// Later...
playButton.addEventListener('pointerup', function(event) {
  <strong>audio.play()</strong>
  .then(_ => { /* Set up media session... */ })
  .catch(error => { console.log(error) });
});

Personalizar a notificação

Quando seu app da Web estiver reproduzindo áudio, você já 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 ele pode encontrar.

Sem sessão de mídia
Sem sessão de mídia
Com sessão de mídia
Com sessão de mídia

Definir metadados

Vamos conferir 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.

// When audio starts playing...
if ('mediaSession' in navigator) {

    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 é 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á-la para garantir que sempre mostre informações relevantes na notificação de mídia.

Faixa anterior / próxima faixa

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

let audio = document.createElement('audio');

let playlist = ['audio1.mp3', 'audio2.mp3', 'audio3.mp3'];
let index = 0;

navigator.mediaSession.setActionHandler('previoustrack', function() {
    // User clicked "Previous Track" media notification icon.
    index = (index - 1 + playlist.length) % playlist.length;
    playAudio();
});

navigator.mediaSession.setActionHandler('nexttrack', function() {
    // User clicked "Next Track" media notification icon.
    index = (index + 1) % playlist.length;
    playAudio();
});

function playAudio() {
    audio.src = playlist[index];
    audio.play()
    .then(_ => { /* Set up media session... */ })
    .catch(error => { console.log(error); });
}

playButton.addEventListener('pointerup', function(event) {
    playAudio();
});

Os gerenciadores de ações de mídia vão ser mantidos. Isso é muito semelhante ao padrão de listener de eventos, exceto pelo fato de que processar um evento significa que o navegador para de realizar um 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 são exibidos, a menos que você defina o gerenciador 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.

Retroceder / avançar

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

let skipTime = 10; // Time to skip in seconds

navigator.mediaSession.setActionHandler('seekbackward', function() {
    // User clicked "Seek Backward" media notification icon.
    audio.currentTime = Math.max(audio.currentTime - skipTime, 0);
});

navigator.mediaSession.setActionHandler('seekforward', function() {
    // User clicked "Seek Forward" media notification icon.
    audio.currentTime = Math.min(audio.currentTime + skipTime, audio.duration);
});

Reproduzir / pausar

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 os eventos de mídia "Reproduzir" e "Pausar".

navigator.mediaSession.setActionHandler('play', function() {
    // User clicked "Play" media notification icon.
    // Do something more than just playing current audio...
});

navigator.mediaSession.setActionHandler('pause', function() {
    // User clicked "Pause" media notification icon.
    // Do something more than just pausing current audio...
});

Notificações em qualquer lugar

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 são visíveis. A notificação de mídia é sincronizada automaticamente com qualquer dispositivo wearable pareado. E elas também são exibidas nas telas de bloqueio.

Bloquear tela
Tela de bloqueio - Foto de Michael Alø-Nielsen / CC BY 2.0
Notificação do Wear
Notificação do Wear

Curta vídeos off-line

Sei o que você está pensando agora. Service worker ao resgate!

Verdadeiro, mas acima de tudo, você quer garantir que todos os itens desta lista de verificação estejam marcados:

  • Todos os arquivos de mídia e de arte são veiculados com o cabeçalho HTTP Cache-Control apropriado. Dessa forma, o navegador pode armazenar em cache e reutilizar os recursos buscados anteriormente. Consulte a Lista de verificação de armazenamento em cache.
  • Todos os arquivos de mídia e arte precisam ser veiculados com o cabeçalho HTTP Allow-Control-Allow-Origin: *. Isso permitirá que apps da Web de terceiros busquem e consumam respostas HTTP do seu servidor da Web.

Estratégia de armazenamento em cache do service worker

Em relação a arquivos de mídia, recomendamos uma estratégia simples "Cache, fallback para rede", conforme ilustrado por Jake Archibald.

Para a arte, é um pouco mais específico e escolher a abordagem abaixo:

  • A arte If já está no cache. Disponibilizar a partir do cache
  • Else busca arte da rede
    • A busca de If foi concluída. Adicione a arte da rede ao cache e a exiba
    • Else disponibiliza a arte substituta do cache.

Dessa forma, as notificações de mídia sempre terão um bom ícone de arte, mesmo quando o navegador não conseguir fazer a busca. Veja como implementar isso:

const FALLBACK_ARTWORK_URL = 'fallbackArtwork.png';

addEventListener('install', event => {
    self.skipWaiting();
    event.waitUntil(initArtworkCache());
});

function initArtworkCache() {
    caches.open('artwork-cache-v1')
    .then(cache => cache.add(FALLBACK_ARTWORK_URL));
}

addEventListener('fetch', event => {
    if (/artwork-[0-9]+\.png$/.test(event.request.url)) {
    event.respondWith(handleFetchArtwork(event.request));
    }
});

function handleFetchArtwork(request) {
    // Return cache request if it's in the cache already, otherwise fetch
    // network artwork.
    return getCacheArtwork(request)
    .then(cacheResponse => cacheResponse || getNetworkArtwork(request));
}

function getCacheArtwork(request) {
    return caches.open('artwork-cache-v1')
    .then(cache => cache.match(request));
}

function getNetworkArtwork(request) {
    // Fetch network artwork.
    return fetch(request)
    .then(networkResponse => {
    if (networkResponse.status !== 200) {
        return Promise.reject('Network artwork response is not valid');
    }
    // Add artwork to the cache for later use and return network response.
    addArtworkToCache(request, networkResponse.clone())
    return networkResponse;
    })
    .catch(error => {
    // Return cached fallback artwork.
    return getCacheArtwork(new Request(FALLBACK_ARTWORK_URL))
    });
}

function addArtworkToCache(request, response) {
    return caches.open('artwork-cache-v1')
    .then(cache => cache.put(request, response));
}

Permitir que o usuário controle o cache

À medida que o usuário consome conteúdo do seu app da Web, arquivos de mídia e arte podem ocupar muito espaço no dispositivo. É sua responsabilidade mostrar quanto o cache é usado e permitir que os usuários possam limpá-lo. Felizmente, isso é muito fácil com a API Cache.

// Here's how I'd compute how much cache is used by artwork files...
caches.open('artwork-cache-v1')
.then(cache => cache.matchAll())
.then(responses => {
    let cacheSize = 0;
    let blobQueue = Promise.resolve();

    responses.forEach(response => {
    let responseSize = response.headers.get('content-length');
    if (responseSize) {
        // Use content-length HTTP header when possible.
        cacheSize += Number(responseSize);
    } else {
        // Otherwise, use the uncompressed blob size.
        blobQueue = blobQueue.then(_ => response.blob())
            .then(blob => { cacheSize += blob.size; blob.close(); });
    }
    });

    return blobQueue.then(_ => {
    console.log('Artwork cache is about ' + cacheSize + ' Bytes.');
    });
})
.catch(error => { console.log(error); });

// And here's how to delete some artwork files...
const artworkFilesToDelete = ['artwork1.png', 'artwork2.png', 'artwork3.png'];

caches.open('artwork-cache-v1')
.then(cache => Promise.all(artworkFilesToDelete.map(artwork => cache.delete(artwork))))
.catch(error => { console.log(error); });

Observações sobre implementação

  • O Chrome para Android solicita a seleção de áudio "total" para mostrar notificações de mídia somente quando a duração do arquivo de mídia é de pelo menos cinco segundos.
  • A arte de notificação é compatível com URLs de blob e URLs de dados.
  • Se nenhuma arte for definida e houver uma imagem de ícone em um tamanho desejável, as notificações de mídia vão usá-la.
  • O tamanho da arte da notificação no Google Chrome para Android é 512x512. Em dispositivos mais simples, o valor é 256x256.
  • Dispense notificações de mídia com audio.src = ''.
  • Como a API Web Audio não solicita o foco de áudio do Android por motivos históricos, a única maneira de fazer isso funcionar com a API Media Session é conectar um elemento <audio> como a origem de entrada da API Web Audio. Esperamos que a API Web AudioFocus proposta melhore a situação em um futuro.
  • As chamadas de sessão de mídia só vão afetar as notificações de mídia se vierem do mesmo frame que o recurso de mídia. Confira o snippet abaixo.
<iframe id="iframe">
  <audio>...</audio>
</iframe>
<script>
  iframe.contentWindow.navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Never Gonna Give You Up',
    ...
  });
</script>

Suporte

Até o momento, o Google Chrome para Android era a única plataforma compatível com a API Media Session. Confira informações mais atualizadas sobre o status de implementação do navegador na página Status da plataforma do Chrome.

Amostras e demonstrações

Confira nossos exemplos oficiais de sessões de mídia do Chrome com a Blender Foundation e o trabalho de Jan Morgenstern.

Recursos

Especificação de sessão de mídia: wicg.github.io/mediasession (link em inglês)

Problemas de especificação: github.com/WICG/mediasession/issues

Bugs do Chrome: crbug.com