Esta guía documenta la implementación completa del modo Picture-in-Picture (PIP) en el SDK de Mediastream para Android. El PIP permite que los usuarios continúen viendo contenido de video en una ventana flotante mientras navegan por otras aplicaciones, mejorando significativamente la experiencia de usuario y la multitarea.
Picture-in-Picture es una característica de Android (API 26+) que permite reproducir video en una pequeña ventana flotante superpuesta sobre otras aplicaciones. El usuario puede:
- Ver contenido mientras usa otras apps
- Mover y redimensionar la ventana PIP
- Acceder a controles básicos de reproducción
- Cerrar o expandir el video cuando lo desee
- Mínimo: Android 8.0 (API 26 / Oreo)
- Anotación requerida:
@RequiresApi(Build.VERSION_CODES.O)
No se requieren permisos especiales en el AndroidManifest.xml, solo la declaración de soporte en la actividad.
MediastreamPlayer
├── pipHandler: MediastreamPlayerPip
├── startPiP()
└── onPictureInPictureModeChanged()
MediastreamPlayerPip
├── enterPictureInPictureMode()
├── updatePipParams()
└── onPictureInPictureModeChanged()
Activity (VideoOnDemandActivity)
├── onUserLeaveHint()
└── onPictureInPictureModeChanged()
┌─────────────────────────────────────────┐
│ Usuario reproduce video VOD │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Usuario presiona botón HOME │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ onUserLeaveHint() se ejecuta │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ player?.startPiP() verifica config │
└──────────────┬──────────────────────────┘
│
┌─────┴─────┐
│ │
▼ ▼
HABILITADO DESHABILITADO
│ │
│ └──> Sale de la app
│
▼
┌─────────────────────────────────────────┐
│ pipHandler.enterPictureInPictureMode() │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Video se muestra en ventana 16:9 │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ onPictureInPictureModeChanged(true) │
│ - Oculta controles innecesarios │
│ - Ajusta UI para modo compacto │
└─────────────────────────────────────────┘
La actividad que soportará PIP debe declararlo explícitamente:
<activity
android:name=".video.VideoOnDemandActivity"
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:exported="false" />Atributos clave:
android:supportsPictureInPicture="true": Habilita el soporte PIP para esta actividadandroid:configChanges: Evita que la actividad se destruya y recree cuando cambia el tamaño o la orientaciónscreenSize: Cambios de tamaño de pantallasmallestScreenSize: Cambios en el tamaño mínimoscreenLayout: Cambios en el layout de pantallaorientation: Cambios de orientación
@RequiresApi(Build.VERSION_CODES.O)
override fun onUserLeaveHint() {
player?.startPiP()
}¿Qué hace onUserLeaveHint()?
Este método del ciclo de vida de Android se invoca cuando el usuario está a punto de dejar la actividad de forma voluntaria, típicamente al:
- Presionar el botón HOME
- Cambiar a otra aplicación desde el selector de apps recientes
- Abrir una notificación
No se invoca cuando:
- Se abre un diálogo
- Llega una llamada telefónica
- Se abre otra actividad de la misma app
@RequiresApi(Build.VERSION_CODES.O)
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
player?.onPictureInPictureModeChanged(isInPictureInPictureMode)
}Parámetros:
isInPictureInPictureMode:truecuando entra en PIP,falsecuando salenewConfig: Nueva configuración del sistema (tamaño de pantalla, orientación, etc.)
Usos típicos:
- Ocultar/mostrar controles según el modo
- Ajustar el layout de la UI
- Pausar/reanudar funcionalidades no esenciales
- Actualizar la visualización de subtítulos
fun startPiP() {
val shouldEnterPiP = when (msConfig?.pip) {
MediastreamPlayerConfig.FlagStatus.ENABLE -> true
MediastreamPlayerConfig.FlagStatus.DISABLE -> false
MediastreamPlayerConfig.FlagStatus.NONE -> mediaInfo?.player?.pip == true
else -> false
}
if (shouldEnterPiP) {
pipHandler?.enterPictureInPictureMode()
}
}Sistema de Prioridades:
-
Configuración Local (Máxima prioridad)
config.pip = MediastreamPlayerConfig.FlagStatus.ENABLE // Fuerza habilitado config.pip = MediastreamPlayerConfig.FlagStatus.DISABLE // Fuerza deshabilitado
-
Configuración desde API (Prioridad media)
config.pip = MediastreamPlayerConfig.FlagStatus.NONE // Se usa el valor de: mediaInfo?.player?.pip
-
Valor por defecto (Prioridad baja)
// Si no hay configuración: false (deshabilitado)
fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
pipHandler?.onPictureInPictureModeChanged(isInPictureInPictureMode)
}Delega el manejo al MediastreamPlayerPip para mantener la separación de responsabilidades.
Esta clase encapsula toda la lógica específica de PIP:
class MediastreamPlayerPip(private val activity: Activity) {
var TAGDEBUG = "MP-Debug"
@RequiresApi(Build.VERSION_CODES.O)
fun enterPictureInPictureMode() {
val pipParams = PictureInPictureParams.Builder()
.setAspectRatio(Rational(16, 9))
.build()
activity.enterPictureInPictureMode(pipParams)
}
@RequiresApi(Build.VERSION_CODES.O)
fun updatePipParams(): PictureInPictureParams {
val aspectRadio = Rational(16, 9)
return PictureInPictureParams.Builder()
.setAspectRatio(aspectRadio)
.build()
}
fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
if (isInPictureInPictureMode) {
Log.d(TAGDEBUG, "Entró en modo PIP")
} else {
Log.d(TAGDEBUG, "Salió de modo PIP")
}
}
}Aspect Ratio (Relación de Aspecto):
.setAspectRatio(Rational(16, 9))- Define las proporciones de la ventana PIP
16:9es el estándar para video widescreen- Puede ser
4:3para contenido clásico - Rango válido: entre
0.4y2.39aproximadamente
Otros parámetros disponibles:
PictureInPictureParams.Builder()
.setAspectRatio(Rational(16, 9))
.setSourceRectHint(sourceRectHint) // Área de origen para animación
.setActions(actions) // Acciones personalizadas (play, pause, etc.)
.setAutoEnterEnabled(true) // Auto-entrar en PIP (API 31+)
.setSeamlessResizeEnabled(true) // Redimensionamiento suave (API 31+)
.build()val config = MediastreamPlayerConfig()
config.id = "video_id_here"
config.type = MediastreamPlayerConfig.VideoTypes.VOD
config.pip = MediastreamPlayerConfig.FlagStatus.ENABLE // Habilitar PIP
player = MediastreamPlayer(this, config, container, playerView, supportFragmentManager)config.pip = MediastreamPlayerConfig.FlagStatus.DISABLE // Deshabilitar PIPconfig.pip = MediastreamPlayerConfig.FlagStatus.NONE
// El SDK usará el valor que venga del endpoint de configuraciónEscenario: Usuario reproduce un video bajo demanda y presiona el botón HOME para revisar un mensaje.
Flujo:
- Usuario carga video VOD en
VideoOnDemandActivity - Video se reproduce normalmente
- Usuario presiona HOME
onUserLeaveHint()detecta la salidaplayer?.startPiP()verifica configuración- PIP está habilitado → entra en modo PIP
- Video continúa en ventana flotante
- Usuario puede revisar mensajes mientras mira
Resultado: ✅ Experiencia sin interrupciones
Escenario: Contenido premium que no debe reproducirse en PIP por políticas de licenciamiento.
Flujo:
- Configuración:
config.pip = FlagStatus.DISABLE - Usuario presiona HOME
startPiP()verifica configuración- PIP está deshabilitado → no activa PIP
- Video se pausa y app va al background
Resultado: ✅ Respeta políticas de contenido
Escenario: Control centralizado de PIP desde el backend.
Flujo:
- Configuración:
config.pip = FlagStatus.NONE - SDK consulta API de configuración
- API responde:
mediaInfo.player.pip = true - Usuario presiona HOME
- Se activa PIP según valor de API
Resultado: ✅ Control remoto de funcionalidades
Escenario: Usuario quiere volver a ver el video en pantalla completa.
Flujo:
- Video en modo PIP
- Usuario toca la ventana PIP
- Activity se restaura automáticamente
onPictureInPictureModeChanged(false)se ejecuta- Controles completos se muestran nuevamente
- Video continúa desde la misma posición
Resultado: ✅ Transición fluida entre modos
✅ Recomendado:
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (isInPictureInPictureMode) {
// Ocultar UI no esencial
hideControls()
hideTitle()
hideSubtitles() // O ajustar tamaño
} else {
// Restaurar UI completa
showControls()
showTitle()
showSubtitles()
}
player?.onPictureInPictureModeChanged(isInPictureInPictureMode)
}❌ No Recomendado:
// No asumir que siempre se debe entrar en PIP
override fun onUserLeaveHint() {
enterPictureInPictureMode() // Ignora configuración
}@RequiresApi(Build.VERSION_CODES.O)
private fun isPipSupported(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
} else {
false
}
}@RequiresApi(Build.VERSION_CODES.O)
override fun onUserLeaveHint() {
try {
if (isPipSupported()) {
player?.startPiP()
}
} catch (e: Exception) {
Log.e(TAG, "Error al entrar en modo PIP: ${e.message}")
// Continuar con comportamiento normal
}
}En modo PIP:
- ❌ No mostrar diálogos (no son visibles)
- ❌ No mostrar notificaciones intrusivas
- ✅ Mantener controles básicos (play/pause)
- ✅ Continuar reproducción sin interrupciones
Al salir de PIP:
- ✅ Restaurar estado completo de UI
- ✅ Sincronizar posición de reproducción
- ✅ Reactivar funcionalidades pausadas
Pasos:
- Iniciar reproducción de video VOD
- Presionar botón HOME
- Verificar que entra en modo PIP
- Verificar que video continúa reproduciéndose
Resultado esperado: ✅ Video se muestra en ventana flotante 16:9 ✅ Reproducción continúa sin interrupciones
Pasos:
- Estar en modo PIP
- Tocar la ventana PIP
- Verificar que vuelve a pantalla completa
Resultado esperado: ✅ Activity se restaura completamente ✅ Controles completos visibles ✅ Posición de reproducción se mantiene
Pasos:
- Configurar
config.pip = FlagStatus.DISABLE - Presionar HOME durante reproducción
- Verificar comportamiento
Resultado esperado: ✅ No entra en modo PIP ✅ Video se pausa ✅ Activity va al background normal
Pasos:
- Configurar
config.pip = FlagStatus.NONE - Simular respuesta API con
pip = true - Presionar HOME
Resultado esperado: ✅ Entra en modo PIP según valor de API
Pasos:
- Entrar en modo PIP
- Rotar dispositivo
- Verificar ventana PIP
Resultado esperado: ✅ Ventana PIP se adapta a nueva orientación ✅ Reproducción continúa sin problemas
- Tiempo de transición: < 300ms al entrar/salir de PIP
- Continuidad de reproducción: 0 interrupciones durante transición
- Sincronización de estado: 100% de precisión en posición de reproducción
- Estabilidad de UI: Sin crashes durante cambios de modo
- Compatibilidad: Funciona en 100% de dispositivos API 26+
// En MediastreamPlayerPip
Log.d("MP-Debug", "Entrando en modo PIP")
Log.d("MP-Debug", "PIP activado: isInPictureInPictureMode = $isInPictureInPictureMode")
// En MediastreamPlayer
Log.d("MP-Debug", "startPiP() - shouldEnterPiP = $shouldEnterPiP")
Log.d("MP-Debug", "Config local pip = ${msConfig?.pip}")
Log.d("MP-Debug", "API pip = ${mediaInfo?.player?.pip}")
// En Activity
Log.d("VideoOnDemand", "onUserLeaveHint() llamado")
Log.d("VideoOnDemand", "PIP mode changed: $isInPictureInPictureMode")# Forzar entrada en PIP
adb shell am broadcast -a android.intent.action.ENTER_PICTURE_IN_PICTURE
# Verificar si PIP está soportado
adb shell pm list features | grep "feature:android.software.picture_in_picture"
# Simular presión de botón HOME
adb shell input keyevent KEYCODE_HOME
# Ver logs relacionados con PIP
adb logcat | grep -i "pip\|picture"Síntomas:
- Presionar HOME no activa PIP
- Video se pausa en lugar de entrar en PIP
Causas posibles:
supportsPictureInPictureno está en el manifestconfig.pip = FlagStatus.DISABLE- Versión de Android < 8.0
- Dispositivo no soporta PIP
Solución:
// Verificar soporte antes de intentar
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
player?.startPiP()
} else {
Log.w(TAG, "PIP no soportado en este dispositivo")
}
}Síntomas:
- Ventana PIP muy pequeña o muy grande
- Aspect ratio incorrecto
Solución:
// Asegurar aspect ratio correcto
val aspectRatio = Rational(16, 9)
val params = PictureInPictureParams.Builder()
.setAspectRatio(aspectRatio)
.build()
activity.setPictureInPictureParams(params)Síntomas:
- Controles no se ocultan/muestran correctamente
- Layout incorrecto después de salir de PIP
Solución:
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
// Forzar actualización de UI
window.decorView.requestLayout()
player?.onPictureInPictureModeChanged(isInPictureInPictureMode)
}// API 26+: Agregar botones de acción
val actions = arrayListOf(
// Play/Pause
RemoteAction(
Icon.createWithResource(this, R.drawable.ic_pause),
"Pausar",
"Pausar reproducción",
pausePendingIntent
),
// Siguiente
RemoteAction(
Icon.createWithResource(this, R.drawable.ic_next),
"Siguiente",
"Siguiente episodio",
nextPendingIntent
)
)
val params = PictureInPictureParams.Builder()
.setActions(actions)
.build()// API 31+: Entrar automáticamente en PIP sin esperar onUserLeaveHint
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val params = PictureInPictureParams.Builder()
.setAutoEnterEnabled(true)
.build()
setPictureInPictureParams(params)
}// API 31+: Permitir expansión sin salir de PIP
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val params = PictureInPictureParams.Builder()
.setExpandedAspectRatio(Rational(1, 1))
.build()
}fun trackPipUsage() {
analytics.logEvent("pip_entered", Bundle().apply {
putString("content_id", currentVideoId)
putLong("position_ms", player?.currentPosition ?: 0)
putString("content_type", "vod")
})
}mediastreamplatformsdkandroid/src/main/java/am/mediastre/mediastreamplatformsdkandroid/MediastreamPlayerPip.ktmediastreamplatformsdkandroid/src/main/java/am/mediastre/mediastreamplatformsdkandroid/MediastreamPlayer.ktapp/src/main/java/am/mediastre/mediastreamsampleapp/video/VideoOnDemandActivity.ktapp/src/main/AndroidManifest.xml
La implementación de PIP en el SDK de Mediastream proporciona:
✅ Experiencia de usuario mejorada con reproducción continua en multitarea
✅ Flexibilidad de configuración con prioridad local sobre API
✅ Implementación robusta con manejo adecuado de estados
✅ Fácil integración para desarrolladores que usan el SDK
✅ Compatibilidad con Android 8.0+
El equipo de desarrollo debe enfocarse en:
- Validar todos los casos de prueba documentados
- Monitorear analytics de uso de PIP
- Considerar implementar controles personalizados (API 26+)
- Evaluar auto-entrada en PIP para dispositivos Android 12+ (API 31+)
Documento preparado para el equipo de desarrollo
Fecha: Febrero 2026
Versión: 1.0
Estado: Documentado e implementado