API jembatan JavaScript
Media dan casting
Notifikasi media di layar kunci yang dikendalikan halaman Anda, dan mengirim video ke TV.
requestPictureInPicture standar web#
video.requestPictureInPicture() — Picture-in-Picture
Contoh
API Picture-in-Picture standar: mengambangkan video di jendela kecil saat pengguna melakukan hal lain. Chrome di Android memilikinya dan WebView Android tidak; di aplikasi video memenuhi jendela mengambang, seperti di Chrome.
Mengembalikan: Promise untuk PictureInPictureWindow (event width, height, resize). Video mendapat enterpictureinpicture, lalu leavepictureinpicture saat pengguna menutup atau memperluas jendela; document.pictureInPictureElement adalah video itu sementara itu. Ditolak dengan NotAllowedError tanpa ketukan, InvalidStateError sebelum metadata video dimuat atau saat ada disablePictureInPicture. Memerlukan: Picture-in-picture diaktifkan di build (aktif sendiri saat kode Anda memanggil requestPictureInPicture), Android 8 atau lebih baru.
<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>Munculkan otomatis saat pengguna pergi, dan laporkan build yang tidak mendukung:
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
}
});Tunggu metadata, dan ubah ukuran bersama jendela:
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'));Catatan: Android memasukkan seluruh aplikasi ke jendela mengambang, sehingga video lebih dulu ditampilkan sendiri (layar penuh) dan jendela mengikuti bentuk video; saat keluar dari jendela, video kembali ke halaman. document.exitPictureInPicture() mengembalikan aplikasi ke layar penuh. Jika build tidak memiliki picture-in-picture, document.pictureInPictureEnabled bernilai false, seperti di browser yang tidak mendukungnya. Perubahan mode sampai ke halaman sebagai appmint:pip. Transport internal: __pipEnter, __pipExit, __pipAvailable, __pipPrepare.
__pipAvailable jembatan#
window.WebToApk.__pipAvailable(): Boolean
Apakah video.requestPictureInPicture() bisa berfungsi: PiP aktif di build, Android 8+.
Contoh
Didokumentasikan bersama WebToApk.__pipEnter - contoh di sana menunjukkan yang ini juga.
Transport internal di balik video.requestPictureInPicture() dan document.exitPictureInPicture(). Halaman memakai API standar; jangan pernah memanggil ini langsung, karena metode __ dapat berubah tanpa pemberitahuan.
Mengembalikan: __pipEnter(width, height) mengembalikan "ok" saat Android menerima jendela (dengan rasio aspek video), jika tidak "unsupported" / "failed". Pembukaan dan penutupan jendela datang sebagai appmint:pip dengan {inPip}. __pipAvailable() bernilai true saat build mengaktifkan picture-in-picture di Android 8+. Memerlukan: picture-in-picture diaktifkan di build.
Gunakan API publik:
const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());Periksa dukungan dulu, dan laporkan penolakan:
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.');
}
}Keluar lagi dan ikuti statusnya dengan event standar:
video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };Catatan: Diimplementasikan di shell aplikasi (PictureInPictureShim.kt). __pipPrepare mencegah shell memutar layar ke samping untuk langkah layar penuh saat masuk ke jendela.
__pipEnter jembatan#
window.WebToApk.__pipEnter(width: Int, height: Int): String
Membuka jendela PiP dengan rasio aspek video [width]×[height]: "ok", atau "unsupported" / "failed". Perubahan mode datang sebagai `appmint:pip`.
Contoh
Transport internal di balik video.requestPictureInPicture() dan document.exitPictureInPicture(). Halaman memakai API standar; jangan pernah memanggil ini langsung, karena metode __ dapat berubah tanpa pemberitahuan.
Mengembalikan: __pipEnter(width, height) mengembalikan "ok" saat Android menerima jendela (dengan rasio aspek video), jika tidak "unsupported" / "failed". Pembukaan dan penutupan jendela datang sebagai appmint:pip dengan {inPip}. __pipAvailable() bernilai true saat build mengaktifkan picture-in-picture di Android 8+. Memerlukan: picture-in-picture diaktifkan di build.
Gunakan API publik:
const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());Periksa dukungan dulu, dan laporkan penolakan:
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.');
}
}Keluar lagi dan ikuti statusnya dengan event standar:
video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };Catatan: Diimplementasikan di shell aplikasi (PictureInPictureShim.kt). __pipPrepare mencegah shell memutar layar ke samping untuk langkah layar penuh saat masuk ke jendela.
__pipExit jembatan#
window.WebToApk.__pipExit()
document.exitPictureInPicture(): mengembalikan aplikasi ke layar penuh.
Contoh
Didokumentasikan bersama WebToApk.__pipEnter - contoh di sana menunjukkan yang ini juga.
Transport internal di balik video.requestPictureInPicture() dan document.exitPictureInPicture(). Halaman memakai API standar; jangan pernah memanggil ini langsung, karena metode __ dapat berubah tanpa pemberitahuan.
Mengembalikan: __pipEnter(width, height) mengembalikan "ok" saat Android menerima jendela (dengan rasio aspek video), jika tidak "unsupported" / "failed". Pembukaan dan penutupan jendela datang sebagai appmint:pip dengan {inPip}. __pipAvailable() bernilai true saat build mengaktifkan picture-in-picture di Android 8+. Memerlukan: picture-in-picture diaktifkan di build.
Gunakan API publik:
const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());Periksa dukungan dulu, dan laporkan penolakan:
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.');
}
}Keluar lagi dan ikuti statusnya dengan event standar:
video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };Catatan: Diimplementasikan di shell aplikasi (PictureInPictureShim.kt). __pipPrepare mencegah shell memutar layar ke samping untuk langkah layar penuh saat masuk ke jendela.
__pipPrepare jembatan#
window.WebToApk.__pipPrepare()
Halaman akan menampilkan video layar penuh untuk PiP: jangan paksa mode lanskap untuknya.
Contoh
Didokumentasikan bersama WebToApk.__pipEnter - contoh di sana menunjukkan yang ini juga.
Transport internal di balik video.requestPictureInPicture() dan document.exitPictureInPicture(). Halaman memakai API standar; jangan pernah memanggil ini langsung, karena metode __ dapat berubah tanpa pemberitahuan.
Mengembalikan: __pipEnter(width, height) mengembalikan "ok" saat Android menerima jendela (dengan rasio aspek video), jika tidak "unsupported" / "failed". Pembukaan dan penutupan jendela datang sebagai appmint:pip dengan {inPip}. __pipAvailable() bernilai true saat build mengaktifkan picture-in-picture di Android 8+. Memerlukan: picture-in-picture diaktifkan di build.
Gunakan API publik:
const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());Periksa dukungan dulu, dan laporkan penolakan:
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.');
}
}Keluar lagi dan ikuti statusnya dengan event standar:
video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };Catatan: Diimplementasikan di shell aplikasi (PictureInPictureShim.kt). __pipPrepare mencegah shell memutar layar ke samping untuk langkah layar penuh saat masuk ke jendela.
castMedia jembatan#
window.WebToApk.castMedia(mediaUrl: String)
Menawarkan [mediaUrl] ke apa pun di ponsel ini yang dapat memutarnya di layar lain - Chromecast, aplikasi Smart TV, pemutar lokal - lewat pemilih sistem. Tipenya diambil dari URL ([CastMediaType]): berkas audio (.mp3, .m4a, .aac, .ogg, .opus, .wav, .flac …) atau URL `data:` audio ditawarkan ke aplikasi audio, selain itu - berkas video, manifes stream, URL tanpa ekstensi - ke aplikasi video. Ini adalah penyerahan, bukan sesi cast: aplikasi tidak tetap mengendalikan pemutaran, dan tidak ada callback. Halaman yang membutuhkan sesi sungguhan sebaiknya memakai Cast SDK di JavaScript.
Contoh
Menawarkan alamat video atau audio ke aplikasi di ponsel yang dapat memutarnya di layar lain (aplikasi Chromecast, aplikasi Smart TV, pemutar video), lewat pemilih "Cast ke..." Android.
Mengembalikan: tidak ada, dan tanpa callback. Membutuhkan: tidak ada yang perlu diaktifkan. Ponsel membutuhkan aplikasi yang menerima tautan tersebut.
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);
});Kirim episode podcast dari daftar ke speaker atau aplikasi TV (tautan .mp3 ditawarkan ke aplikasi 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);
}
});Tampilkan tombol Cast hanya di dalam aplikasi, dan jeda pemutar halaman sendiri sebelum menyerahkan siaran langsung:
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
});Catatan:
- Ini adalah serah-terima, bukan sesi cast: halaman Anda tidak mengendalikan pemutaran setelah pengguna memilih aplikasi.
- Alamat harus berupa tautan
https://lengkap yang dapat dibuka aplikasi lain. Berkas di dalam ZIP aplikasi Anda, URLblob:atau alamat halaman web tidak akan diputar. - Tipe berasal dari tautan: berkas audio (
.mp3,.m4a,.aac,.ogg,.opus,.wav,.flac…) atau URLaudio/data:ditawarkan ke aplikasi audio; selain itu - berkas video, stream.m3u8, tautan tanpa ekstensi berkas - ke aplikasi video.
clearMediaNotification jembatan#
window.WebToApk.clearMediaNotification()
Memungkinkan konten web mengendalikan notifikasi bergaya media Android dan menerima callback kontrol transport (putar/jeda/berikutnya/sebelumnya) dari layar kunci dan bayangan notifikasi. Penggunaan dari JavaScript: window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // memutar, posisi ms, durasi ms window.WebToApk.clearMediaNotification() Callback dari Android → JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')
Dijelaskan oleh grupnya, Jembatan Notifikasi Media, bukan sendiri.
Contoh
Menghapus notifikasi media dan kontrol layar kuncinya, misalnya saat pengguna menutup pemutar Anda.
Mengembalikan: tidak ada. Memerlukan: aktifkan Notifikasi Media di Langkah 4 (Akses) saat Anda membuat build. Tanpanya panggilan ini tidak melakukan apa-apa.
Tidak ada panggilan web standar untuk ini, jadi gunakan jembatan:
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;
});
}Timer tidur yang menghentikan audio dan menghapus notifikasi setelah jumlah menit yang dipilih:
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); });Hapus notifikasi saat trek terakhir daftar putar selesai, daripada membiarkan tombol Putar tanpa guna:
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();
}
});Catatan: Gunakan ini untuk "berhenti", bukan untuk "jeda" (untuk jeda, kirim setPlaybackState(false, ...) agar tombol Putar tetap ada). Mengatur navigator.mediaSession.metadata = null tidak menghapus notifikasi. setMediaMetadata / setPlaybackState berikutnya (atau metadata / status navigator.mediaSession yang baru) memunculkan kembali notifikasi dan kontrol layar kunci.
setMediaMetadata jembatan#
window.WebToApk.setMediaMetadata(title: String, artist: String, artworkUrl: String)
Memungkinkan konten web mengendalikan notifikasi bergaya media Android dan menerima callback kontrol transport (putar/jeda/berikutnya/sebelumnya) dari layar kunci dan bayangan notifikasi. Penggunaan dari JavaScript: window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // memutar, posisi ms, durasi ms window.WebToApk.clearMediaNotification() Callback dari Android → JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')
Dijelaskan oleh grupnya, Jembatan Notifikasi Media, bukan sendiri.
Contoh
Mengatur judul, artis, dan gambar sampul notifikasi media serta pemutar layar kunci. Halaman biasanya mengatur navigator.mediaSession.metadata, yang memanggil ini untuk Anda.
Mengembalikan: tidak ada. Memerlukan: aktifkan Notifikasi Media di Langkah 4 (Akses) saat Anda membuat build. Tanpanya panggilan ini tidak melakukan apa-apa.
Cara publik (gunakan ini):
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' }]
});
}Panggilan mentah. Argumen: title, artist, artworkUrl (gunakan '' untuk tanpa gambar):
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(); });
}Jaga judul notifikasi tetap terbaru untuk siaran radio langsung, di mana lagu berganti tanpa halaman memuat berkas baru:
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 });Catatan:
- Notifikasi muncul (atau diperbarui) dengan panggilan ini. Judul kosong menampilkan nama aplikasi.
artworkUrlharus berupa alamathttps://lengkap di internet. Aplikasi mengunduhnya (batas 4 detik); jika gagal, ikon aplikasi ditampilkan.- Terima penekanan tombol dengan
navigator.mediaSession.setActionHandler(lihatnavigator.mediaSession), atau denganwindow.WebToApkOnMediaAction = function (action) { ... }Anda sendiri (play,pause,next,prev,stop) - gunakan salah satu dari keduanya. SetelahclearMediaNotification(), panggilan berikutnya ke ini (atausetPlaybackState) memunculkan kembali notifikasi dan kontrol layar kunci.
setPlaybackState jembatan#
window.WebToApk.setPlaybackState(isPlaying: Boolean, positionMs: Long, durationMs: Long)
Memungkinkan konten web mengendalikan notifikasi bergaya media Android dan menerima callback kontrol transport (putar/jeda/berikutnya/sebelumnya) dari layar kunci dan bayangan notifikasi. Penggunaan dari JavaScript: window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // memutar, posisi ms, durasi ms window.WebToApk.clearMediaNotification() Callback dari Android → JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')
Dijelaskan oleh grupnya, Jembatan Notifikasi Media, bukan sendiri.
Contoh
Memberi tahu notifikasi media apakah Anda sedang memutar atau dijeda, dan di mana posisi Anda dalam trek. Halaman biasanya mengatur navigator.mediaSession.playbackState dan memanggil setPositionState, yang memanggil ini untuk Anda.
Mengembalikan: tidak ada. Memerlukan: aktifkan Notifikasi Media di Langkah 4 (Akses) saat Anda membuat build. Tanpanya panggilan ini tidak melakukan apa-apa.
Cara publik (gunakan ini):
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);Panggilan mentah. Argumen: isPlaying (true/false), positionMs, durationMs (milidetik bulat):
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)
);
}Laporkan posisi baru setelah pengguna melakukan seek, dan alihkan notifikasi ke Putar saat trek berakhir:
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 awayCatatan: true menampilkan tombol Jeda dan mencegah notifikasi digeser hilang; false menampilkan Putar. Panggil setMediaMetadata lebih dulu, agar notifikasi punya judul. Untuk menghapus notifikasi saat pengguna berhenti, panggil clearMediaNotification().