Create Free APK

JavaScript bridge API

Media and casting

The lock-screen media notification your page drives, and handing a video to a TV.

navigator.mediaSession web standard#

navigator.mediaSession + MediaMetadata

Example

Shows what your page is playing in a media notification and on the lock screen, with Previous / Play-Pause / Next / Stop buttons that call back into your page. Uses the normal Media Session API.

Returns: nothing; button presses call the handlers you set with setActionHandler, with { action } as the standard says. Needs: turn on Media Notification in Step 4 (Access) when you build. AI-built apps get it switched on for you when the code uses navigator.mediaSession.

var audio = document.getElementById('player');
var tracks = [
  { src: 'https://example.com/a.mp3', title: 'Morning', artist: 'Band', art: 'https://example.com/a.jpg' },
  { src: 'https://example.com/b.mp3', title: 'Evening', artist: 'Band', art: 'https://example.com/b.jpg' }
];
var index = 0;
var handlersReady = false;

function setupHandlers() {
  if (handlersReady || !('mediaSession' in navigator)) return;
  handlersReady = true;
  var ms = navigator.mediaSession;
  ms.setActionHandler('play', function () { audio.play(); });
  ms.setActionHandler('pause', function () { audio.pause(); });
  ms.setActionHandler('nexttrack', function () { playTrack(index + 1); });
  ms.setActionHandler('previoustrack', function () { playTrack(index - 1); });
  ms.setActionHandler('stop', function () { audio.pause(); audio.currentTime = 0; });
}

function playTrack(i) {
  index = (i + tracks.length) % tracks.length;
  var t = tracks[index];
  audio.src = t.src;
  audio.play();
  setupHandlers();   // on the first play, not at page load (see Notes)
  if ('mediaSession' in navigator && window.MediaMetadata) {
    navigator.mediaSession.metadata = new MediaMetadata({
      title: t.title, artist: t.artist, album: '',
      artwork: [{ src: t.art, sizes: '512x512', type: 'image/jpeg' }]
    });
  }
}

document.getElementById('play-btn').addEventListener('click', function () { playTrack(0); });

Keep the play/pause button and the progress bar right:

function publishState() {
  if (!('mediaSession' in navigator)) return;
  navigator.mediaSession.playbackState = audio.paused ? 'paused' : 'playing';   // set this FIRST
  if (audio.duration > 0 && navigator.mediaSession.setPositionState) {
    navigator.mediaSession.setPositionState({
      duration: audio.duration, position: audio.currentTime, playbackRate: 1
    });
  }
}
audio.addEventListener('play', publishState);
audio.addEventListener('pause', publishState);
audio.addEventListener('seeked', publishState);

A podcast player where the notification's Next / Previous buttons skip 30 seconds forward and 15 seconds back instead of changing episode:

var audio = document.getElementById('player');
function skip(seconds) {
  var end = audio.duration || 0;
  audio.currentTime = Math.max(0, Math.min(end, audio.currentTime + seconds));
  if ('mediaSession' in navigator && navigator.mediaSession.setPositionState && end > 0) {
    navigator.mediaSession.setPositionState({ duration: end, position: audio.currentTime });
  }
}
if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('play', function () { audio.play(); });
  navigator.mediaSession.setActionHandler('pause', function () { audio.pause(); });
  navigator.mediaSession.setActionHandler('nexttrack', function () { skip(30); });
  navigator.mediaSession.setActionHandler('previoustrack', function () { skip(-15); });
  if (window.MediaMetadata) {
    navigator.mediaSession.metadata = new MediaMetadata({
      title: 'Episode 12: Getting started', artist: 'My Podcast',
      artwork: [{ src: 'https://example.com/podcast-512.jpg', sizes: '512x512' }]
    });
  }
}

