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 notificações de mídia fornecendo metadados para a mídia que o app da Web está reproduzindo. 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. Gostou? Teste os exemplos oficiais de sessão de mídia.

A API Media Session tem suporte no Chrome 57 (Beta em fevereiro de 2017, estável em março de 2017).

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

Gimme what I want

Você já conhece a API Media Session e está voltando para copiar e colar sem vergonha algum 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() {});
}

Acessar o 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 os eventos pointerup, click e touchend. Em outras palavras, o usuário precisa clicar em um botão antes que o app da Web possa 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 reproduzir o áudio logo após a primeira interação, recomendamos usar o método load() do elemento de áudio. Essa é uma maneira de o navegador acompanhar 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 o app da Web está reproduzindo áudio, 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 informações adequadas usando o título do documento e a imagem de ícone maior que puder encontrar.

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

Definir metadados

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

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 a atualização para garantir que as informações relevantes sejam sempre mostradas na notificação de mídia.

Faixa anterior / próxima faixa

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

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

Voltar / avançar

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.

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

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 todos os lugares

O legal da API MediaSession é 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.

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

Deixar o conteúdo off-line mais agradável

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

É verdade, mas antes de tudo, verifique se todos os itens desta lista de verificação estão marcados:

  • Todos os arquivos de mídia e arte são veiculados com o cabeçalho HTTP Cache-Control adequado. Isso permite que o navegador armazene em cache e reutilize recursos buscados anteriormente. Consulte a lista de verificação de armazenamento em cache.
  • Confira se todos os arquivos de mídia e arte são veiculados com o cabeçalho HTTP Allow-Control-Allow-Origin: *. Isso permite que apps da Web de terceiros busquem e consumam respostas HTTP do seu servidor da Web.

A estratégia de armazenamento em cache do service worker

Em relação aos arquivos de mídia, recomendo uma estratégia simples de cache, com fallback para a rede, como ilustrado por Jake Archibald.

No caso de artes, porém, eu seria um pouco mais específico e escolheria a abordagem abaixo:

  • A arte do If já está no cache, então mostre-a
  • Else buscar artes da rede
    • O fetch de If foi concluído, adicione a arte da rede ao cache e a exiba
    • Else veicular a arte de substituição do cache

Dessa forma, as notificações de mídia sempre terão um ícone de arte legal, 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, os arquivos de mídia e arte podem ocupar muito espaço no dispositivo. É sua responsabilidade mostrar quanto de cache é usado e permitir que os usuários o limpem. Felizmente para nós, 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 o foco de áudio "total" para mostrar notificações de mídia somente quando a duração do arquivo de mídia é de pelo menos 5 segundos.
  • A arte da notificação oferece suporte a URLs de dados e blob.
  • Se nenhuma arte for definida e houver uma imagem de ícone no tamanho desejado, as notificações de mídia vão usar essa imagem.
  • O tamanho da arte da notificação no Chrome para Android é 512x512. Para dispositivos básicos, é 256x256.
  • Dispense as 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 com que ela funcione com a API Media Session é vincular um elemento <audio> como a origem de entrada da API Web Audio. Esperamos que a API Web AudioFocus proposta melhore a situação em breve.
  • As chamadas de sessão de mídia só vão afetar as notificações de mídia se vierem do mesmo frame do 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

No momento da redação deste artigo, o Chrome para Android é a única plataforma que oferece suporte à API Media Session. Confira informações mais atualizadas sobre o status de implementação do navegador em Status da plataforma do Chrome.

Exemplos e demonstrações

Confira os exemplos oficiais de sessão 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 (link em inglês)