Создать APK бесплатно

JavaScript bridge API

Медиа и трансляция

Уведомление с управлением медиа на экране блокировки, которым управляет ваша страница, и передача видео на телевизор.

navigator.mediaSession веб-стандарт#

navigator.mediaSession + MediaMetadata

Пример

Показывает, что воспроизводит ваша страница, в медиауведомлении и на экране блокировки с кнопками «Назад» / «Воспроизведение-Пауза» / «Далее» / «Стоп», которые обращаются обратно к вашей странице. Использует обычный Media Session API.

Возвращает: ничего; нажатия кнопок вызывают обработчики, которые вы задали через setActionHandler, с { action }, как предписывает стандарт. Нужно: при сборке включите Медиауведомление на шаге 4 (Доступ). В приложениях, созданных ИИ, оно включается автоматически, если код использует 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); });

Следите за правильной кнопкой воспроизведения/паузы и индикатором прогресса:

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

Подкаст-плеер, где кнопки «Далее» / «Назад» в уведомлении перематывают на 30 секунд вперёд и на 15 секунд назад вместо смены выпуска:

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

Примечания:

  • В приложении navigator.mediaSession устанавливается до первого скрипта страницы, поэтому обработчики и метаданные можно задавать в любое время — в начале страницы или при старте воспроизведения. Он существует на главной странице, но не внутри iframe.
  • Поддерживаемые действия: play, pause, nexttrack, previoustrack, stop. Перемотка из уведомления не поддерживается, поэтому seekto никогда не срабатывает.
  • Установка playbackState сохраняет позицию из вашего последнего вызова setPositionState; вызовите setPositionState снова, когда позиция действительно меняется (перемотка).
  • Используется только последнее изображение обложки. Это должен быть полный адрес https:// в интернете (приложение скачивает его, лимит 4 с); файл внутри ZIP вашего приложения не отображается.
  • <audio> / <video> элементы отслеживаются за вас: когда страница обрабатывает кнопку ни через setActionHandler, ни через собственный window.WebToApkOnMediaAction, Play / Pause / Stop управляют воспроизводящимся медиаэлементом (иначе — тем, что играл последним, иначе — первым).
  • Чтобы убрать уведомление, вызовите WebToApk.clearMediaNotification() (установка metadata = null его не убирает).
  • Страница, определяющая window.WebToApkOnMediaAction = function (action) { ... } (действия play, pause, next, prev, stop), получает и там каждое нажатие кнопки — приложение его больше не перезаписывает. Используйте одно из двух, а не оба, иначе нажатие выполнится дважды.

requestPictureInPicture веб-стандарт#

video.requestPictureInPicture() — Picture-in-Picture

Пример

Стандартный API «картинка в картинке»: видео плавает в маленьком окне, пока пользователь занят другим. В Chrome на Android он есть, а в Android WebView его нет; в приложении видео заполняет плавающее окно, как в Chrome.

Возвращает: Promise с PictureInPictureWindow (событие width, height, resize). Видео получает enterpictureinpicture, затем leavepictureinpicture, когда пользователь закрывает или разворачивает окно; document.pictureInPictureElement — это видео в промежутке. Отклоняется с NotAllowedError без нажатия, InvalidStateError до загрузки метаданных видео или когда у него disablePictureInPicture. Требуется: включённый в сборке режим «картинка в картинке» (включается сам, когда ваш код вызывает requestPictureInPicture), Android 8 или новее.

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

Автоматически выходить в окно, когда пользователь уходит, и сообщать о неподдерживаемой сборке:

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

Дождитесь метаданных и меняйте размер вместе с окном:

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

Примечания: Android помещает всё приложение в плавающее окно, поэтому видео сначала показывается отдельно (на весь экран), а окно принимает форму видео; при выходе из окна видео возвращается на страницу. document.exitPictureInPicture() возвращает приложение в полноэкранный режим. Если в сборке нет режима «картинка в картинке», document.pictureInPictureEnabled равно false, как в браузере без него. Смена режима приходит на страницу как appmint:pip. Внутренние транспорты: __pipEnter, __pipExit, __pipAvailable, __pipPrepare.

__pipAvailable мост#

window.WebToApk.__pipAvailable(): Boolean

Сможет ли работать video.requestPictureInPicture(): режим «картинка в картинке» включён в сборке, Android 8+.

Пример

Описано вместе с WebToApk.__pipEnter — пример там показывает и этот.

Внутренний транспорт за video.requestPictureInPicture() и document.exitPictureInPicture(). Страницы используют стандартный API; никогда не вызывайте их напрямую, потому что методы __ могут измениться без предупреждения.

Возвращает: __pipEnter(width, height) возвращает "ok", когда Android принял окно (с соотношением сторон видео), иначе "unsupported" / "failed". Открытие и закрытие окна приходят как appmint:pip с {inPip}. __pipAvailable() истинно, когда в сборке включён режим «картинка в картинке» на Android 8+. Требуется: включённый в сборке режим «картинка в картинке».

Используйте публичный API:

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

