Programming Field

[Web] Media Session APIとメディア通知の表示

Web技術における「Media Session API」あるいは単に「Media Session」は検討段階のWeb APIの1つであり、オーディオやビデオなどを再生する際に、端末に対して独自のメタデータの表示を行ったり、端末上のUIに表示される再生ボタンやハードウェアキーなどをスクリプト側でハンドル・制御することを可能にする機能です。まだ比較的新しい仕様であるため、このページを作成時点ではAndroid上のGoogle Chrome(バージョン57以上; 以下Chrome)でのみ使用可能です。

※ このページを作成時点では「Media Session」の仕様は「Editor's Draft」の状態であり、将来大いに変更される可能性が考えられます。利用される際はご注意ください。

TL;DR

  • audio要素に「src」属性(またはsource要素)を指定して再生すると、一部の条件を除いて「メディア通知」を表示させることができます。
  • この「メディア通知」は「navigator.mediaSession」オブジェクトをはじめとする「Media Session API」を利用してカスタマイズさせることができます。
  • Web Audioでは「メディア通知」は表示されませんが、(現時点では)無音の音声データを用いて無理やり表示させるということができます。

メディア通知表示

「メディア通知」表示(media notifications)は、端末上に通知として表示されるもののうち、再生ボタンなどメディアの操作機能を有しているものです。Chromeにおいては、audio要素(スクリプト上では「Audio」コンストラクターでも生成可能)を用いてデータを再生すると、そのデータの再生を制御できる通知が表示されるようになります。なお、この際のaudio要素は「controls」属性の有無は関係なく、現在表示しているページ内のDOM要素として追加されているかどうかも関係しません。

メディア通知表示の例

※ この「メディア通知」は通知が表示される場所に表示されるものなので、例えばAndroidではロック画面でも表示され、ロック画面上で再生・一時停止の操作を行うことも可能です。

メディア通知が出ている間(≒ audio要素によって再生中の場合)は、再生中のままブラウザーがバックグラウンドに回っても、多重アプリ起動などにより端末のメモリが不足してもブラウザープロセスが破棄される(タスクが自動でキルされる)可能性が大きく減ります(実際に破棄されるかどうか未確認であり、一切破棄されない可能性もあります※)。そのため、audio要素で再生することで音楽プレーヤーのような機能を提供することが可能になります。

ただし、audio要素を使って再生している場合でも、「src」属性が空である、あるいは「srcObject」を使って再生を行っている、という場合は通知が表示されません。また、Chromeではメディアの長さが0秒ではなく、かつ5秒以下の場合通知が表示されません。(わずかでも超えれば表示されます。なお、0秒の場合は一時停止状態が使えないようです。)

※ (2021/02/18 追記) 「Page Lifecycle」の概念に従うと、「オーディオ再生中」の場合は通常「Frozen」(凍結)状態や「Discarded」(廃棄)状態にならないように配慮されます。例として、Google Chromeでは「Heuristics for Freezing & Discarding」というドキュメントでその配慮が明記されています。ただしあくまで「配慮」であるため、メモリ不足になった場合などでは廃棄される可能性があります。

メディア通知のカスタマイズ - MediaSessionとMediaMetadata

audio要素の再生で表示される標準のメディア通知は、メディアのタイトル(無い場合はURL)と「再生 / 一時停止」ボタン程度しか表示されない簡素なものになっています。単に1つのメディアを再生するのであればこれだけでも十分かもしれませんが、「Media Session」の機能を利用するとこれを拡張することができます。

navigator.mediaSession

「Media Session」に対応している場合、「navigator」オブジェクトに「mediaSession」プロパティーが存在します。

if ('mediaSession' in navigator) {
    // navigator.mediaSession が利用可能
}

この「navigator.mediaSession」は「MediaSession」オブジェクトであり、以下のプロパティー/メソッドを利用することができます。

  • metadata プロパティー : 後述する独自のメタデータを設定する場合に用います。
  • playbackState プロパティー : 音は鳴っていないものの再生中であることを示すなど、再生状態を明示する際に用います。
  • setActionHandler メソッド : 再生や一時停止、「次のトラック」操作などを登録・解除します。ボタン表示に直結します。

MediaMetadata の作成と設定

