미디어 알림 맞춤설정 및 재생목록 처리

François Beaufort
François Beaufort

이제 새로운 Media Session API를 사용하여 웹 앱에서 재생 중인 미디어의 메타데이터를 제공하여 미디어 알림을 맞춤설정할 수 있습니다. 또한 알림이나 미디어 키에서 발생할 수 있는 탐색 또는 트랙 변경과 같은 미디어 관련 이벤트를 처리할 수 있습니다. 관심이 있으시면 공식 미디어 세션 샘플을 사용해 보세요.

Media Session API는 Chrome 57 (2017년 2월 베타, 2017년 3월 정식 버전)에서 지원됩니다.

미디어 세션 요약
사진 : 마이클 알로-닐센/ CC BY 2.0

Gimme what I want

Media Session API에 관해 이미 알고 있고 부끄러워하지 않고 템플릿 코드를 복사하여 붙여넣고 있나요? 바로 이겁니다.

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() {});
}

코드 살펴보기

Let's play 🎷

웹페이지에 간단한 <audio> 요소를 추가하고 브라우저에서 가장 적합한 소스를 선택할 수 있도록 여러 미디어 소스를 할당합니다.

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

아시다시피 Android용 Chrome에서는 오디오 요소에 autoplay가 사용 중지되어 있으므로 오디오 요소의 play() 메서드를 사용해야 합니다. 이 메서드는 터치나 마우스 클릭과 같은 사용자 제스처에 의해 트리거되어야 합니다. 즉, pointerup, click, touchend 이벤트를 수신 대기해야 합니다. 즉, 웹 앱에서 실제로 소리를 내기 전에 사용자가 버튼을 클릭해야 합니다.

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

첫 번째 상호작용 직후에 오디오를 재생하고 싶지 않다면 오디오 요소의 load() 메서드를 사용하는 것이 좋습니다. 이는 브라우저가 사용자가 요소와 상호작용했는지 추적하는 한 가지 방법입니다. 콘텐츠가 이미 로드되므로 재생이 원활해질 수도 있습니다.

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

알림 맞춤설정

웹 앱에서 오디오를 재생하면 이미 알림 트레이에 미디어 알림이 표시됩니다. Android에서 Chrome은 문서 제목과 찾을 수 있는 가장 큰 아이콘 이미지를 사용하여 적절한 정보를 표시하기 위해 최선을 다합니다.

미디어 세션 없음
미디어 세션 없음
미디어 세션 사용
미디어 세션 사용

메타데이터 설정

Media Session API를 사용하여 제목, 아티스트, 앨범 이름, 아트워크와 같은 일부 미디어 세션 메타데이터를 설정하여 이 미디어 알림을 맞춤설정하는 방법을 살펴보겠습니다.

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

재생이 완료되면 알림이 자동으로 사라지므로 미디어 세션을 '해제'할 필요가 없습니다. 재생이 시작되면 현재 navigator.mediaSession.metadata가 사용됩니다. 따라서 미디어 알림에 항상 관련 정보를 표시하려면 업데이트해야 합니다.

이전 트랙 / 다음 트랙

웹 앱에서 재생목록을 제공하는 경우 사용자가 '이전 트랙' 및 '다음 트랙' 아이콘을 사용하여 미디어 알림에서 바로 재생목록을 탐색할 수 있도록 허용할 수 있습니다.

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

미디어 작업 핸들러는 유지됩니다. 이는 이벤트 리스너 패턴과 매우 유사하지만 이벤트를 처리하면 브라우저가 기본 동작을 중지하고 이를 웹 앱이 미디어 작업을 지원한다는 신호로 사용한다는 점이 다릅니다. 따라서 적절한 작업 핸들러를 설정하지 않으면 미디어 작업 컨트롤이 표시되지 않습니다.

미디어 작업 핸들러를 설정 해제하는 것은 null에 할당하는 것만큼 쉽습니다.

뒤로 탐색 / 앞으로 탐색

Media Session API를 사용하면 건너뛴 시간을 제어하려는 경우 '뒤로 탐색' 및 '앞으로 탐색' 미디어 알림 아이콘을 표시할 수 있습니다.

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

재생 / 일시중지

'재생/일시중지' 아이콘은 항상 미디어 알림에 표시되며 관련 이벤트는 브라우저에서 자동으로 처리됩니다. 어떠한 이유로든 기본 동작이 작동하지 않는 경우에도 '재생' 및 '일시중지' 미디어 이벤트를 처리할 수 있습니다.

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

어디서나 알림

