Esta es la plantilla que más importa en InteeBuild: radio / podcast / streaming que sigue sonando con la app minimizada o la pantalla apagada.
Desde esta versión las plantillas Radio y Streaming activan nativeAudio: el audio lo reproduce RadioService.java con MediaPlayer + MediaSession en Java, y tu HTML es solo la cara con botones. Así no importa lo que haga el WebView: el sonido es nativo y no se corta.
Cómo funciona:
- En el Studio, tarjeta
Audio 100% nativo: marcaReproducir audio en Java nativoy pega la URL de tu stream (ejhttps://tu-servidor.com:8000/stream). - Las plantillas Radio/Streaming ya lo traen marcado (Radio con auto-play).
- En tu HTML usa el puente (con fallback web si se abre en navegador):
<button onclick="nPlay()">▶ Play nativo</button>
<button onclick="nPause()">⏸ Pausa</button>
<script>
function nPlay(){
var u = 'https://tu-servidor.com:8000/stream';
if (window.InteeAudio) InteeAudio.play(u); // Java nativo
else { var a = document.getElementById('p'); a.src = u; a.load(); a.play(); }
}
function nPause(){
if (window.InteeAudio) InteeAudio.pause();
}
</script>Qué genera (verificable en el ZIP): RadioService.java con MediaPlayer + Icy-MetaData: 0 (mp3 limpio de Shoutcast) + MediaSession con Play/Pausa en notificación y pantalla de bloqueo, AudioBridge.java (window.InteeAudio), patch que lo inyecta en MainActivity, y la URL horneada como constante (más lo que pases por JS).
Cara lista para pegar: radio-face.html — el diseño Tailwind completo ya adaptado a InteeAudio con fallback web. Cópialo al editor HTML, cambia STREAM_URL y compila.
Prueba: abre la app → debe aparecer la notificación con botón Play/Pausa aunque no hayas tocado nada (si hay auto-play) o al primer tap. Minimiza, apaga pantalla: sigue. Los botones de la notificación y del lock-screen controlan el stream.
Marca en el Studio: Servicio Foreground + Pantalla Encendida (WAKE_LOCK) + Notificaciones.
POST /api/project o POST /api/build con (o elige la plantilla Radio en el Studio, que ya trae todo esto):
{
"appName": "Mi Radio",
"url": "https://mi-radio.com",
"template": "radio",
"permissions": { "foreground": true, "wakeLock": true, "notifications": true },
"nativeAudio": true,
"nativeAutoplay": true,
"streamUrl": "https://tu-servidor.com:8000/stream"
}Sin
streamUrlel readiness bloquea el build (canBuild: false) con el aviso "Audio nativo activo pero sin URL del stream". Es a propósito: sin URL no hay nada que suene en nativo.
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<service android:name=".RadioService" android:exported="false" android:foregroundServiceType="dataSync|mediaPlayback" />Compruébalo en el ZIP: main-manifest.xml debe contener RadioService y FOREGROUND_SERVICE_MEDIA_PLAYBACK. Si no está, el audit marca fail y el build se bloquea.
Archivo RadioService.java en el ZIP, paquete = tu packageName:
- Crea canal
inteebuild_radio(IMPORTANCE_LOW). startForeground(1, notificación)con iconoic_media_play.onStartCommand → START_STICKY(el sistema lo revive).
patch-main-activity.js inyecta en MainActivity.java:
startForegroundService(new Intent(this, RadioService.class));En el log del workflow debes ver:
--- servicio instalado ---
1
Si ves 0, el patch no se aplicó: revisa build-config.json → permissions.foreground: true.
Modo nativo (recomendado): tu HTML solo tiene botones que llaman a window.InteeAudio. El sonido lo reproduce Java, así que estas reglas no aplican: puedes minimizar y apagar sin miedo. Usa radio-face.html tal cual.
Modo web fallback (si abres la cara en un navegador, o compilas sin nativeAudio): ahí sí el <audio> lo controla tu página y debe cumplir esto o se corta igual:
<audio id="player" src="https://mi-radio.com/stream.mp3" preload="none"></audio>
<script>
const a = document.getElementById('player');
// NO pausar en visibilitychange / pagehide
document.addEventListener('visibilitychange', () => {
// intencionalmente vacío: el audio sigue
});
// Play debe venir de un gesto del usuario (autoplay bloqueado en WebView)
document.getElementById('play').onclick = () => a.play();
</script>Reglas:
srcenhttps://(nohttp://siuseCleartextes false).- No llames
audio.pause()envisibilitychange,pagehideniblur. - El primer
play()siempre desde un tap/click. - Si usas HLS (
.m3u8), verifica que tu servidor envíe CORS correcto.
{ "appName": "Minima", "inputType": "html", "htmlCode": "<!DOCTYPE html><html><body><h1>Hola</h1></body></html>" }Esperado en ZIP:
main-manifest.xmlsoloINTERNET,ACCESS_NETWORK_STATE,ACCESS_WIFI_STATE.- Sin
NativePermissions.java, sinRadioService.java. - Audit:
total 0, readiness 100.
Instala, abre, cierra: no debe pedir ningún permiso. Si pide algo, el Engine está metiendo de más.
Activa la plantilla Radio + URL en Audio nativo (o el JSON de arriba). Esperado:
- Manifest con las 6 líneas de arriba +
RadioService. RadioService.java(conMediaPlayer) +AudioBridge.java+patch-main-activity.js+patch-audio.jsen el ZIP.build-config.jsonconnativeAudio: trueyhasStreamUrl: true.- Audit:
foreground ok (GENERATED),wakeLock ok,notifications ok, todoverified: true. - Readiness 100%. Sin
streamUrl:canBuild: falsecon aviso.
Instala en Android real:
- Abre la app → debe aparecer notificación con botón Play aunque no hayas tocado nada (auto-play). Si no hay notificación, no instales más: el servicio no arrancó.
- Dale play en tu cara (o en la notificación) → minimiza → el audio sigue.
- Apaga pantalla 30s → el audio sigue (WAKE_LOCK +
PARTIAL_WAKE_LOCKdel player). - La notificación no se puede descartar (
setOngoing(true)); sus botones y los del lock-screen controlan el stream (MediaSession).
En GitHub Actions → job compile:
--- permisos aplicados ---
android:name="android.permission.FOREGROUND_SERVICE"
--- permisos nativos instalados ---
--- catalog patches aplicados ---
--- servicio instalado ---
1
--- audio nativo instalado ---
1
manifest XML OK
Nota: el servicio usa solo APIs del framework (
Notification.MediaStyle,MediaPlayer,MediaSession). No requiere dependencias androidx extras: si vespackage androidx.media.app ... does not exist, regenera con esta versión.
Si grep -c RadioService da 0, el servicio no se inyectó: no instales ese APK, revisa packageName y build-config.json. Si grep -c InteeAudio da 0, el puente JS no se inyectó: tus botones no sonarán en nativo (caerán al fallback web).
Los streams Shoutcast/Icecast se caen solos cada cierto tiempo. Sin reconexión, el <audio> queda en silencio para siempre y parece que "la app lo cortó". Agrega esto:
<script>
const a = document.getElementById('player');
let wantPlay = false;
document.getElementById('play').onclick = () => {
if (a.paused) { wantPlay = true; a.load(); a.play().catch(() => {}); }
else { wantPlay = false; a.pause(); }
};
// Si el servidor corta el stream, reintenta solo
['error', 'stalled', 'suspend'].forEach(ev => a.addEventListener(ev, () => {
if (!wantPlay) return;
setTimeout(() => { a.load(); a.play().catch(() => {}); }, 3000);
}));
a.addEventListener('ended', () => {
if (!wantPlay) return;
a.load(); a.play().catch(() => {});
});
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('play', () => { wantPlay = true; a.load(); a.play().catch(() => {}); });
navigator.mediaSession.setActionHandler('pause', () => { wantPlay = false; a.pause(); });
}
</script>| Síntoma | Causa | Fix |
|---|---|---|
| El audio se corta al apagar pantalla | Falta WAKE_LOCK o el HTML pausa en visibilitychange |
Marca Wake Lock y quita pause() |
| No aparece notificación | RadioService no arrancó |
Revisa log servicio instalado, debe ser 1 |
| Play rechaza el AAB | ACCESS_BACKGROUND_LOCATION sin justificación |
No marques ubicación en segundo plano para una radio |
| Se corta a los minutos (con notificación visible) | Optimización de batería / OEM agresivo (Xiaomi, Huawei, Samsung) | Ajustes → Apps → tu app → Batería → Sin restricciones + permitir actividad en segundo plano |
| Se corta al minimizar (sin notificación) | El build no trae Foreground (plantilla Radio no aplicada) | Recompila con foreground + wakeLock + notifications y verifica servicio instalado: 1 en el log |
onPermissionRequest concede todo |
Patch viejo (grant-all) | Regenera: el patch actual usa wants()+hasPerm() y deny() por defecto |
- Plantilla Radio aplicada +
streamUrlcon tu servidor en Audio nativo - Manifest tiene
FOREGROUND_SERVICE_MEDIA_PLAYBACKyRadioService - ZIP trae
RadioService.java(conMediaPlayer),AudioBridge.java,patch-audio.js - Audit
foreground/notifications/wakeLockenokyverified: true, readiness 100% - Cara usa
window.InteeAudio.play(url)(ver radio-face.html) - Stream en
https://con certificado válido - Notificación con Play visible al abrir la app
- Probado minimizado + pantalla apagada 30s