メディア通知に表示されるテキストをカスタマイズするには、「navigator.mediaSession.metadata」プロパティーに独自のメタデータを設定します。メタデータは「MediaMetadata」コンストラクターを使って作成します。

function updateMeta() {
    // 「new MediaMetadata」で生成したデータを直接設定
    navigator.mediaSession.metadata = new MediaMetadata({
        title: 'simple metadata test',
        artist: 'the artist',
        album: 'simple metadata album',
        artwork: [
            // サポートするサイズの画像を指定(通常は512x512、小さい端末では256x256が使用される)
            // ※ data URL や blob URL も利用可能
            { src: 'art_small.png', sizes: '128x128', type: 'image/png' },
            { src: 'art_medium.png', sizes: '256x256', type: 'image/png' },
            { src: 'art_large.png', sizes: '512x512', type: 'image/png' }
        ]
    });
}

操作をハンドルする処理の登録

メディア通知に表示されるテキストは前述の方法で変更できますが、「再生」などの表示されるボタンについては、「navigator.mediaSession.setActionHandler」メソッドにハンドラーを登録することで追加されます。

// 「再生」ボタンの登録
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 () { });
// 'skip-ad' は仕様から削除されているため、登録しようとするとエラーになります。(2021/02/18 追記)
// // 「広告スキップ」ボタンの登録
// navigator.mediaSession.setActionHandler('skip-ad', function () { });
// 再生位置指定の登録
navigator.mediaSession.setActionHandler('seekto', function () { });

いずれのハンドラーも実際に対応するボタンが押されたときに処理が実行されます。また、Androidでは「再生」「一時停止」は同じ位置にボタンが表示され、状態によって表示が切り替わります。

なお、ボタンを表示したくない場合は単に登録を行わないことで実現できます。既に登録を行っている場合は、ハンドラーを指定する引数(第2引数)に null を指定することで解除できます。

※ これらの登録はメタデータの設定と同時に行う必要はなく、メタデータ設定前に行うことも可能です。また、メタデータが変更された場合でもこれらのハンドラーの登録が解除されることはありません。
※ 「再生」「一時停止」はハンドラーを登録しない場合でもボタンが表示され、その場合はaudio要素に対してそれぞれのアクションが行われます。それらの既定のアクションが適している場合は、ハンドラーの登録を省略することが可能です。
「広告スキップ」ボタンの挙動は未確認です。 → 現在のWDでは仕様に存在せず、'skip-ad' を指定するとエラーになるため、指定する必要はありません。(2021/02/18 追記)

作成したメタデータの表示

MediaMetadataを通じて作成したデータを「navigator.mediaSession.metadata」プロパティーに設定したら、それを表示させるためにaudio要素を使った再生を行います。前述のメタデータの作成は、「navigator.mediaSession.metadata」に設定しただけでは表示されません。あくまで「既定のメディア通知を独自のメタデータを使って置き換える」ということをしているだけであるため、メディア通知を表示させるための処理を走らせる必要があります。

function playAudio() {
    // メタデータの設定
    updateMeta();
    // 再生するメディアの設定
    audioElement.src = 'someMedia.mp3';
    // audio要素の再生開始
    audioElement.play();
}

メタデータを設定しアクションを登録した状態でaudio要素による再生を開始すると、メディア通知がカスタマイズされた状態で表示されます。

メタデータとアクションを設定したメディア通知表示の例

なお、通知はaudio要素の「src」プロパティーが空文字列「''」である場合に表示されないので、表示された通知をプログラム側から非表示にしたい場合は、audio要素の「src」プロパティーを空文字列に設定します。

function stopAudio() {
    // audio要素の(一時)停止
    audioElement.pause();
    // audio要素のコンテンツをリセット→通知を削除
    audioElement.src = '';
}

再生状態のカスタマイズ

一度メディア通知が表示された状態になったら、audio要素の再生中の場合は「一時停止」ボタンが、一時停止中の場合は「再生」ボタンが利用可能になります。これを、「navigator.mediaSession.playbackState」プロパティーを使うことにより、audio要素が一時停止中であっても「再生」状態としたい場合や、逆に再生中であっても「一時停止中」状態とすることも可能です。