Media Session API의 좋은 점은 미디어 메타데이터와 컨트롤이 표시되는 위치가 알림 트레이뿐만이 아니라는 것입니다. 미디어 알림은 페어링된 모든 웨어러블 기기에 자동으로 동기화됩니다. 잠금 화면에 표시되기도 합니다.

잠금 화면
잠금 화면 - 사진 마이클 알로-닐센 / CC BY 2.0
Wear 알림
Wear 알림

오프라인에서 원활하게 재생되도록 설정

지금 무슨 생각을 하고 계신지 알고 있습니다. 서비스 워커를 사용하면 됩니다.

맞습니다. 하지만 무엇보다 먼저 이 체크리스트의 모든 항목이 선택되어 있는지 확인해야 합니다.

  • 모든 미디어 및 아트워크 파일은 적절한 Cache-Control HTTP 헤더와 함께 제공됩니다. 이렇게 하면 브라우저에서 이전에 가져온 리소스를 캐시하고 재사용할 수 있습니다. 캐싱 체크리스트를 참고하세요.
  • 모든 미디어 및 아트워크 파일이 Allow-Control-Allow-Origin: * HTTP 헤더와 함께 게재되는지 확인합니다. 이렇게 하면 서드 파티 웹 앱이 웹 서버에서 HTTP 응답을 가져와 사용할 수 있습니다.

서비스 워커 캐싱 전략

미디어 파일과 관련하여 제이크 아치볼드가 설명한 대로 간단한 '캐시, 네트워크로 대체' 전략을 사용하는 것이 좋습니다.

아트워크의 경우 좀 더 구체적으로 아래 접근 방식을 선택하겠습니다.

  • If 아트워크가 이미 캐시에 있으므로 캐시에서 제공
  • Else 네트워크에서 아트워크 가져오기
    • If 가져오기가 성공하면 캐시에 네트워크 아트워크를 추가하고 게재합니다.
    • Else 캐시에서 대체 아트워크를 제공합니다.

이렇게 하면 브라우저에서 아트워크를 가져올 수 없는 경우에도 미디어 알림에 항상 멋진 아트워크 아이콘이 표시됩니다. 이를 구현하는 방법은 다음과 같습니다.

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

사용자가 캐시를 제어하도록 허용

사용자가 웹 앱의 콘텐츠를 소비하면 미디어 및 아트워크 파일이 기기에서 많은 공간을 차지할 수 있습니다. 캐시가 얼마나 사용되는지 표시하고 사용자에게 캐시를 지울 수 있는 기능을 제공하는 것은 개발자의 책임입니다. 다행히 Cache API를 사용하면 쉽게 할 수 있습니다.

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

구현 참고사항

  • Android용 Chrome은 미디어 파일 길이가 5초 이상인 경우에만 미디어 알림을 표시하기 위해 '전체' 오디오 포커스를 요청합니다.
  • 알림 아트워크는 blob URL 및 데이터 URL을 지원합니다.
  • 아트워크가 정의되어 있지 않고 원하는 크기의 아이콘 이미지가 있는 경우 미디어 알림에서 이를 사용합니다.
  • Android용 Chrome의 알림 아트워크 크기는 512x512입니다. 저사양 기기의 경우 256x256입니다.
  • audio.src = ''를 사용하여 미디어 알림을 닫습니다.
  • Web Audio API는 이전 이유로 Android 오디오 포커스를 요청하지 않으므로 Media Session API와 함께 작동하도록 하는 유일한 방법은 <audio> 요소를 Web Audio API의 입력 소스로 연결하는 것입니다. 제안된 Web AudioFocus API가 가까운 시일 내에 이 문제를 해결할 수 있기를 바랍니다.
  • 미디어 세션 호출은 미디어 리소스와 동일한 프레임에서 발생하는 경우에만 미디어 알림에 영향을 미칩니다. 아래 스니펫을 참고하세요.
<iframe id="iframe">
  <audio>...</audio>
</iframe>
<script>
  iframe.contentWindow.navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Never Gonna Give You Up',
    ...
  });
</script>

지원

이 글을 작성하는 시점에서 Android용 Chrome은 Media Session API를 지원하는 유일한 플랫폼입니다. 브라우저 구현 상태에 관한 최신 정보는 Chrome 플랫폼 상태에서 확인할 수 있습니다.

샘플 및 데모

Blender FoundationJan Morgenstern의 작품이 포함된 공식 Chrome 미디어 세션 샘플을 확인하세요.

리소스

미디어 세션 사양: wicg.github.io/mediasession

사양 문제: github.com/WICG/mediasession/issues

Chrome 버그: crbug.com