API du pont JavaScript
Média et diffusion
La notification multimédia de l'écran de verrouillage pilotée par votre page, et l'envoi d'une vidéo vers un téléviseur.
requestPictureInPicture standard web#
video.requestPictureInPicture() — Picture-in-Picture
Exemple
L'API standard Picture-in-Picture : faites flotter une vidéo dans une petite fenêtre pendant que l'utilisateur fait autre chose. Chrome sur Android l'a, la WebView d'Android non ; dans l'application, la vidéo remplit la fenêtre flottante, comme dans Chrome.
Renvoie : une Promise pour un PictureInPictureWindow (événements width, height, resize). La vidéo reçoit enterpictureinpicture, puis leavepictureinpicture lorsque l'utilisateur ferme ou agrandit la fenêtre ; document.pictureInPictureElement est la vidéo entre-temps. Rejette avec NotAllowedError sans toucher, InvalidStateError avant le chargement des métadonnées de la vidéo ou lorsqu'elle a disablePictureInPicture. Nécessite : Picture-in-picture activé dans la compilation (il s'active de lui-même lorsque votre code appelle requestPictureInPicture), Android 8 ou plus récent.
<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>Passer automatiquement en fenĂȘtre flottante quand l'utilisateur part, et signaler une compilation non compatible :
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
}
});Attendez les métadonnées et redimensionnez avec la fenêtre :
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'));Remarques : Android place toute l'application dans la fenĂȘtre flottante : la vidĂ©o est donc d'abord affichĂ©e seule (plein Ă©cran) et la fenĂȘtre prend la forme de la vidĂ©o ; en quittant la fenĂȘtre, la vidĂ©o revient dans la page. document.exitPictureInPicture() ramĂšne l'application en plein Ă©cran. Quand la compilation n'a pas de picture-in-picture, document.pictureInPictureEnabled vaut false, comme dans un navigateur qui ne le gĂšre pas. Le changement de mode parvient Ă la page sous la forme appmint:pip. Transports internes : __pipEnter, __pipExit, __pipAvailable, __pipPrepare.
__pipAvailable pont#
window.WebToApk.__pipAvailable(): Boolean
Indique si video.requestPictureInPicture() peut fonctionner : PiP activé dans la compilation, Android 8+.
Exemple
Documenté avec WebToApk.__pipEnter : l'exemple qui s'y trouve illustre aussi celui-ci.
Transport interne derrière video.requestPictureInPicture() et document.exitPictureInPicture(). Les pages utilisent l'API standard ; n'appelez jamais ces méthodes directement, car les méthodes __ peuvent changer sans préavis.
Renvoie : __pipEnter(width, height) renvoie "ok" lorsqu'Android a accepté la fenêtre (avec le format de la vidéo), sinon "unsupported" / "failed". L'ouverture et la fermeture de la fenêtre arrivent sous la forme appmint:pip avec {inPip}. __pipAvailable() est vrai lorsque la compilation a le picture-in-picture activé sur Android 8+. Nécessite : picture-in-picture activé dans la compilation.
Utilisez l'API publique :
const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());Vérifiez d'abord la prise en charge, et signalez un refus :
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.');
}
}Quittez-le à nouveau et suivez l'état avec les événements standard :
video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };Remarques : ImplĂ©mentĂ© dans la coque de l'application (PictureInPictureShim.kt). __pipPrepare empĂȘche la coque de faire pivoter l'Ă©cran pour l'Ă©tape plein Ă©cran lors du passage en fenĂȘtre.
__pipEnter pont#
window.WebToApk.__pipEnter(width: Int, height: Int): String
Ouvre la fenĂȘtre PiP avec le format d'une vidĂ©o de [width]×[height] : « ok », ou « unsupported » / « failed ». Le changement de mode arrive sous la forme `appmint:pip`.
Exemple
Transport interne derrière video.requestPictureInPicture() et document.exitPictureInPicture(). Les pages utilisent l'API standard ; n'appelez jamais ces méthodes directement, car les méthodes __ peuvent changer sans préavis.
Renvoie : __pipEnter(width, height) renvoie "ok" lorsqu'Android a accepté la fenêtre (avec le format de la vidéo), sinon "unsupported" / "failed". L'ouverture et la fermeture de la fenêtre arrivent sous la forme appmint:pip avec {inPip}. __pipAvailable() est vrai lorsque la compilation a le picture-in-picture activé sur Android 8+. Nécessite : picture-in-picture activé dans la compilation.
Utilisez l'API publique :
const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());Vérifiez d'abord la prise en charge, et signalez un refus :
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.');
}
}Quittez-le à nouveau et suivez l'état avec les événements standard :
video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };Remarques : ImplĂ©mentĂ© dans la coque de l'application (PictureInPictureShim.kt). __pipPrepare empĂȘche la coque de faire pivoter l'Ă©cran pour l'Ă©tape plein Ă©cran lors du passage en fenĂȘtre.
__pipExit pont#
window.WebToApk.__pipExit()
document.exitPictureInPicture() : ramène l'application en plein écran.
Exemple
Documenté avec WebToApk.__pipEnter : l'exemple qui s'y trouve illustre aussi celui-ci.
Transport interne derrière video.requestPictureInPicture() et document.exitPictureInPicture(). Les pages utilisent l'API standard ; n'appelez jamais ces méthodes directement, car les méthodes __ peuvent changer sans préavis.
Renvoie : __pipEnter(width, height) renvoie "ok" lorsqu'Android a accepté la fenêtre (avec le format de la vidéo), sinon "unsupported" / "failed". L'ouverture et la fermeture de la fenêtre arrivent sous la forme appmint:pip avec {inPip}. __pipAvailable() est vrai lorsque la compilation a le picture-in-picture activé sur Android 8+. Nécessite : picture-in-picture activé dans la compilation.
Utilisez l'API publique :
const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());Vérifiez d'abord la prise en charge, et signalez un refus :
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.');
}
}Quittez-le à nouveau et suivez l'état avec les événements standard :
video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };Remarques : ImplĂ©mentĂ© dans la coque de l'application (PictureInPictureShim.kt). __pipPrepare empĂȘche la coque de faire pivoter l'Ă©cran pour l'Ă©tape plein Ă©cran lors du passage en fenĂȘtre.
__pipPrepare pont#
window.WebToApk.__pipPrepare()
La page va passer une vidéo en plein écran pour le PiP : ne forcez pas le mode paysage pour elle.
Exemple
Documenté avec WebToApk.__pipEnter : l'exemple qui s'y trouve illustre aussi celui-ci.
Transport interne derrière video.requestPictureInPicture() et document.exitPictureInPicture(). Les pages utilisent l'API standard ; n'appelez jamais ces méthodes directement, car les méthodes __ peuvent changer sans préavis.
Renvoie : __pipEnter(width, height) renvoie "ok" lorsqu'Android a accepté la fenêtre (avec le format de la vidéo), sinon "unsupported" / "failed". L'ouverture et la fermeture de la fenêtre arrivent sous la forme appmint:pip avec {inPip}. __pipAvailable() est vrai lorsque la compilation a le picture-in-picture activé sur Android 8+. Nécessite : picture-in-picture activé dans la compilation.
Utilisez l'API publique :
const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());Vérifiez d'abord la prise en charge, et signalez un refus :
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.');
}
}Quittez-le à nouveau et suivez l'état avec les événements standard :
video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };Remarques : ImplĂ©mentĂ© dans la coque de l'application (PictureInPictureShim.kt). __pipPrepare empĂȘche la coque de faire pivoter l'Ă©cran pour l'Ă©tape plein Ă©cran lors du passage en fenĂȘtre.
castMedia pont#
window.WebToApk.castMedia(mediaUrl: String)
Propose [mediaUrl] à tout ce qui, sur ce téléphone, peut le lire sur un autre écran - Chromecast, une application de Smart TV, un lecteur local - via le sélecteur du système. Le type est déduit de l'URL ([CastMediaType]) : un fichier audio (.mp3, .m4a, .aac, .ogg, .opus, .wav, .flac …) ou une URL `data:` audio est proposé aux applications audio, tout le reste - fichiers vidéo, manifestes de flux, URL sans extension - aux applications vidéo. C'est un transfert, pas une session de diffusion : l'application ne garde pas le contrôle de la lecture, et il n'y a pas de rappel. Une page qui a besoin d'une vraie session doit utiliser le SDK Cast en JavaScript.
Exemple
Propose une adresse vidéo ou audio aux applications du téléphone qui peuvent la lire sur un autre écran (une application Chromecast, une application de Smart TV, un lecteur vidéo), via le sélecteur Android « Caster vers… ».
Retourne : rien, et aucun callback. Nécessite : rien à activer. Le téléphone doit avoir une application qui accepte le lien.
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);
});Envoyez un épisode de podcast d'une liste vers une enceinte ou une application de TV (un lien .mp3 est proposé aux applications audio) :
// <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);
}
});Afficher le bouton Cast uniquement dans l'application, et mettre en pause le lecteur de la page avant de passer un flux en direct :
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
});Remarques :
- C'est un transfert, pas une session de diffusion : votre page ne contrĂŽle pas la lecture une fois que l'utilisateur a choisi une application.
- L'adresse doit être un lien
https://complet que d'autres applications peuvent ouvrir. Un fichier dans le ZIP de votre application, une URLblob:ou l'adresse d'une page web ne sera pas lue. - Le type vient du lien : un fichier audio (
.mp3,.m4a,.aac,.ogg,.opus,.wav,.flac…) ou une URLaudio/data:est proposé aux applications audio ; tout le reste - fichiers vidéo, flux.m3u8, liens sans extension de fichier - aux applications vidéo.
clearMediaNotification pont#
window.WebToApk.clearMediaNotification()
Permet au contenu web de piloter la notification multimĂ©dia d'Android et de recevoir les callbacks de commande de lecture (lecture/pause/suivant/prĂ©cĂ©dent) depuis l'Ă©cran de verrouillage et le volet de notifications. Utilisation depuis JavaScript : window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // lecture, pos ms, durĂ©e ms window.WebToApk.clearMediaNotification() Callback d'Android → JS : window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')
Décrit par son groupe, Pont de notification média, plutôt que seul.
Exemple
Supprime la notification média et ses commandes de l'écran de verrouillage, par exemple lorsque l'utilisateur ferme votre lecteur.
Renvoie : rien. Nécessite : activez Notification média à l'étape 4 (Accès) lors de la compilation. Sans cela, l'appel ne fait rien.
Il n'existe pas d'appel web standard pour cela ; utilisez donc le pont :
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;
});
}Un minuteur de sommeil qui arrĂȘte l'audio et supprime la notification aprĂšs le nombre de minutes choisi :
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); });Supprimez la notification lorsque la dernière piste d'une liste de lecture est terminée, au lieu de laisser un bouton Lecture inutile :
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();
}
});Remarques : Utilisez ceci pour « arrĂȘter », pas pour « mettre en pause » (pour la pause, envoyez setPlaybackState(false, ...) afin que le bouton Lecture reste affichĂ©). DĂ©finir navigator.mediaSession.metadata = null ne supprime pas la notification. Le prochain setMediaMetadata / setPlaybackState (ou de nouvelles mĂ©tadonnĂ©es / un nouvel Ă©tat navigator.mediaSession) ramĂšne la notification et les commandes de l'Ă©cran de verrouillage.
setMediaMetadata pont#
window.WebToApk.setMediaMetadata(title: String, artist: String, artworkUrl: String)
Permet au contenu web de piloter la notification multimĂ©dia d'Android et de recevoir les callbacks de commande de lecture (lecture/pause/suivant/prĂ©cĂ©dent) depuis l'Ă©cran de verrouillage et le volet de notifications. Utilisation depuis JavaScript : window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // lecture, pos ms, durĂ©e ms window.WebToApk.clearMediaNotification() Callback d'Android → JS : window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')
Décrit par son groupe, Pont de notification média, plutôt que seul.
Exemple
Définit le titre, l'artiste et la pochette de la notification multimédia et du lecteur de l'écran de verrouillage. Les pages définissent normalement navigator.mediaSession.metadata, qui appelle cette fonction pour vous.
Renvoie : rien. Nécessite : activez Notification média à l'étape 4 (Accès) lors de la compilation. Sans cela, l'appel ne fait rien.
La méthode publique (utilisez celle-ci) :
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' }]
});
}L'appel brut. Arguments : title, artist, artworkUrl (utilisez '' pour aucune image) :
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(); });
}Gardez le titre de la notification à jour pour une radio en direct, où la chanson change sans que la page charge un nouveau fichier :
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 });Remarques :
- La notification apparaît (ou se met à jour) avec cet appel. Un titre vide affiche le nom de l'application.
artworkUrldoit être une adressehttps://complète sur Internet. L'application la télécharge (limite de 4 secondes) ; en cas d'échec, l'icône de l'application s'affiche.- Recevez les appuis sur les boutons avec
navigator.mediaSession.setActionHandler(voirnavigator.mediaSession), ou avec votre proprewindow.WebToApkOnMediaAction = function (action) { ... }(play,pause,next,prev,stop) - utilisez l'un des deux. AprèsclearMediaNotification(), le prochain appel à celle-ci (ou àsetPlaybackState) fait revenir la notification et les commandes de l'écran de verrouillage.
setPlaybackState pont#
window.WebToApk.setPlaybackState(isPlaying: Boolean, positionMs: Long, durationMs: Long)
Permet au contenu web de piloter la notification multimĂ©dia d'Android et de recevoir les callbacks de commande de lecture (lecture/pause/suivant/prĂ©cĂ©dent) depuis l'Ă©cran de verrouillage et le volet de notifications. Utilisation depuis JavaScript : window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // lecture, pos ms, durĂ©e ms window.WebToApk.clearMediaNotification() Callback d'Android → JS : window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')
Décrit par son groupe, Pont de notification média, plutôt que seul.
Exemple
Indique à la notification média si vous êtes en lecture ou en pause, et où vous en êtes dans la piste. Les pages définissent normalement navigator.mediaSession.playbackState et appellent setPositionState, qui appellent cette méthode pour vous.
Renvoie : rien. Nécessite : activez Notification média à l'étape 4 (Accès) lors de la compilation. Sans cela, l'appel ne fait rien.
La méthode publique (utilisez celle-ci) :
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);L'appel brut. Arguments : isPlaying (true/false), positionMs, durationMs (millisecondes entières) :
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)
);
}Signalez la nouvelle position après une recherche de l'utilisateur, et passez la notification sur Lecture à la fin de la piste :
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 awayRemarques : true affiche le bouton Pause et empĂȘche de faire glisser la notification pour la supprimer ; false affiche Lecture. Appelez d'abord setMediaMetadata, pour que la notification ait un titre. Pour supprimer la notification quand l'utilisateur arrĂȘte, appelez clearMediaNotification().