※ audio要素が「再生中」である場合は、「paused」を設定しても再生中扱いになります。この場合はaudio要素の「pause」メソッドを明示的に呼び出して一時停止する必要があります。
※ 「setActionHandler」が一切利用されていない状況でも「playbackState」の変更は有効です。

var nextPlayTimer;

// 次の再生を続ける前にウェイトを入れる
function waitForNextPlay() {
    // audio要素の再生を止める
    audioElement.pause();
    // ウェイト中も再生状態として扱うため状態を書き換え
    navigator.mediaSession.playbackState = 'playing';
    // ウェイト中の「一時停止」操作をハンドルする
    // (ここでは「一時停止」操作をそのままキャンセル操作として扱う)
    navigator.mediaSession.setActionHandler('pause', cancelNextPlay);
    // UI表示をウェイト中として扱うためにCSSクラスを設定
    // (別途CSSやHTMLをこのクラスに対応した実装にしていることを前提)
    document.body.classList.add('wait-for-next-play');
    // 5秒後に次の再生を進めるようにタイマーを設定
    nextPlayTimer = setTimeout(doNextPlay, 5000);
}

// 次の再生を開始する処理
function doNextPlay() {
    // 既に次の再生を行っていたりキャンセルされたりした場合は何もしない
    if (!nextPlayTimer) {
        return;
    }
    // タイマー処理が行われないように設定(doNextPlay をタイマー以外からも呼び出せる場合には必要)
    nextPlayTimer = void 0;
    // UI表示をウェイト中から解除する
    document.body.classList.remove('wait-for-next-play');
    // メディア通知における再生状態に対して既定の状態が反映されるようにする
    navigator.mediaSession.playbackState = 'none';
    // 「一時停止」操作時に既定のアクションが行われるようにする
    navigator.mediaSession.setActionHandler('pause', null);
    // 次に再生するメディアを指定して再生を開始
    // (これによりメディア通知は引き続き表示が残る)
    audioElement.src = 'nextMedia.mp3';
    audioElement.play();
}

// 次の再生をキャンセルする処理
function cancelNextPlay() {
    // (以下のクリア処理は doNextPlay と同様)
    nextPlayTimer = void 0;
    document.body.classList.remove('wait-for-next-play');
    navigator.mediaSession.playbackState = 'none';
    navigator.mediaSession.setActionHandler('pause', null);
    // メディア通知を消すために空文字列を指定
    audioElement.src = '';
}

Web Audioの再生にメディア通知を使う?

※ ここに記載の内容はAndroid 9 / Google Chrome 71時点(2019年1月時点)では有効であることを確認していますが、今後も有効であるかどうかは不明です。また、「Web AudioFocus API」という仕様の提案もあるため、そちらの仕様がある程度定まり実装が入った際には、それを利用することが望ましいと思われます。

Web上での音声の再生にはaudio要素を利用する他に、Web Audio APIを使って再生する方法もあり、より複雑な音声再生を行いたい場合はWeb Audioを使う必要があります。

しかし、前述の通り、メディア通知の表示は「5秒を超える長さのメディアをaudio要素の『src』に設定して再生」する必要があり、Web Audioをそのまま使うとこの条件に当てはまりません。(audio要素を使っていないため「オーディオ再生中」扱いにもならないようで、一定時間経つとキルされる可能性があります。)

そこで、無理やりな方法として以下の手順でメディア通知を利用します。

  1. 5秒を超える無音の音声データを作成する(有効なデータであればdata URLでも可)
  2. ダミーのaudio要素を用意し、その音声データを「src」プロパティーに指定する
  3. audio要素の「loop」を「true」、「volume」を「0」とする(volume 設定は任意)
  4. 以下のいずれかを行う
    • navigator.mediaSession.setActionHandler」メソッドを使って最低限「再生」「一時停止」をハンドルする
    • audio要素の「play」「pause」「ended」の各イベントをハンドルする(setActionHandler でハンドルしない場合)
  5. Web Audio再生時にaudio要素の再生も開始する
  6. 一時停止や再生(再開)が行われた場合はWeb Audioの処理をそれに連動させる

