API del puente de JavaScript
Multimedia y transmisión
La notificaciĂłn multimedia de la pantalla de bloqueo que controla tu pĂĄgina, y cĂłmo pasar un vĂdeo a una TV.
requestPictureInPicture estándar web#
video.requestPictureInPicture() — Picture-in-Picture
Ejemplo
La API estándar de Imagen en imagen: muestra un video flotando en una ventana pequeña mientras el usuario hace otra cosa. Chrome en Android la tiene y el WebView de Android no; en la app el video llena la ventana flotante, como en Chrome.
Devuelve: una Promise de un PictureInPictureWindow (evento width, height, resize). El video recibe enterpictureinpicture, luego leavepictureinpicture cuando el usuario cierra o expande la ventana; document.pictureInPictureElement es el video mientras tanto. Se rechaza con NotAllowedError sin un toque, InvalidStateError antes de que se hayan cargado los metadatos del video o cuando tiene disablePictureInPicture. Requiere: Imagen en imagen activada en la compilación (se activa sola cuando tu código llama a requestPictureInPicture), Android 8 o posterior.
<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>Salir automĂĄticamente cuando el usuario se va e informar de una compilaciĂłn no 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
}
});Espera los metadatos y cambia el tamaño con la ventana:
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'));Notas: Android pasa toda la app a la ventana flotante, asĂ que el vĂdeo se muestra primero solo (a pantalla completa) y la ventana adopta la forma del vĂdeo; al salir de la ventana el vĂdeo vuelve a la pĂĄgina. document.exitPictureInPicture() devuelve la app a pantalla completa. Cuando la compilaciĂłn no tiene imagen en imagen, document.pictureInPictureEnabled es false, igual que en un navegador que no la tiene. El cambio de modo llega a la pĂĄgina como appmint:pip. Transportes internos: __pipEnter, __pipExit, __pipAvailable, __pipPrepare.
__pipAvailable puente#
window.WebToApk.__pipAvailable(): Boolean
Si video.requestPictureInPicture() puede funcionar: PiP activado en la compilaciĂłn, Android 8+.
Ejemplo
Documentado junto con WebToApk.__pipEnter: el ejemplo de allí también muestra este.
Transporte interno detrás de video.requestPictureInPicture() y document.exitPictureInPicture(). Las páginas usan la API estándar; nunca llames a estos directamente, porque los métodos __ pueden cambiar sin aviso.
Devuelve: __pipEnter(width, height) devuelve "ok" cuando Android aceptó la ventana (con la relación de aspecto del video), o si no "unsupported" / "failed". La apertura y el cierre de la ventana llegan como appmint:pip con {inPip}. __pipAvailable() es verdadero cuando la compilación tiene imagen en imagen en Android 8+. Requiere: imagen en imagen activada en la compilación.
Usa la API pública:
const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());Comprueba primero la compatibilidad e informa de un rechazo:
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.');
}
}Sal de nuevo y sigue el estado con los eventos estándar:
video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };Notas: Implementado en el shell de la app (PictureInPictureShim.kt). __pipPrepare evita que el shell gire la pantalla de lado para el paso a pantalla completa al entrar en la ventana.
__pipEnter puente#
window.WebToApk.__pipEnter(width: Int, height: Int): String
Abre la ventana PiP con la relaciĂłn de aspecto de un vĂdeo de [width]×[height]: "ok", o "unsupported" / "failed". El cambio de modo llega como `appmint:pip`.
Ejemplo
Transporte interno detrás de video.requestPictureInPicture() y document.exitPictureInPicture(). Las páginas usan la API estándar; nunca llames a estos directamente, porque los métodos __ pueden cambiar sin aviso.
Devuelve: __pipEnter(width, height) devuelve "ok" cuando Android aceptó la ventana (con la relación de aspecto del video), o si no "unsupported" / "failed". La apertura y el cierre de la ventana llegan como appmint:pip con {inPip}. __pipAvailable() es verdadero cuando la compilación tiene imagen en imagen en Android 8+. Requiere: imagen en imagen activada en la compilación.
Usa la API pública:
const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());Comprueba primero la compatibilidad e informa de un rechazo:
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.');
}
}Sal de nuevo y sigue el estado con los eventos estándar:
video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };Notas: Implementado en el shell de la app (PictureInPictureShim.kt). __pipPrepare evita que el shell gire la pantalla de lado para el paso a pantalla completa al entrar en la ventana.
__pipExit puente#
window.WebToApk.__pipExit()
document.exitPictureInPicture(): devuelve la app a pantalla completa.
Ejemplo
Documentado junto con WebToApk.__pipEnter: el ejemplo de allí también muestra este.
Transporte interno detrás de video.requestPictureInPicture() y document.exitPictureInPicture(). Las páginas usan la API estándar; nunca llames a estos directamente, porque los métodos __ pueden cambiar sin aviso.
Devuelve: __pipEnter(width, height) devuelve "ok" cuando Android aceptó la ventana (con la relación de aspecto del video), o si no "unsupported" / "failed". La apertura y el cierre de la ventana llegan como appmint:pip con {inPip}. __pipAvailable() es verdadero cuando la compilación tiene imagen en imagen en Android 8+. Requiere: imagen en imagen activada en la compilación.
Usa la API pública:
const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());Comprueba primero la compatibilidad e informa de un rechazo:
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.');
}
}Sal de nuevo y sigue el estado con los eventos estándar:
video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };Notas: Implementado en el shell de la app (PictureInPictureShim.kt). __pipPrepare evita que el shell gire la pantalla de lado para el paso a pantalla completa al entrar en la ventana.
__pipPrepare puente#
window.WebToApk.__pipPrepare()
La pĂĄgina estĂĄ a punto de poner un vĂdeo a pantalla completa para PiP: no fuerces el modo horizontal para ello.
Ejemplo
Documentado junto con WebToApk.__pipEnter: el ejemplo de allí también muestra este.
Transporte interno detrás de video.requestPictureInPicture() y document.exitPictureInPicture(). Las páginas usan la API estándar; nunca llames a estos directamente, porque los métodos __ pueden cambiar sin aviso.
Devuelve: __pipEnter(width, height) devuelve "ok" cuando Android aceptó la ventana (con la relación de aspecto del video), o si no "unsupported" / "failed". La apertura y el cierre de la ventana llegan como appmint:pip con {inPip}. __pipAvailable() es verdadero cuando la compilación tiene imagen en imagen en Android 8+. Requiere: imagen en imagen activada en la compilación.
Usa la API pública:
const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());Comprueba primero la compatibilidad e informa de un rechazo:
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.');
}
}Sal de nuevo y sigue el estado con los eventos estándar:
video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };Notas: Implementado en el shell de la app (PictureInPictureShim.kt). __pipPrepare evita que el shell gire la pantalla de lado para el paso a pantalla completa al entrar en la ventana.
castMedia puente#
window.WebToApk.castMedia(mediaUrl: String)
Ofrece [mediaUrl] a lo que haya en este teléfono que pueda reproducirlo en otra pantalla (Chromecast, una app de Smart TV, un reproductor local) mediante el selector del sistema. El tipo se toma de la URL ([CastMediaType]): un archivo de audio (.mp3, .m4a, .aac, .ogg, .opus, .wav, .flac …) o una URL `data:` de audio se ofrece a apps de audio; cualquier otra cosa (archivos de video, manifiestos de transmisión, URL sin extensión) a apps de video. Es un traspaso, no una sesión de transmisión: la app no sigue controlando la reproducción y no hay callback. Una página que necesite una sesión real debe usar el SDK de Cast en JavaScript.
Ejemplo
Ofrece la dirección de un video o audio a las apps del teléfono que pueden reproducirlo en otra pantalla (una app de Chromecast, una app de Smart TV, un reproductor de video), mediante el selector "Transmitir a..." de Android.
Devuelve: nada, y sin callback. Requiere: nada que activar. El teléfono necesita una app que acepte el enlace.
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);
});Envía un episodio de podcast desde una lista a una app de altavoz o TV (un enlace .mp3 se ofrece a apps de 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);
}
});Muestra el botĂłn de Cast solo dentro de la app y pausa el reproductor de la propia pĂĄgina antes de pasar una transmisiĂłn en vivo:
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
});Notas:
- Esto es un traspaso, no una sesión de cast: tu pågina no controla la reproducción después de que el usuario elige una app.
- La dirección debe ser un enlace
https://completo que otras apps puedan abrir. Un archivo dentro del ZIP de tu app, una URLblob:o la dirección de una página web no se reproducirán. - El tipo viene del enlace: un archivo de audio (
.mp3,.m4a,.aac,.ogg,.opus,.wav,.flac…) o una URLaudio/data:se ofrece a apps de audio; cualquier otra cosa (archivos de video, transmisiones.m3u8, enlaces sin extensión de archivo) a apps de video.
clearMediaNotification puente#
window.WebToApk.clearMediaNotification()
Permite que el contenido web controle la notificaciĂłn de estilo multimedia de Android y reciba los callbacks de control de transporte (play/pause/next/prev) desde la pantalla de bloqueo y el panel de notificaciones. Uso desde JavaScript: window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // reproduciendo, pos ms, dur ms window.WebToApk.clearMediaNotification() Callback de Android → JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')
Se describe por su grupo, Puente de notificación multimedia, y no por separado.
Ejemplo
Quita la notificación multimedia y sus controles de la pantalla de bloqueo, por ejemplo cuando el usuario cierra tu reproductor.
Devuelve: nada. Requiere: activa Notificación multimedia en el Paso 4 (Acceso) al compilar. Sin eso la llamada no hace nada.
No hay una llamada web estándar para esto, así que usa el puente:
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 temporizador de sueño que detiene el audio y elimina la notificaciĂłn tras los minutos elegidos:
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); });Quita la notificación cuando termine la última pista de una lista de reproducción, en lugar de dejar un botón Reproducir sin sentido:
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();
}
});Notas: Usa esto para "detener", no para "pausar" (para pausar, envĂa setPlaybackState(false, ...) y asĂ se mantiene el botĂłn de reproducir). Establecer navigator.mediaSession.metadata = null no elimina la notificaciĂłn. La siguiente llamada a setMediaMetadata / setPlaybackState (o nuevos metadatos / estado de navigator.mediaSession) devuelve la notificaciĂłn y los controles de la pantalla de bloqueo.
setMediaMetadata puente#
window.WebToApk.setMediaMetadata(title: String, artist: String, artworkUrl: String)
Permite que el contenido web controle la notificaciĂłn de estilo multimedia de Android y reciba los callbacks de control de transporte (play/pause/next/prev) desde la pantalla de bloqueo y el panel de notificaciones. Uso desde JavaScript: window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // reproduciendo, pos ms, dur ms window.WebToApk.clearMediaNotification() Callback de Android → JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')
Se describe por su grupo, Puente de notificación multimedia, y no por separado.
Ejemplo
Establece el tĂtulo, el artista y la carĂĄtula de la notificaciĂłn multimedia y del reproductor de la pantalla de bloqueo. Normalmente las pĂĄginas establecen navigator.mediaSession.metadata, que llama a esto por ti.
Devuelve: nada. Requiere: activa Notificación multimedia en el Paso 4 (Acceso) al compilar. Sin eso la llamada no hace nada.
La forma pública (usa esta):
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' }]
});
}La llamada directa. Argumentos: title, artist, artworkUrl (usa '' para no poner imagen):
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(); });
}Mantén actualizado el título de la notificación para una transmisión de radio en vivo, donde la canción cambia sin que la página cargue un archivo nuevo:
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 });Notas:
- La notificación aparece (o se actualiza) con esta llamada. Un título vacío muestra el nombre de la app.
artworkUrldebe ser una direcciónhttps://completa en internet. La app la descarga (límite de 4 segundos); si falla, se muestra el icono de la app.- Recibe las pulsaciones de botones con
navigator.mediaSession.setActionHandler(consultanavigator.mediaSession), o con tu propiowindow.WebToApkOnMediaAction = function (action) { ... }(play,pause,next,prev,stop): usa uno de los dos. Después declearMediaNotification(), la siguiente llamada a esto (o asetPlaybackState) devuelve la notificación y los controles de la pantalla de bloqueo.
setPlaybackState puente#
window.WebToApk.setPlaybackState(isPlaying: Boolean, positionMs: Long, durationMs: Long)
Permite que el contenido web controle la notificaciĂłn de estilo multimedia de Android y reciba los callbacks de control de transporte (play/pause/next/prev) desde la pantalla de bloqueo y el panel de notificaciones. Uso desde JavaScript: window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // reproduciendo, pos ms, dur ms window.WebToApk.clearMediaNotification() Callback de Android → JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')
Se describe por su grupo, Puente de notificación multimedia, y no por separado.
Ejemplo
Indica a la notificación multimedia si estás reproduciendo o en pausa, y dónde vas en la pista. Las páginas normalmente fijan navigator.mediaSession.playbackState y llaman a setPositionState, que llaman a esto por ti.
Devuelve: nada. Requiere: activa Notificación multimedia en el Paso 4 (Acceso) al compilar. Sin eso la llamada no hace nada.
La forma pública (usa esta):
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);La llamada directa. Argumentos: isPlaying (true/false), positionMs, durationMs (milisegundos enteros):
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)
);
}Informa la nueva posición después de que el usuario busque y cambia la notificación a Reproducir cuando termine la pista:
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 awayNotas: true muestra el botĂłn Pausa y evita que la notificaciĂłn se pueda descartar deslizando; false muestra Reproducir. Llama primero a setMediaMetadata, para que la notificaciĂłn tenga tĂtulo. Para quitar la notificaciĂłn cuando el usuario detiene, llama a clearMediaNotification().