Notes:

  • In the app, navigator.mediaSession is installed before the page's first script, so handlers and metadata can be set at any time - at the top of the page or when playback starts. It exists in the main page, not inside iframes.
  • Supported actions: play, pause, nexttrack, previoustrack, stop. Seeking from the notification is not supported, so seekto never fires.
  • Setting playbackState keeps the position from your last setPositionState call; call setPositionState again when the position really changes (a seek).
  • Only the last artwork image is used. It must be a full https:// address on the internet (the app downloads it, 4 s limit); a file inside your app ZIP does not show.
  • <audio> / <video> elements are tracked for you: when the page handles a button neither with setActionHandler nor with its own window.WebToApkOnMediaAction, Play / Pause / Stop control the media element that is playing (else the one played last, else the first).
  • To remove the notification, call WebToApk.clearMediaNotification() (setting metadata = null does not remove it).
  • A page that defines window.WebToApkOnMediaAction = function (action) { ... } (actions play, pause, next, prev, stop) gets every button press there too - the app no longer overwrites it. Use one of the two, not both, or a press runs twice.

requestPictureInPicture web standard#

video.requestPictureInPicture() β€” Picture-in-Picture

Example

The standard Picture-in-Picture API: float a video in a small window while the user does something else. Chrome on Android has it and Android's WebView does not; in the app the video fills the floating window, as it does in Chrome.

Returns: a Promise for a PictureInPictureWindow (width, height, resize event). The video gets enterpictureinpicture, then leavepictureinpicture when the user closes or expands the window; document.pictureInPictureElement is the video meanwhile. Rejects NotAllowedError without a tap, InvalidStateError before the video's metadata has loaded or when it has disablePictureInPicture. Needs: Picture-in-picture switched on in the build (it is switched on by itself when your code calls requestPictureInPicture), Android 8 or newer.

<video id="v" src="clip.mp4" controls playsinline></video>
<button id="pip">Pop out</button>
<script>
const v = document.getElementById('v');
const btn = document.getElementById('pip');
btn.hidden = !document.pictureInPictureEnabled;
btn.addEventListener('click', async () => {
  try {
    if (document.pictureInPictureElement) await document.exitPictureInPicture();
    else await v.requestPictureInPicture();
  } catch (e) {
    console.warn('Picture-in-Picture:', e.name);
  }
});
v.addEventListener('enterpictureinpicture', () => { btn.textContent = 'Back to app'; });
v.addEventListener('leavepictureinpicture', () => { btn.textContent = 'Pop out'; });
</script>

Pop out automatically when the user leaves, and report an unsupported build:

const v = document.getElementById('v');
if (!document.pictureInPictureEnabled || v.disablePictureInPicture) {
  document.getElementById('pip').hidden = true;      // this build has no picture-in-picture
}
document.addEventListener('visibilitychange', async () => {
  if (document.hidden && !v.paused && !document.pictureInPictureElement) {
    try { await v.requestPictureInPicture(); }
    catch (e) { console.warn(e.name); }               // NotAllowedError without a tap
  }
});

Wait for the metadata, and resize with the window:

async function popOut(video) {
  if (video.readyState < 1) await new Promise((r) => video.addEventListener('loadedmetadata', r, { once: true }));
  try {
    const pipWin = await video.requestPictureInPicture();
    pipWin.addEventListener('resize', () => console.log(pipWin.width, pipWin.height));
  } catch (e) {
    if (e.name === 'InvalidStateError') showMessage('This video cannot float.');
    else if (e.name === 'NotAllowedError') showMessage('Tap the button to pop out.');
  }
}
document.getElementById('pip').onclick = () => popOut(document.getElementById('v'));

Notes: Android puts the whole app into the floating window, so the video is first shown alone (full screen) and the window takes the video's shape; leaving the window puts the video back in the page. document.exitPictureInPicture() brings the app back to full screen. When the build has no picture-in-picture, document.pictureInPictureEnabled is false, as in a browser without it. The mode change reaches the page as appmint:pip. Internal transports: __pipEnter, __pipExit, __pipAvailable, __pipPrepare.

__pipAvailable bridge#

window.WebToApk.__pipAvailable(): Boolean