※ Web Audioは出力先をMediaStreamにすることが可能であり(MediaStreamAudioDestinationNode)、そのMediaStreamをaudio要素の「srcObject」に指定することができますが、これだとメディア通知の表示条件を満たさないことがあるので注意が必要です(少なくともGoogle Chrome 71時点では表示されません)。

Web Audioでの再生のみではメディア通知が表示されないため、ブラウザーがバックグラウンドに回った際に再生中であってもプロセスが終了される可能性がありますが、通知を表示させることによってこれを防止することも可能になります。

このページにメディア通知の表示を実装しています。以下の「再生」ボタンを押下すると、Web Audioを使った単純な波形を再生しますが、その際にメディア通知を表示させる処理を加えています。また、MediaSessionを利用して表示データのカスタマイズ等も合わせて行っています。

メディア通知表示のサンプル:
(このブラウザーではMediaSessionはサポートされていません。)

このサンプルにおける通知表示の例

このサンプルのソースコードは以下のようになっています。

/**
 * AudioContext のインスタンス
 * @type {AudioContext}
 */
var audioContext;
/**
 * 再生するAudioNode
 * @type {OscillatorNode}
 */
var playingNode = null;
/**
 * 再生の音量調整用GainNode
 * @type {GainNode}
 */
var gainNode = null;
/** AudioNode再生処理用タイマー */
var processTimer = null;
var processTimerInterval = 1000;
/** 一時停止中かどうか */
var isPause = false;
/**
 * ダミーのaudio要素
 * @type {HTMLAudioElement}
 */
var audioElement;
/** 5秒+数フレームの無音オーディオデータをmp3化したものをdata URLに変換したもの */
var emptyMediaMp3Data = 'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjU3LjEwMAAAAAAAAAAAAAAA/+NEwAAAAAAAAAAAAEluZm8AAAAPAAAAawAAFNAADRASFBcZGx0gIiQnKSswMjU3OTs+QEJFR0lMTlNVV1lcXmBjZWdqbG5xdXd6fH6Bg4WIioyOkZOVmpyfoaOmqKqsr7Gztri9v8HExsjKzc/R1NbY29/i5Obo6+3v8vT2+fv9AAAAAExhdmM1Ny42NQAAAAAAAAAAAAAAACQEHgAAAAAAABTQhPcRhwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MUxAAAAANIAAAAAExBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxCMAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxEYAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxGkAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxIwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxK8AAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNIAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MUxNwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV';
/** 再生回数(メタデータデモ用) */
var playCountForMetadata = 0;

function playAudio() {
    if (typeof AudioContext === 'undefined') {
        alert('このブラウザーでは対応していません。');
        return;
    }
    if (!audioContext) {
        audioContext = new AudioContext();
    }
    if (!audioElement) {
        audioElement = new Audio();
    }

    // 再生するAudioNodeを初期化
    playingNode = audioContext.createOscillator();
    playingNode.frequency.value = 440;
    gainNode = audioContext.createGain();
    gainNode.gain.value = 0.5;
    playingNode.connect(gainNode);
    gainNode.connect(audioContext.destination);

    // 無音データを設定
    audioElement.src = emptyMediaMp3Data;
    audioElement.loop = true;
    audioElement.volume = 0;

    // メディア通知の表示を更新
    ++playCountForMetadata;
    updateMetadata();

    // 再生を開始
    isPause = false;
    processTimer = setTimeout(processAudioNode, (processTimerInterval = 1000));
    playingNode.start();
    audioElement.play();
    if (navigator.mediaSession) {
        navigator.mediaSession.playbackState = 'playing';
    }
    document.getElementById('PlayButton').innerText = '停止';
}

// 一時停止の切り替え処理
function pauseAudio() {
    if (!playingNode) {
        return;
    }
    if (isPause) {
        // 「再生再開」としてAudioNodeを再び繋ぐ
        gainNode.connect(audioContext.destination);
        // 再生状態に切り替え
        audioElement.play();
        if (navigator.mediaSession) {
            navigator.mediaSession.playbackState = 'playing';
        }
        isPause = false;
    } else {
        // 「一時停止」としてAudioNodeを切り離す
        gainNode.disconnect();
        // 一時停止状態に切り替え
        audioElement.pause();
        if (navigator.mediaSession) {
            navigator.mediaSession.playbackState = 'paused';
        }
        isPause = true;
    }
}

