メディア通知のカスタマイズ、プレイリストの処理

François Beaufort
François Beaufort

新しい Media Session API では、ウェブアプリで再生中のメディアのメタデータを提供することで、メディア通知をカスタマイズできるようになりました。また、通知やメディアキーから発生するシークやトラックの変更など、メディア関連のイベントを処理することもできます。それにはまず、公式の Media Session サンプルをお試しください。

Media Session API は Chrome 57 でサポートされています(ベータ版は 2017 年 2 月、安定版は 2017 年 3 月です)。

メディア セッションの概要
写真 by Michael Alø-Nielsen / CC BY 2.0

見たいものを教えて

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

コードに取り組む

遊びましょう 🎷?

シンプルな <audio> 要素をウェブページに追加し、複数のメディアソースを割り当てて、ブラウザが最適なものを選択できるようにします。

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

Chrome for Android では、音声要素に対して autoplay が無効になっているため、音声要素の play() メソッドを使用する必要があります。このメソッドは、タップやマウスクリックなどのユーザー操作によってトリガーする必要があります。つまり、pointerupclicktouchend のイベントをリッスンします。つまり、ウェブアプリが実際にノイズを出す前に、ユーザーがボタンをクリックする必要があります。

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() メソッドを使用することをおすすめします。これは、ユーザーが要素を操作したかどうかをブラウザが追跡する 1 つの方法です。コンテンツはすでに読み込まれているため、再生をスムーズに行うこともできます。

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 を使用すると、「Seek Backward」メディア通知アイコンと「Seek Forward」メディア通知アイコンを表示して、スキップする時間を制御できます。

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 の優れている点は、メディアのメタデータとコントロールが表示される場所は通知トレイだけではないことです。メディア通知は、ペア設定されたウェアラブル デバイスと自動的に同期されます。ロック画面にも表示されます

ロック画面
ロック画面 - 写真 作成者: Michael Alø-Nielsen / CC BY 2.0
Wear の通知
Wear の通知

オフラインで快適に再生できるようにする

あなたが今考えていることはわかります。Service Worker の活用

正しいですが、何よりもまず、このチェックリストのすべての項目がオンになっていることを確認してください。

  • すべてのメディア ファイルとアートワーク ファイルは、適切な Cache-Control HTTP ヘッダーとともに提供されます。これにより、ブラウザは以前に取得したリソースをキャッシュに保存し、再利用できます。キャッシュのチェックリストをご覧ください。
  • すべてのメディア ファイルとアートワーク ファイルが Allow-Control-Allow-Origin: * HTTP ヘッダーとともに配信されるようにします。これにより、サードパーティのウェブアプリがウェブサーバーから HTTP レスポンスを取得して消費できるようになります。

Service Worker のキャッシュ戦略は、

メディア ファイルに関しては、Jake Archibald が説明しているように、シンプルな「Cache,falling to network」戦略をおすすめします。

アートワークの場合はもう少し具体的にし、以下の方法を選択します。

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

実装上の注意

  • Chrome for Android は、メディア ファイルの再生時間が 5 秒以上の場合にのみ、メディア通知を表示するため「フル」の音声フォーカスをリクエストします。
  • 通知アートワークは、blob URL とデータ URL をサポートしています。
  • アートワークが定義されておらず、適切なサイズのアイコン画像がある場合は、メディア通知で使用されます。
  • Chrome for Android の通知アートワークのサイズは 512x512 です。ローエンド デバイスの場合は 256x256 です。
  • audio.src = '' でメディア通知を閉じます。
  • Web Audio API は、歴史的な理由から Android Audio Focus をリクエストすることはないため、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>

サポート

このドキュメントの作成時点では、Media Session API をサポートしている唯一のプラットフォームは Chrome for Android です。ブラウザの実装ステータスの最新情報については、Chrome プラットフォームのステータスをご覧ください。

サンプルとデモ

Blender FoundationJan Morgenstern の作品を取り上げた Chrome の公式 Media Session サンプルをご覧ください。

リソース

メディア セッションの仕様: wicg.github.io/mediasession

仕様の問題: github.com/WICG/mediasession/issues

Chrome のバグ: crbug.com