Whether video.requestPictureInPicture() can work: PiP on in the build, Android 8+.

Example

Documented together with WebToApk.__pipEnter - the example there shows this one too.

Internal transport behind video.requestPictureInPicture() and document.exitPictureInPicture(). Pages use the standard API; never call these directly, because the __ methods may change without notice.

Returns: __pipEnter(width, height) returns "ok" when Android accepted the window (with the video's aspect ratio), else "unsupported" / "failed". The window opening and closing arrive as appmint:pip with {inPip}. __pipAvailable() is true when the build has picture-in-picture on Android 8+. Needs: picture-in-picture switched on in the build.

Use the public API:

const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());

Check support first, and report a refusal:

async function floatVideo(video) {
  if (!document.pictureInPictureEnabled) { showMessage('Picture-in-picture is not on in this app.'); return; }
  try {
    await video.requestPictureInPicture();
  } catch (err) {
    showMessage('Could not open the floating window.');
  }
}

Leave it again and follow the state with the standard events:

video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };

Notes: Implemented in the app shell (PictureInPictureShim.kt). __pipPrepare stops the shell turning the screen sideways for the full-screen step on the way into the window.

__pipEnter bridge#

window.WebToApk.__pipEnter(width: Int, height: Int): String

Opens the PiP window with a [width]Γ—[height] video's aspect ratio: "ok", or "unsupported" / "failed". The mode change arrives as `appmint:pip`.

Example

Internal transport behind video.requestPictureInPicture() and document.exitPictureInPicture(). Pages use the standard API; never call these directly, because the __ methods may change without notice.

Returns: __pipEnter(width, height) returns "ok" when Android accepted the window (with the video's aspect ratio), else "unsupported" / "failed". The window opening and closing arrive as appmint:pip with {inPip}. __pipAvailable() is true when the build has picture-in-picture on Android 8+. Needs: picture-in-picture switched on in the build.

Use the public API:

const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());

Check support first, and report a refusal:

async function floatVideo(video) {
  if (!document.pictureInPictureEnabled) { showMessage('Picture-in-picture is not on in this app.'); return; }
  try {
    await video.requestPictureInPicture();
  } catch (err) {
    showMessage('Could not open the floating window.');
  }
}

Leave it again and follow the state with the standard events:

video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };

Notes: Implemented in the app shell (PictureInPictureShim.kt). __pipPrepare stops the shell turning the screen sideways for the full-screen step on the way into the window.

__pipExit bridge#

window.WebToApk.__pipExit()

document.exitPictureInPicture(): brings the app back to full screen.

Example

Documented together with WebToApk.__pipEnter - the example there shows this one too.

Internal transport behind video.requestPictureInPicture() and document.exitPictureInPicture(). Pages use the standard API; never call these directly, because the __ methods may change without notice.

Returns: __pipEnter(width, height) returns "ok" when Android accepted the window (with the video's aspect ratio), else "unsupported" / "failed". The window opening and closing arrive as appmint:pip with {inPip}. __pipAvailable() is true when the build has picture-in-picture on Android 8+. Needs: picture-in-picture switched on in the build.

Use the public API:

const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());

Check support first, and report a refusal:

async function floatVideo(video) {
  if (!document.pictureInPictureEnabled) { showMessage('Picture-in-picture is not on in this app.'); return; }
  try {
    await video.requestPictureInPicture();
  } catch (err) {
    showMessage('Could not open the floating window.');
  }
}

Leave it again and follow the state with the standard events:

video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };

Notes: Implemented in the app shell (PictureInPictureShim.kt). __pipPrepare stops the shell turning the screen sideways for the full-screen step on the way into the window.

__pipPrepare bridge#

window.WebToApk.__pipPrepare()

The page is about to put a video fullscreen for PiP: do not force landscape for it.

Example

Documented together with WebToApk.__pipEnter - the example there shows this one too.

Internal transport behind video.requestPictureInPicture() and document.exitPictureInPicture(). Pages use the standard API; never call these directly, because the __ methods may change without notice.