Сначала проверьте поддержку и сообщите об отказе:

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

Выйдите из режима снова и следите за состоянием через стандартные события:

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

Примечания: Реализовано в оболочке приложения (PictureInPictureShim.kt). __pipPrepare не даёт оболочке повернуть экран набок на полноэкранном шаге при переходе в окно.

__pipEnter мост#

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

Открывает окно PiP с соотношением сторон видео [width]×[height]: «ok», либо «unsupported» / «failed». Смена режима приходит как `appmint:pip`.

Пример

Внутренний транспорт за video.requestPictureInPicture() и document.exitPictureInPicture(). Страницы используют стандартный API; никогда не вызывайте их напрямую, потому что методы __ могут измениться без предупреждения.

Возвращает: __pipEnter(width, height) возвращает "ok", когда Android принял окно (с соотношением сторон видео), иначе "unsupported" / "failed". Открытие и закрытие окна приходят как appmint:pip с {inPip}. __pipAvailable() истинно, когда в сборке включён режим «картинка в картинке» на Android 8+. Требуется: включённый в сборке режим «картинка в картинке».

Используйте публичный API:

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

Сначала проверьте поддержку и сообщите об отказе:

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

Выйдите из режима снова и следите за состоянием через стандартные события:

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

Примечания: Реализовано в оболочке приложения (PictureInPictureShim.kt). __pipPrepare не даёт оболочке повернуть экран набок на полноэкранном шаге при переходе в окно.

__pipExit мост#

window.WebToApk.__pipExit()

document.exitPictureInPicture(): возвращает приложение на полный экран.

Пример

Описано вместе с WebToApk.__pipEnter — пример там показывает и этот.

Внутренний транспорт за video.requestPictureInPicture() и document.exitPictureInPicture(). Страницы используют стандартный API; никогда не вызывайте их напрямую, потому что методы __ могут измениться без предупреждения.

Возвращает: __pipEnter(width, height) возвращает "ok", когда Android принял окно (с соотношением сторон видео), иначе "unsupported" / "failed". Открытие и закрытие окна приходят как appmint:pip с {inPip}. __pipAvailable() истинно, когда в сборке включён режим «картинка в картинке» на Android 8+. Требуется: включённый в сборке режим «картинка в картинке».

Используйте публичный API:

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

Сначала проверьте поддержку и сообщите об отказе:

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

Выйдите из режима снова и следите за состоянием через стандартные события:

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

Примечания: Реализовано в оболочке приложения (PictureInPictureShim.kt). __pipPrepare не даёт оболочке повернуть экран набок на полноэкранном шаге при переходе в окно.

__pipPrepare мост#

window.WebToApk.__pipPrepare()

Страница собирается развернуть видео на весь экран для PiP: не принуждайте для этого альбомную ориентацию.

Пример

Описано вместе с WebToApk.__pipEnter — пример там показывает и этот.

Внутренний транспорт за video.requestPictureInPicture() и document.exitPictureInPicture(). Страницы используют стандартный API; никогда не вызывайте их напрямую, потому что методы __ могут измениться без предупреждения.

Возвращает: __pipEnter(width, height) возвращает "ok", когда Android принял окно (с соотношением сторон видео), иначе "unsupported" / "failed". Открытие и закрытие окна приходят как appmint:pip с {inPip}. __pipAvailable() истинно, когда в сборке включён режим «картинка в картинке» на Android 8+. Требуется: включённый в сборке режим «картинка в картинке».

Используйте публичный API:

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

Сначала проверьте поддержку и сообщите об отказе:

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

Выйдите из режима снова и следите за состоянием через стандартные события:

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

Примечания: Реализовано в оболочке приложения (PictureInPictureShim.kt). __pipPrepare не даёт оболочке повернуть экран набок на полноэкранном шаге при переходе в окно.

castMedia мост#

window.WebToApk.castMedia(mediaUrl: String)

Предлагает [mediaUrl] всему, что есть на этом телефоне и может воспроизвести его на другом экране — Chromecast, приложению Smart TV, локальному плееру — через системное окно выбора. Тип определяется по URL ([CastMediaType]): аудиофайл (.mp3, .m4a, .aac, .ogg, .opus, .wav, .flac …) или аудио-URL `data:` предлагается аудиоприложениям, всё остальное — видеофайлы, манифесты потоков, URL без расширения — видеоприложениям. Это передача, а не сеанс трансляции: приложение не продолжает управлять воспроизведением, и обратного вызова нет. Странице, которой нужен настоящий сеанс, лучше использовать Cast SDK в JavaScript.

Пример

Предлагает адрес видео или аудио приложениям на телефоне, которые могут воспроизвести его на другом экране (приложение Chromecast, приложение Smart TV, видеоплеер), через системное окно Android «Транслировать на...».

Возвращает: ничего и без обратного вызова. Нужно: ничего включать не требуется. На телефоне должно быть приложение, которое принимает ссылку.

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

Отправьте выпуск подкаста из списка на колонку или приложение ТВ (ссылка .mp3 предлагается аудиоприложениям):

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