// 「早送り」としてここではタイマーの頻度を上げる
function forwardAudio() {
    if (processTimer === null) {
        return;
    }
    if (processTimerInterval > 100) {
        processTimerInterval -= 100;
    }
}

// 「巻き戻し」としてここではタイマーの頻度を下げる
function backwardAudio() {
    if (processTimer === null) {
        return;
    }
    if (2000 > processTimerInterval) {
        processTimerInterval += 100;
    }
}

function stopAudio() {
    // 再生を停止
    if (processTimer) {
        clearTimeout(processTimer);
        processTimer = null;
    }
    if (navigator.mediaSession) {
        navigator.mediaSession.playbackState = 'none';
    }
    playingNode.stop();
    audioElement.pause();
    audioElement.src = '';

    // クリーンアップ
    playingNode.disconnect();
    playingNode = null;
    gainNode.disconnect();
    gainNode = null;
    document.getElementById('PlayButton').innerText = '再生';
}

function processAudioNode() {
    // 一定時間ごとにHzを切り替える
    playingNode.frequency.value = (playingNode.frequency.value === 440) ? 330 : 440;
    processTimer = setTimeout(processAudioNode, processTimerInterval);
}

// メタデータの設定
function updateMetadata() {
    if (!navigator.mediaSession) {
        return;
    }
    navigator.mediaSession.metadata = new MediaMetadata({
        title: 'simple metadata test: ' + playCountForMetadata,
        artist: 'the artist',
        album: 'simple metadata album'
    });
}

function togglePlaying() {
    if (playingNode) {
        stopAudio();
    } else {
        playAudio();
    }
}

function checkAndInitMediaSession() {
    if (navigator.mediaSession) {
        // アクションハンドラーを登録
        navigator.mediaSession.setActionHandler('play', pauseAudio);
        navigator.mediaSession.setActionHandler('pause', pauseAudio);
        navigator.mediaSession.setActionHandler('seekbackward', backwardAudio);
        navigator.mediaSession.setActionHandler('seekforward', forwardAudio);
    } else {
        document.body.className += ' media-session-not-supported';
    }
}
if (!document.body) {
    window.addEventListener('DOMContentLoaded', checkAndInitMediaSession, false);
} else {
    checkAndInitMediaSession();
}

(2020/04/12 追記) 新しいAndroid OSでは通知表示の中にインジケーター(メディアの長さと再生位置)も表示されることがありますが、上記の方法を使っていると無音データの再生状況がそのまま反映されてしまいます(5秒が延々ループします)。Media Session APIではこのインジケーターを制御する機能として、setPositionState メソッドでの設定と 'seekto' アクションのイベントハンドルの機能が提供されています。例えば以下のようにすることで、再生位置の情報を通知表示から省略させることができます。

    // (setPositionState がサポートされていない場合メソッドが存在しない)
    if (navigator.mediaSession.setPositionState) {
        // ・「duration」に「0」を指定すると長さ無し扱いになる
        // ・「duration」に正の数(秒単位)を指定するとそれが長さとして用いられる(追加で「position」フィールドを入れることで現在位置を指定)
        // ・オブジェクトではなく null を指定するとデフォルトの挙動に戻す(「{ duration: 0 }」指定とは異なる)
        navigator.mediaSession.setPositionState({
            duration: 0
        });
    }

メディア通知表示のサンプル改訂版(setPositionState 有効版):

ただし setPositionState は2020年4月11日時点ではGoogle Chrome 81以降でのみ利用可能であることを確認しています

※ 実際の挙動は端末によって異なる場合があります。

(2021/02/18 追記) なお setPositionState を使っている場合、Google Chromeにおいて再生中にたまに再生位置が常に終了位置になってしまう場合があります(Google Chrome 88.0.4324 × Google Pixel 3で確認)。この場合、audio要素の再生を開始した際にnavigator.mediaSession.playbackState'paused' にすることで、再生位置が setPositionState で設定した値になるようです。('paused' に設定しても、audio要素再生中はそちらが優先されるため、メディア通知上のボタンは(再生中なので)「一時停止」ボタンが有効になります。)

参考リンク