Returns: __pipEnter(width, height) returns "ok" when Android accepted the window (with the video's aspect ratio), else "unsupported" / "failed". The window opening and closing arrive as appmint:pip with {inPip}. __pipAvailable() is true when the build has picture-in-picture on Android 8+. Needs: picture-in-picture switched on in the build.

Use the public API:

const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());

Check support first, and report a refusal:

async function floatVideo(video) {
  if (!document.pictureInPictureEnabled) { showMessage('Picture-in-picture is not on in this app.'); return; }
  try {
    await video.requestPictureInPicture();
  } catch (err) {
    showMessage('Could not open the floating window.');
  }
}

Leave it again and follow the state with the standard events:

video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };

Notes: Implemented in the app shell (PictureInPictureShim.kt). __pipPrepare stops the shell turning the screen sideways for the full-screen step on the way into the window.

castMedia bridge#

window.WebToApk.castMedia(mediaUrl: String)

Offers [mediaUrl] to whatever on this phone can play it on another screen - Chromecast, a Smart TV app, a local player - through the system chooser. The type is taken from the URL ([CastMediaType]): an audio file (.mp3, .m4a, .aac, .ogg, .opus, .wav, .flac …) or an audio `data:` URL is offered to audio apps, anything else - video files, stream manifests, extension-less URLs - to video apps. This is a hand-off, not a cast session: the app does not stay in control of playback, and there is no callback. A page that needs a real session should use the Cast SDK in JavaScript instead.

Example

Offers a video or audio address to the apps on the phone that can play it on another screen (a Chromecast app, a Smart TV app, a video player), through the Android "Cast to..." chooser.

Returns: nothing, and no callback. Needs: nothing to switch on. The phone needs an app that accepts the link.

function castToTv(url) {
  if (window.WebToApk && typeof window.WebToApk.castMedia === 'function') {
    window.WebToApk.castMedia(url);          // opens the "Cast to..." chooser
  } else {
    alert('Casting is only available in the app');
  }
}

document.getElementById('cast-btn').addEventListener('click', function () {
  var video = document.querySelector('video');
  var url = video && video.currentSrc ? video.currentSrc : 'https://example.com/movie.mp4';
  castToTv(url);
});

Send a podcast episode from a list to a speaker or TV app (an .mp3 link is offered to audio apps):

// <li><button class="cast" data-src="https://example.com/ep12.mp3">Cast</button> Episode 12</li>
document.getElementById('episodes').addEventListener('click', function (e) {
  var btn = e.target.closest('button.cast');
  if (!btn) return;
  var src = btn.getAttribute('data-src') || '';
  if (src.indexOf('https://') !== 0) {
    alert('This episode cannot be cast');   // files inside the app cannot be handed over
    return;
  }
  if (window.WebToApk && typeof window.WebToApk.castMedia === 'function') {
    window.WebToApk.castMedia(src);
  }
});

Show the Cast button only inside the app, and pause the page's own player before handing a live stream over:

var castBtn = document.getElementById('cast-live');
var canCast = !!(window.WebToApk && typeof window.WebToApk.castMedia === 'function');
castBtn.hidden = !canCast;

castBtn.addEventListener('click', function () {
  var video = document.getElementById('live');
  video.pause();                                   // avoid playing twice, here and on the TV
  window.WebToApk.castMedia('https://example.com/live/stream.m3u8');   // offered to video apps
});

Notes:

  • This is a hand-off, not a cast session: your page does not control playback after the user picks an app.
  • The address must be a full https:// link that other apps can open. A file inside your app ZIP, a blob: URL or a web page address will not play.
  • The type comes from the link: an audio file (.mp3, .m4a, .aac, .ogg, .opus, .wav, .flac …) or an audio/ data: URL is offered to audio apps; anything else - video files, .m3u8 streams, links without a file extension - to video apps.

clearMediaNotification bridge#

window.WebToApk.clearMediaNotification()