Показывайте кнопку Cast только внутри приложения и ставьте на паузу собственный плеер страницы, прежде чем передать прямую трансляцию:

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

Примечания:

  • Это передача, а не сеанс трансляции: после того как пользователь выберет приложение, ваша страница не управляет воспроизведением.
  • Адрес должен быть полной ссылкой https://, которую могут открыть другие приложения. Файл внутри ZIP вашего приложения, URL blob: или адрес веб-страницы воспроизвести не получится.
  • Тип определяется по ссылке: аудиофайл (.mp3, .m4a, .aac, .ogg, .opus, .wav, .flac …) или URL audio/ data: предлагается аудиоприложениям; всё остальное — видеофайлы, потоки .m3u8, ссылки без расширения файла — видеоприложениям.

clearMediaNotification мост#

window.WebToApk.clearMediaNotification()

Позволяет веб-контенту управлять медиауведомлением Android и получать обратные вызовы управления воспроизведением (play/pause/next/prev) с экрана блокировки и шторки уведомлений. Использование из 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() Обратный вызов Android → JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

Описывается в своей группе, Мост медиауведомлений, а не отдельно.

Пример

Убирает медиауведомление и его элементы управления на экране блокировки, например когда пользователь закрывает ваш плеер.

Возвращает: ничего. Требуется: включите Медиауведомление на шаге 4 (Доступ) при сборке. Без этого вызов ничего не делает.

Стандартного веб-вызова для этого нет, поэтому используйте мост:

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

Таймер сна, который останавливает звук и убирает уведомление через выбранное число минут:

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

Уберите уведомление, когда последний трек плейлиста закончился, вместо того чтобы оставлять кнопку «Воспроизвести» впустую:

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

Примечания: Используйте это для «остановки», а не для «паузы» (для паузы отправьте setPlaybackState(false, ...), чтобы кнопка «Воспроизвести» осталась). Установка navigator.mediaSession.metadata = null не убирает уведомление. Следующий вызов setMediaMetadata / setPlaybackState (или новые метаданные / состояние navigator.mediaSession) возвращает уведомление и элементы управления на экране блокировки.

setMediaMetadata мост#

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

Позволяет веб-контенту управлять медиауведомлением Android и получать обратные вызовы управления воспроизведением (play/pause/next/prev) с экрана блокировки и шторки уведомлений. Использование из 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() Обратный вызов Android → JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

Описывается в своей группе, Мост медиауведомлений, а не отдельно.

Пример

Задаёт название, исполнителя и обложку для медиауведомления и плеера на экране блокировки. Обычно страницы задают navigator.mediaSession.metadata, который вызывает это за вас.

Возвращает: ничего. Требуется: включите Медиауведомление на шаге 4 (Доступ) при сборке. Без этого вызов ничего не делает.

Публичный способ (используйте его):

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

Прямой вызов. Аргументы: title, artist, artworkUrl (используйте '', если картинки нет):

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

Поддерживайте актуальное название уведомления для прямой радиотрансляции, где песня меняется без загрузки страницей нового файла:

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

Примечания:

  • Уведомление появляется (или обновляется) этим вызовом. Пустое название показывает имя приложения.
  • artworkUrl должен быть полным адресом https:// в интернете. Приложение скачивает его (лимит 4 секунды); если не получилось, показывается значок приложения.
  • Получайте нажатия кнопок через navigator.mediaSession.setActionHandler (см. navigator.mediaSession) или через собственный window.WebToApkOnMediaAction = function (action) { ... } (play, pause, next, prev, stop) — используйте один из двух. После clearMediaNotification() следующий вызов этого метода (или setPlaybackState) возвращает уведомление и элементы управления на экране блокировки.

setPlaybackState мост#

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

Позволяет веб-контенту управлять медиауведомлением Android и получать обратные вызовы управления воспроизведением (play/pause/next/prev) с экрана блокировки и шторки уведомлений. Использование из 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() Обратный вызов Android → JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

Описывается в своей группе, Мост медиауведомлений, а не отдельно.

Пример

Сообщает медиауведомлению, играете вы или на паузе и в каком месте трека находитесь. Страницы обычно задают navigator.mediaSession.playbackState и вызывают setPositionState, которые вызывают это за вас.

Возвращает: ничего. Требуется: включите Медиауведомление на шаге 4 (Доступ) при сборке. Без этого вызов ничего не делает.

Публичный способ (используйте его):

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

Прямой вызов. Аргументы: isPlaying (true/false), positionMs, durationMs (целые миллисекунды):

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

Сообщите новую позицию после перемотки пользователем и переключите уведомление на «Воспроизвести», когда трек закончился:

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

Примечания: true показывает кнопку «Пауза» и не даёт смахнуть уведомление; false показывает «Воспроизвести». Сначала вызовите setMediaMetadata, чтобы у уведомления был заголовок. Чтобы убрать уведомление, когда пользователь остановил воспроизведение, вызовите clearMediaNotification().