Lets web content drive the Android media-style notification and receive transport control callbacks (play/pause/next/prev) from the lock screen and notification shade. Usage from JavaScript: window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // playing, pos ms, dur ms window.WebToApk.clearMediaNotification() Callback from Android β†’ JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

Described by its group, Media Notification Bridge, rather than on its own.

Example

Removes the media notification and its lock-screen controls, for example when the user closes your player.

Returns: nothing. Needs: turn on Media Notification in Step 4 (Access) when you build. Without it the call does nothing.

There is no standard web call for this, so use the bridge:

function closePlayer() {
  var audio = document.getElementById('player');
  audio.pause();
  audio.currentTime = 0;
  if (window.WebToApk && typeof window.WebToApk.clearMediaNotification === 'function') {
    window.WebToApk.clearMediaNotification();
  }
}

document.getElementById('close-player').addEventListener('click', closePlayer);

// The notification's own Stop button already removes it; just stop your audio:
if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('stop', function () {
    var audio = document.getElementById('player');
    audio.pause();
    audio.currentTime = 0;
  });
}

A sleep timer that stops the audio and removes the notification after the chosen number of minutes:

var sleepTimer = null;
function setSleepTimer(minutes) {
  clearTimeout(sleepTimer);
  sleepTimer = setTimeout(function () {
    var audio = document.getElementById('player');
    audio.pause();
    if (window.WebToApk && typeof window.WebToApk.clearMediaNotification === 'function') {
      window.WebToApk.clearMediaNotification();
    }
  }, minutes * 60 * 1000);
}
document.getElementById('sleep-30').addEventListener('click', function () { setSleepTimer(30); });

Remove the notification when the last track of a playlist has finished, instead of leaving a Play button for nothing:

var audio = document.getElementById('player');
var playlist = ['https://example.com/1.mp3', 'https://example.com/2.mp3'];
var current = 0;

audio.addEventListener('ended', function () {
  current++;
  if (current < playlist.length) {
    audio.src = playlist[current];
    audio.play();
  } else if (window.WebToApk && typeof window.WebToApk.clearMediaNotification === 'function') {
    window.WebToApk.clearMediaNotification();
  }
});

Notes: Use this for "stop", not for "pause" (for pause, send setPlaybackState(false, ...) so the Play button stays). Setting navigator.mediaSession.metadata = null does not remove the notification. The next setMediaMetadata / setPlaybackState (or new navigator.mediaSession metadata / state) brings the notification and the lock-screen controls back.

setMediaMetadata bridge#

window.WebToApk.setMediaMetadata(title: String, artist: String, artworkUrl: String)

Lets web content drive the Android media-style notification and receive transport control callbacks (play/pause/next/prev) from the lock screen and notification shade. Usage from JavaScript: window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // playing, pos ms, dur ms window.WebToApk.clearMediaNotification() Callback from Android β†’ JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

Described by its group, Media Notification Bridge, rather than on its own.

Example

Sets the title, artist and cover picture of the media notification and lock-screen player. Pages normally set navigator.mediaSession.metadata, which calls this for you.

Returns: nothing. Needs: turn on Media Notification in Step 4 (Access) when you build. Without it the call does nothing.

The public way (use this):

if ('mediaSession' in navigator && window.MediaMetadata) {
  navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Episode 12', artist: 'My Podcast',
    artwork: [{ src: 'https://example.com/cover.jpg', sizes: '512x512' }]
  });
}

The raw call. Arguments: title, artist, artworkUrl (use '' for no picture):

function showNowPlaying(track) {
  if (!(window.WebToApk && typeof window.WebToApk.setMediaMetadata === 'function')) return;
  window.WebToApk.setMediaMetadata(track.title, track.artist || '', track.cover || '');
  window.WebToApk.setPlaybackState(true, 0, Math.round(track.seconds * 1000));
}

// Buttons in the notification come back through the standard API:
if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('nexttrack', function () { playNext(); });
  navigator.mediaSession.setActionHandler('previoustrack', function () { playPrevious(); });
}

Keep the notification's title up to date for a live radio stream, where the song changes without the page loading a new file:

var lastSong = '';
function refreshNowPlaying() {
  fetch('https://example.com/radio/now-playing.json')
    .then(function (r) { return r.json(); })
    .then(function (np) {            // e.g. { "title": "...", "artist": "...", "cover": "https://..." }
      var key = np.title + '|' + np.artist;
      if (key === lastSong) return;  // only update when the song really changed
      lastSong = key;
      if (window.WebToApk && typeof window.WebToApk.setMediaMetadata === 'function') {
        window.WebToApk.setMediaMetadata(np.title || 'Live radio', np.artist || '', np.cover || '');
      }
    })
    .catch(function () { /* try again on the next tick */ });
}
document.getElementById('radio').addEventListener('play', function () {
  refreshNowPlaying();
  setInterval(refreshNowPlaying, 30000);
}, { once: true });

Notes:

  • The notification appears (or updates) with this call. An empty title shows the app name.
  • artworkUrl must be a full https:// address on the internet. The app downloads it (4-second limit); if that fails, the app icon is shown.
  • Receive button presses with navigator.mediaSession.setActionHandler (see navigator.mediaSession), or with your own window.WebToApkOnMediaAction = function (action) { ... } (play, pause, next, prev, stop) - use one of the two. After clearMediaNotification(), the next call to this (or setPlaybackState) brings the notification and the lock-screen controls back.

setPlaybackState bridge#

window.WebToApk.setPlaybackState(isPlaying: Boolean, positionMs: Long, durationMs: Long)

Lets web content drive the Android media-style notification and receive transport control callbacks (play/pause/next/prev) from the lock screen and notification shade. Usage from JavaScript: window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // playing, pos ms, dur ms window.WebToApk.clearMediaNotification() Callback from Android β†’ JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

Described by its group, Media Notification Bridge, rather than on its own.

Example

Tells the media notification whether you are playing or paused, and where you are in the track. Pages normally set navigator.mediaSession.playbackState and call setPositionState, which call this for you.

Returns: nothing. Needs: turn on Media Notification in Step 4 (Access) when you build. Without it the call does nothing.

The public way (use this):

var audio = document.getElementById('player');
function publish() {
  if (!('mediaSession' in navigator)) return;
  navigator.mediaSession.playbackState = audio.paused ? 'paused' : 'playing';
  if (audio.duration > 0 && navigator.mediaSession.setPositionState) {
    navigator.mediaSession.setPositionState({ duration: audio.duration, position: audio.currentTime });
  }
}
audio.addEventListener('play', publish);
audio.addEventListener('pause', publish);

The raw call. Arguments: isPlaying (true/false), positionMs, durationMs (whole milliseconds):

function publishRaw(audio) {
  if (!(window.WebToApk && typeof window.WebToApk.setPlaybackState === 'function')) return;
  window.WebToApk.setPlaybackState(
    !audio.paused,
    Math.floor(audio.currentTime * 1000),
    Math.floor((audio.duration || 0) * 1000)
  );
}

Report the new position after the user seeks, and switch the notification to Play when the track ends:

var audio = document.getElementById('player');
function report(playing) {
  if (!(window.WebToApk && typeof window.WebToApk.setPlaybackState === 'function')) return;
  var durMs = Math.floor((audio.duration || 0) * 1000);
  var posMs = Math.floor(audio.currentTime * 1000);
  window.WebToApk.setPlaybackState(playing, Math.min(posMs, durMs || posMs), durMs);
}
audio.addEventListener('seeked', function () { report(!audio.paused); });
audio.addEventListener('ended', function () { report(false); });   // Play button, notification can be swiped away

Notes: true shows the Pause button and keeps the notification from being swiped away; false shows Play. Call setMediaMetadata first, so the notification has a title. To remove the notification when the user stops, call clearMediaNotification().

Generated from the app runtime and its example files on every docs build. Read it as Markdown Β· All families.

Checked against the shipped bridge on 2026-11-04.