Los servicios de reproducción permiten que el contenido de audio (y video en segundo plano) continúe reproduciéndose incluso cuando la app no está visible, similar a Spotify, YouTube Music o cualquier app de audio profesional. El SDK de Mediastream proporciona dos servicios especializados:
- MediastreamPlayerService: Servicio básico con notificaciones y controles multimedia
- MediastreamPlayerServiceWithSync: Servicio avanzado con MediaSession, Android Auto y sincronización completa
Estos servicios son esenciales para apps de radio, podcasts, música, audiolibros y cualquier contenido que el usuario desee escuchar mientras usa otras aplicaciones.
Usuario escucha audio
↓
Presiona HOME o cambia de app
↓
Activity pasa a background
↓
Sistema destruye Activity (después de tiempo/memoria)
↓
❌ Audio se detiene
Usuario escucha audio
↓
Presiona HOME o cambia de app
↓
Activity pasa a background
↓
Servicio continúa en foreground
↓
✅ Audio sigue reproduciéndose
✅ Notificación con controles
✅ Integración con sistema (lock screen, bluetooth)
✅ Reproducción en background continua
✅ Notificación multimedia con controles (play/pause/next/prev)
✅ MediaSession para integración con sistema
✅ Artwork dinámico en la notificación
✅ Control desde lock screen
✅ Control desde auriculares Bluetooth
✅ Actualización dinámica de metadatos (título, artista, imagen)
✅ Next Episode compatible
✅ Todo lo del servicio básico
✅ MediaLibraryService para apps de audio profesionales
✅ Android Auto soporte completo
✅ Android TV/Google TV integración
✅ Sincronización de estado entre UI y servicio
✅ Navegación por contenido (browse/search)
✅ Comandos personalizados del sistema
✅ EventBus para comunicación bidireccional
Activity
↓
initializeService() (configuración)
↓
startForegroundService()
↓
MediastreamPlayerService
├── onCreate()
│ ├── MediastreamPlayer (interno)
│ ├── MediaSession
│ └── PlayerNotificationManager
├── Notificación en foreground
│ ├── Artwork (Glide)
│ ├── Título/Descripción
│ └── Controles (play/pause/next/prev)
└── Callbacks a la Activity
Activity
↓
initializeService() + MediaController.Builder
↓
MediastreamPlayerServiceWithSync (MediaLibraryService)
├── MediaLibrarySession
│ ├── Callback personalizado
│ ├── Comandos personalizados
│ └── Android Auto integration
├── MediastreamPlayer (interno)
├── PlayerNotificationManager
├── EventBus (comunicación)
└── Android Auto Library Browser
├── Live streams
├── Podcasts
└── Episodes
<manifest>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application>
<!-- Servicio básico -->
<service
android:name="am.mediastre.mediastreamplatformsdkandroid.MediastreamPlayerService"
android:foregroundServiceType="mediaPlayback" />
</application>
</manifest>Permisos requeridos:
FOREGROUND_SERVICE: Permite servicios en primer planoFOREGROUND_SERVICE_MEDIA_PLAYBACK: Específico para reproducción de medios (Android 10+)WAKE_LOCK: Mantiene el dispositivo activo durante reproducción
<!-- activity_audioplayer.xml -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- Contenedor principal -->
<FrameLayout
android:id="@+id/main_media_frame"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- Contenedor del player (puede estar oculto) -->
<FrameLayout
android:id="@+id/playerContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone" />
</LinearLayout>class LiveAudioAsServiceActivity : AppCompatActivity() {
private val TAG = "AudioService"
private lateinit var container: FrameLayout
private lateinit var playerContainer: FrameLayout
private lateinit var miniPlayerConfig: MediastreamMiniPlayerConfig
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_audioplayer)
// 1. Referencias a los contenedores
container = findViewById(R.id.main_media_frame)
playerContainer = findViewById(R.id.playerContainer)
// 2. Configurar el player
val config = MediastreamPlayerConfig().apply {
id = "632c9b89aa9ace684913b815"
accountID = "6271a4d5d206c3172f3c9a9c"
type = MediastreamPlayerConfig.VideoTypes.LIVE
playerType = MediastreamPlayerConfig.PlayerType.AUDIO
environment = MediastreamPlayerConfig.Environment.PRODUCTION
isDebug = true
trackEnable = false
showControls = true
appName = "MyAudioApp"
// Configuración de notificación
notificationSongName = "Radio en Vivo"
notificationDescription = "Escuchando ahora"
notificationImageUrl = "https://example.com/logo.png"
notificationHasNext = true
notificationHasPrevious = false
}
// 3. Iniciar el servicio
startService(config)
}
private fun startService(config: MediastreamPlayerConfig) {
// Configuración del mini player (notificación)
miniPlayerConfig = MediastreamMiniPlayerConfig().apply {
songName = "Nombre de la canción"
description = "Artista o descripción"
imageUrl = "https://example.com/cover.jpg"
albumName = "Nombre del álbum"
color = android.graphics.Color.parseColor("#1DB954") // Verde tipo Spotify
setStateNext = true // Habilitar botón Next
setStatePrev = false // Deshabilitar botón Previous
}
// Callback para eventos del player
val playerCallback = object : MediastreamPlayerCallback {
override fun playerViewReady(msplayerView: PlayerView?) {
Log.d(TAG, "Player listo")
}
override fun onPlay() {
Log.d(TAG, "Reproduciendo")
}
override fun onPause() {
Log.d(TAG, "Pausado")
}
override fun onReady() {
Log.d(TAG, "Contenido listo")
}
override fun onEnd() {
Log.d(TAG, "Reproducción finalizada")
}
override fun onPlayerClosed() {
Log.d(TAG, "Player cerrado")
finish()
}
override fun onError(error: String?) {
Log.e(TAG, "Error: $error")
Toast.makeText(this@LiveAudioAsServiceActivity, error, Toast.LENGTH_LONG).show()
}
override fun onNext() {
Log.d(TAG, "Next presionado")
// Implementar lógica para siguiente canción
}
override fun onPrevious() {
Log.d(TAG, "Previous presionado")
// Implementar lógica para canción anterior
}
override fun onLiveAudioCurrentSongChanged(data: JSONObject?) {
// Actualizar notificación con nueva canción
data?.let {
val songName = it.optString("song", "")
val artist = it.optString("artist", "")
val poster = it.optString("poster", "")
Log.d(TAG, "Nueva canción: $songName - $artist")
// Actualizar mini player
miniPlayerConfig.songName = songName
miniPlayerConfig.description = artist
miniPlayerConfig.imageUrl = poster
}
}
// ... otros callbacks
}
// Inicializar el servicio con la configuración
MediastreamPlayerService.initializeService(
context = this,
activity = this,
config = config,
container = container,
playerContainer = playerContainer,
miniPlayerConfig = miniPlayerConfig,
trackEnable = false,
accountId = config.accountID ?: "",
playerCallback = playerCallback
)
// Iniciar servicio en foreground
val intent = Intent(this, MediastreamPlayerService::class.java)
intent.action = "$packageName.action.startforeground"
try {
ContextCompat.startForegroundService(this, intent)
Log.d(TAG, "Servicio iniciado correctamente")
} catch (e: Exception) {
Log.e(TAG, "Error al iniciar servicio: ${e.message}")
}
}
override fun onBackPressed() {
// Detener el servicio al salir
try {
val stopIntent = Intent(this, MediastreamPlayerService::class.java)
stopService(stopIntent)
} catch (e: Exception) {
Log.e(TAG, "Error al detener servicio: ${e.message}")
}
super.onBackPressed()
}
}miniPlayerConfig.apply {
// Metadatos
songName = "Título de la Canción"
description = "Nombre del Artista"
albumName = "Nombre del Álbum"
imageUrl = "https://cdn.example.com/album-cover.jpg"
// Colores (opcional)
color = Color.parseColor("#FF5722") // Color de acento
imageIconUrl = R.drawable.custom_notification_icon // Icono personalizado
// Botones de navegación
setStateNext = true // Mostrar botón "Siguiente"
setStatePrev = true // Mostrar botón "Anterior"
}
// Configuración adicional en MediastreamPlayerConfig
config.apply {
notificationSongName = "Override del título"
notificationDescription = "Override de la descripción"
notificationImageUrl = "Override de la imagen"
notificationColor = Color.parseColor("#FF5722")
notificationHasNext = true
notificationHasPrevious = true
notificationIconUrl = R.drawable.ic_notification
// Actualización automática para audio en vivo
fillAutomaticallyAudioNotification = true
}// Método 1: Callback onLiveAudioCurrentSongChanged
override fun onLiveAudioCurrentSongChanged(data: JSONObject?) {
data?.let {
miniPlayerConfig.songName = it.optString("song", "")
miniPlayerConfig.description = it.optString("artist", "")
miniPlayerConfig.imageUrl = it.optString("poster", "")
// El servicio actualizará automáticamente la notificación
}
}
// Método 2: Actualización manual
val service = MediastreamPlayerService.getMsPlayer()
service?.let {
// Actualizar configuración
val newMiniConfig = MediastreamMiniPlayerConfig().apply {
songName = "Nueva Canción"
description = "Nuevo Artista"
imageUrl = "nueva-url.jpg"
}
// Forzar actualización
// (requiere acceso al servicio)
}// Desde cualquier parte de la app
val player = MediastreamPlayerService.getMsPlayer()
player?.let {
// Controlar reproducción
it.play()
it.pause()
it.seekTo(30000) // Seek a 30 segundos
// Obtener información
val isPlaying = it.isPlaying()
val currentPosition = it.getCurrentPosition()
val duration = it.getDuration()
// Cambiar velocidad
it.changeSpeed(1.5f) // 1.5x
// Cargar nuevo contenido
val newConfig = MediastreamPlayerConfig().apply {
id = "nuevo_audio_id"
type = MediastreamPlayerConfig.VideoTypes.VOD
}
it.reloadPlayer(newConfig)
}// El servicio respeta el volumen del sistema
// Los controles de volumen del dispositivo afectarán el audio<manifest>
<!-- Permisos -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<!-- Servicio con sincronización -->
<service
android:name="am.mediastre.mediastreamplatformsdkandroid.MediastreamPlayerServiceWithSync"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
<action android:name="androidx.media3.session.MediaLibraryService" />
<action android:name="android.media.browse.MediaBrowserService" />
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
</intent-filter>
</service>
<!-- Metadata para Android Auto (opcional) -->
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
</application>
</manifest>override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Solicitar permiso de notificaciones en Android 13+
if (Build.VERSION.SDK_INT >= 33 &&
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED
) {
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
REQUEST_CODE_NOTIFICATIONS
)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_NOTIFICATIONS) {
if (grantResults.isEmpty() ||
grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(
this,
"Permiso de notificaciones denegado",
Toast.LENGTH_LONG
).show()
}
}
}class AudioWithSyncServiceActivity : AppCompatActivity() {
private val TAG = "AudioSyncService"
private lateinit var container: FrameLayout
private lateinit var playerView: PlayerView
private lateinit var miniPlayerConfig: MediastreamMiniPlayerConfig
// MediaController para sincronización
private lateinit var controllerFuture: ListenableFuture<MediaController>
private val controller: MediaController?
get() = if (controllerFuture.isDone && !controllerFuture.isCancelled) {
controllerFuture.get()
} else null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_audio_sync_service)
container = findViewById(R.id.main_media_frame)
playerView = findViewById(R.id.player_view)
val config = MediastreamPlayerConfig().apply {
id = "646e3d4d5c910108b684a2b0"
accountID = "5fbfd5b96660885379e1a129"
type = MediastreamPlayerConfig.VideoTypes.VOD
playerType = MediastreamPlayerConfig.PlayerType.AUDIO
videoFormat = MediastreamPlayerConfig.AudioVideoFormat.MP3
isDebug = true
trackEnable = false
showControls = true
appName = "MyAudioApp"
}
startService(config)
}
private fun startService(config: MediastreamPlayerConfig) {
miniPlayerConfig = MediastreamMiniPlayerConfig()
val playerCallback = object : MediastreamPlayerCallback {
override fun onPlay() {
Log.d(TAG, "Reproduciendo")
// Actualizar UI si es necesario
}
override fun onPause() {
Log.d(TAG, "Pausado")
}
override fun onPlayerClosed() {
finish()
}
// ... otros callbacks
}
// Inicializar servicio
MediastreamPlayerServiceWithSync.initializeService(
context = this,
activity = this,
config = config,
container = container,
playerContainer = playerView,
miniPlayerConfig = miniPlayerConfig,
trackEnable = false,
accountId = config.accountID ?: "",
playerCallback = playerCallback
)
// Crear MediaController para sincronización
controllerFuture = MediaController.Builder(
this,
SessionToken(
this,
ComponentName(this, MediastreamPlayerServiceWithSync::class.java)
)
).buildAsync()
controllerFuture.addListener(
{ setController(config) },
MoreExecutors.directExecutor()
)
}
private fun setController(config: MediastreamPlayerConfig) {
val controller = this.controller ?: return
// Sincronizar PlayerView con el servicio
playerView.player = controller
playerView.useController = true
// Enviar evento de inicialización (EventBus)
EventBus.getDefault().post(MessageEvent(controller, config))
Log.d(TAG, "Controller sincronizado")
}
override fun onBackPressed() {
super.onBackPressed()
releaseService()
}
private fun releaseService() {
// Liberar controller
playerView.player?.release()
playerView.player = null
if (::controllerFuture.isInitialized) {
MediaController.releaseFuture(controllerFuture)
}
// Detener servicio
val stopIntent = Intent(this, MediastreamPlayerServiceWithSync::class.java)
stopIntent.action = "STOP_SERVICE"
startService(stopIntent)
}
}class AudioWithSyncServiceActivity : AppCompatActivity() {
private lateinit var btnPlayPause: ImageButton
private lateinit var btnNext: Button
private lateinit var btnPrevious: Button
private fun setupControls() {
btnPlayPause = findViewById(R.id.playOrpause)
btnNext = findViewById(R.id.btnNext)
btnPrevious = findViewById(R.id.btnPrevious)
btnPlayPause.setOnClickListener {
MediastreamPlayerServiceWithSync.getMsPlayer()?.let { player ->
if (player.isPlaying()) {
player.pause()
} else {
player.play()
}
}
}
btnNext.setOnClickListener {
val config = MediastreamPlayerConfig().apply {
id = "next_audio_id"
type = MediastreamPlayerConfig.VideoTypes.VOD
playerType = MediastreamPlayerConfig.PlayerType.AUDIO
needReload = true
}
MediastreamPlayerServiceWithSync.getMsPlayer()?.reloadPlayer(config)
}
btnPrevious.setOnClickListener {
val config = MediastreamPlayerConfig().apply {
id = "previous_audio_id"
type = MediastreamPlayerConfig.VideoTypes.VOD
playerType = MediastreamPlayerConfig.PlayerType.AUDIO
needReload = true
}
MediastreamPlayerServiceWithSync.getMsPlayer()?.reloadPlayer(config)
}
}
// Actualizar UI basado en callbacks
private val playerCallback = object : MediastreamPlayerCallback {
override fun onPlay() {
btnPlayPause.setImageResource(android.R.drawable.ic_media_pause)
}
override fun onPause() {
btnPlayPause.setImageResource(android.R.drawable.ic_media_play)
}
override fun onBuffering() {
btnPlayPause.setImageResource(R.drawable.ic_loading)
}
// ... otros callbacks
}
}El servicio MediastreamPlayerServiceWithSync incluye soporte completo para Android Auto.
<!-- res/xml/automotive_app_desc.xml -->
<automotiveApp>
<uses name="media" />
</automotiveApp>Root
├── Live (Radio en vivo)
│ ├── Radio 1
│ ├── Radio 2
│ └── Radio 3
├── Podcasts
│ ├── Podcast A
│ │ ├── Temporada 1
│ │ │ ├── Episodio 1
│ │ │ └── Episodio 2
│ │ └── Temporada 2
│ └── Podcast B
└── ... otros contenidos
El servicio maneja automáticamente:
- Navegación por categorías
- Búsqueda de contenido
- Reproducción desde Android Auto
- Controles en el volante del auto
- Artwork en pantalla del vehículo
Escenario: Estación de radio que transmite 24/7.
class RadioStationActivity : AppCompatActivity() {
private fun setupRadio() {
val config = MediastreamPlayerConfig().apply {
id = "live_radio_stream_id"
accountID = "account_id"
type = MediastreamPlayerConfig.VideoTypes.LIVE
playerType = MediastreamPlayerConfig.PlayerType.AUDIO
isDebug = false
// Configuración de notificación
notificationSongName = "Radio Station FM 99.9"
notificationDescription = "Escuchando en vivo"
notificationImageUrl = "https://example.com/station-logo.png"
notificationHasNext = false // No hay "siguiente" en radio en vivo
notificationHasPrevious = false
// Auto-rellenar con metadata del stream
fillAutomaticallyAudioNotification = true
}
miniPlayerConfig = MediastreamMiniPlayerConfig().apply {
songName = "Radio Station FM 99.9"
description = "En vivo"
imageUrl = "https://example.com/station-logo.png"
color = Color.parseColor("#E91E63")
}
MediastreamPlayerService.initializeService(
this, this, config, container, playerContainer,
miniPlayerConfig, false, config.accountID ?: "",
createRadioCallback()
)
startForegroundService()
}
private fun createRadioCallback() = object : MediastreamPlayerCallback {
override fun onLiveAudioCurrentSongChanged(data: JSONObject?) {
// Actualizar notificación con canción actual
data?.let {
val song = it.optString("song", "")
val artist = it.optString("artist", "")
val poster = it.optString("poster", "")
miniPlayerConfig.songName = song
miniPlayerConfig.description = artist
miniPlayerConfig.imageUrl = poster
// Opcional: Notificar a la UI
updateUI(song, artist, poster)
}
}
override fun onError(error: String?) {
// Intentar reconectar
Handler(Looper.getMainLooper()).postDelayed({
MediastreamPlayerService.getMsPlayer()?.play()
}, 3000)
}
// ... otros callbacks
}
}Escenario: App que reproduce podcasts con navegación entre episodios.
class PodcastPlayerActivity : AppCompatActivity() {
private val episodes = listOf(
Episode("ep1", "Episodio 1", "Intro al tema"),
Episode("ep2", "Episodio 2", "Profundizando"),
Episode("ep3", "Episodio 3", "Conclusiones")
)
private var currentIndex = 0
private fun setupPodcastPlayer() {
val currentEpisode = episodes[currentIndex]
val config = MediastreamPlayerConfig().apply {
id = currentEpisode.id
type = MediastreamPlayerConfig.VideoTypes.EPISODE
playerType = MediastreamPlayerConfig.PlayerType.AUDIO
loadNextAutomatically = true
// Configurar botones de navegación
notificationHasNext = currentIndex < episodes.size - 1
notificationHasPrevious = currentIndex > 0
}
miniPlayerConfig = MediastreamMiniPlayerConfig().apply {
songName = currentEpisode.title
description = currentEpisode.description
albumName = "Mi Podcast"
setStateNext = currentIndex < episodes.size - 1
setStatePrev = currentIndex > 0
}
MediastreamPlayerService.initializeService(
this, this, config, container, playerContainer,
miniPlayerConfig, false, config.accountID ?: "",
createPodcastCallback()
)
startForegroundService()
}
private fun createPodcastCallback() = object : MediastreamPlayerCallback {
override fun onNext() {
if (currentIndex < episodes.size - 1) {
currentIndex++
loadEpisode(episodes[currentIndex])
}
}
override fun onPrevious() {
if (currentIndex > 0) {
currentIndex--
loadEpisode(episodes[currentIndex])
}
}
override fun onEnd() {
// Auto-avanzar al siguiente episodio
if (currentIndex < episodes.size - 1) {
currentIndex++
loadEpisode(episodes[currentIndex])
}
}
// ... otros callbacks
}
private fun loadEpisode(episode: Episode) {
val config = MediastreamPlayerConfig().apply {
id = episode.id
type = MediastreamPlayerConfig.VideoTypes.EPISODE
playerType = MediastreamPlayerConfig.PlayerType.AUDIO
}
miniPlayerConfig.songName = episode.title
miniPlayerConfig.description = episode.description
miniPlayerConfig.setStateNext = currentIndex < episodes.size - 1
miniPlayerConfig.setStatePrev = currentIndex > 0
MediastreamPlayerService.getMsPlayer()?.reloadPlayer(config)
}
}
data class Episode(
val id: String,
val title: String,
val description: String
)Escenario: Reproductor de música con playlist y shuffle.
class MusicPlayerActivity : AppCompatActivity() {
private lateinit var playlist: MutableList<Track>
private var currentTrackIndex = 0
private var isShuffleEnabled = false
private fun setupMusicPlayer() {
// Cargar playlist
playlist = loadPlaylist().toMutableList()
val currentTrack = playlist[currentTrackIndex]
val config = MediastreamPlayerConfig().apply {
id = currentTrack.id
type = MediastreamPlayerConfig.VideoTypes.VOD
playerType = MediastreamPlayerConfig.PlayerType.AUDIO
videoFormat = MediastreamPlayerConfig.AudioVideoFormat.MP3
notificationHasNext = true
notificationHasPrevious = true
}
miniPlayerConfig = MediastreamMiniPlayerConfig().apply {
songName = currentTrack.title
description = currentTrack.artist
albumName = currentTrack.album
imageUrl = currentTrack.coverUrl
color = currentTrack.dominantColor
setStateNext = true
setStatePrev = true
}
// Usar servicio con sincronización para mejor experiencia
MediastreamPlayerServiceWithSync.initializeService(
this, this, config, container, playerView,
miniPlayerConfig, false, config.accountID ?: "",
createMusicCallback()
)
setupMediaController()
setupUIControls()
}
private fun createMusicCallback() = object : MediastreamPlayerCallback {
override fun onNext() {
playNextTrack()
}
override fun onPrevious() {
playPreviousTrack()
}
override fun onEnd() {
// Auto-avanzar al siguiente
playNextTrack()
}
override fun onPlay() {
updatePlayPauseButton(isPlaying = true)
}
override fun onPause() {
updatePlayPauseButton(isPlaying = false)
}
// ... otros callbacks
}
private fun playNextTrack() {
if (isShuffleEnabled) {
currentTrackIndex = (0 until playlist.size).random()
} else {
currentTrackIndex = (currentTrackIndex + 1) % playlist.size
}
loadTrack(playlist[currentTrackIndex])
}
private fun playPreviousTrack() {
currentTrackIndex = if (currentTrackIndex > 0) {
currentTrackIndex - 1
} else {
playlist.size - 1
}
loadTrack(playlist[currentTrackIndex])
}
private fun loadTrack(track: Track) {
val config = MediastreamPlayerConfig().apply {
id = track.id
type = MediastreamPlayerConfig.VideoTypes.VOD
playerType = MediastreamPlayerConfig.PlayerType.AUDIO
}
miniPlayerConfig.apply {
songName = track.title
description = track.artist
albumName = track.album
imageUrl = track.coverUrl
}
MediastreamPlayerServiceWithSync.getMsPlayer()?.reloadPlayer(config)
// Actualizar UI
updateNowPlaying(track)
}
private fun setupUIControls() {
findViewById<ImageButton>(R.id.btnShuffle).setOnClickListener {
isShuffleEnabled = !isShuffleEnabled
it.alpha = if (isShuffleEnabled) 1.0f else 0.5f
}
}
}
data class Track(
val id: String,
val title: String,
val artist: String,
val album: String,
val coverUrl: String,
val dominantColor: Int
)override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// SIEMPRE inicializar antes de usar
MediastreamPlayerService.initializeService(...)
// Luego iniciar el servicio
val intent = Intent(this, MediastreamPlayerService::class.java)
intent.action = "$packageName.action.startforeground"
ContextCompat.startForegroundService(this, intent)
}override fun onDestroy() {
super.onDestroy()
// Solo detener si realmente queremos que pare
// (no detener si queremos que continúe en background)
if (shouldStopPlayback) {
stopService(Intent(this, MediastreamPlayerService::class.java))
}
}
// Para servicio con sync
private fun releaseService() {
playerView.player?.release()
playerView.player = null
if (::controllerFuture.isInitialized) {
MediaController.releaseFuture(controllerFuture)
}
val stopIntent = Intent(this, MediastreamPlayerServiceWithSync::class.java)
stopIntent.action = "STOP_SERVICE"
startService(stopIntent)
}override fun onBackPressed() {
// Opción A: Mantener servicio activo (mover a background)
moveTaskToBack(true)
// Opción B: Detener servicio y salir
stopService(Intent(this, MediastreamPlayerService::class.java))
super.onBackPressed()
// Opción C: Preguntar al usuario
showExitDialog()
}
private fun showExitDialog() {
AlertDialog.Builder(this)
.setTitle("Salir")
.setMessage("¿Deseas detener la reproducción?")
.setPositiveButton("Detener") { _, _ ->
stopService(Intent(this, MediastreamPlayerService::class.java))
finish()
}
.setNegativeButton("Continuar en background") { _, _ ->
moveTaskToBack(true)
}
.show()
}override fun onResume() {
super.onResume()
// Verificar si el servicio ya está corriendo
if (isServiceRunning(MediastreamPlayerService::class.java)) {
// Reconectar a la instancia existente
val player = MediastreamPlayerService.getMsPlayer()
player?.let {
// Actualizar UI con estado actual
updateUIWithCurrentState(it)
}
}
}
private fun isServiceRunning(serviceClass: Class<*>): Boolean {
val manager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
@Suppress("DEPRECATION")
for (service in manager.getRunningServices(Int.MAX_VALUE)) {
if (serviceClass.name == service.service.className) {
return true
}
}
return false
}┌─────────────────────────────────────┐
│ [App Icon] Título de la Canción │
│ Artista - Álbum │
│ │
│ [Album Art - 64x64] │
│ │
│ [◀◀] [▶/⏸] [▶▶] │
│ Prev Play Next │
└─────────────────────────────────────┘
// En MediastreamPlayerConfig
config.apply {
// Texto
notificationSongName = "Never Gonna Give You Up"
notificationDescription = "Rick Astley - Whenever You Need Somebody"
// Imágenes
notificationImageUrl = "https://example.com/album-cover.jpg"
notificationIconUrl = R.drawable.ic_notification_small
// Color de acento (Android 8+)
notificationColor = Color.parseColor("#1DB954") // Verde Spotify
// Botones de navegación
notificationHasNext = true
notificationHasPrevious = true
// Auto-actualización (para streams en vivo)
fillAutomaticallyAudioNotification = true
}// Actualización manual de contenido
val service = MediastreamPlayerService.getMsPlayer()
// Cambiar metadata
miniPlayerConfig.apply {
songName = "Nueva Canción"
description = "Nuevo Artista"
albumName = "Nuevo Álbum"
imageUrl = "nueva-imagen.jpg"
// Controles dinámicos
setStateNext = hasNextSong
setStatePrev = hasPreviousSong
}
// La notificación se actualizará automáticamente// Activar logs en desarrollo
config.isDebug = true
// Los servicios generan logs como:
[MediastreamPlayerService] onCreate
[MediastreamPlayerService] setupPlayerNotificationManager
[MediastreamPlayerService] onStartCommand: action: startforeground
[MediastreamPlayerService] Player initialized successfully
[MediastreamPlayerService] Notification posted1. Iniciar reproducción
2. Presionar HOME
3. Verificar notificación visible
4. Verificar audio continúa
5. Controlar desde notificación
1. Iniciar reproducción
2. Presionar HOME
3. Cerrar app desde Recientes
4. Abrir app nuevamente
5. Verificar reconexión al servicio
6. Verificar estado sincronizado
1. Conectar auriculares Bluetooth
2. Iniciar reproducción
3. Usar controles de auriculares (play/pause/next)
4. Verificar respuesta correcta
1. Iniciar reproducción
2. Bloquear dispositivo
3. Verificar controles en lock screen
4. Verificar artwork visible
5. Controlar desde lock screen
1. Reproducir audio
2. Recibir llamada
3. Verificar pausa automática
4. Terminar llamada
5. Verificar reanudación (opcional)
# Ver servicios en ejecución
adb shell dumpsys activity services | grep Mediastream
# Ver notificaciones activas
adb shell dumpsys notification
# Verificar MediaSession
adb shell dumpsys media_session
# Simular botones de media
adb shell input keyevent KEYCODE_MEDIA_PLAY
adb shell input keyevent KEYCODE_MEDIA_PAUSE
adb shell input keyevent KEYCODE_MEDIA_NEXT
adb shell input keyevent KEYCODE_MEDIA_PREVIOUS
# Ver logs del servicio
adb logcat | grep MediastreamPlayer
# Matar app pero mantener servicio
adb shell am force-stop <package> # Solo mata activity, no servicioSíntomas:
- No aparece notificación
- Audio no se reproduce
- Logs muestran error
Causas posibles:
initializeService()no llamado antes destartForegroundService()- Permisos faltantes en AndroidManifest
- Container null
Solución:
// ORDEN CORRECTO:
// 1. Inicializar primero
MediastreamPlayerService.initializeService(...)
// 2. Luego iniciar servicio
val intent = Intent(this, MediastreamPlayerService::class.java)
intent.action = "$packageName.action.startforeground"
ContextCompat.startForegroundService(this, intent)
// Verificar en manifest:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<service
android:name="am.mediastre.mediastreamplatformsdkandroid.MediastreamPlayerService"
android:foregroundServiceType="mediaPlayback" />Síntomas:
- Notificación muestra información vieja
- Cambios en
miniPlayerConfigno reflejan
Causa:
- Falta callback
onConfigChangeoonLiveAudioCurrentSongChanged
Solución:
override fun onLiveAudioCurrentSongChanged(data: JSONObject?) {
data?.let {
// Actualizar miniPlayerConfig
miniPlayerConfig.songName = it.optString("song", "")
miniPlayerConfig.description = it.optString("artist", "")
miniPlayerConfig.imageUrl = it.optString("poster", "")
// NO es necesario hacer nada más
// El servicio detecta el cambio automáticamente
}
}
// O actualización manual:
override fun onConfigChange(config: MediastreamMiniPlayerConfig?) {
config?.let {
// La notificación ya se actualizó
Log.d(TAG, "Notificación actualizada: ${it.songName}")
}
}Síntomas:
- Audio se detiene después de unos minutos
- Servicio desaparece del sistema
Causa:
- Sistema mata el servicio por recursos
- Falta
START_STICKYo mal implementado
Solución:
// El servicio ya retorna START_STICKY internamente
// Asegurar no llamar stopSelf() incorrectamente
override fun onTaskRemoved(rootIntent: Intent?) {
// NO llamar stopSelf() aquí si quieres que continúe
// super.onTaskRemoved(rootIntent)
// Solo si quieres detener cuando app se cierra:
stopSelf()
}
// En el servicio, verificar:
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// ...
return START_STICKY // Reiniciar servicio si muere
}Síntomas:
- Botones en notificación no responden
- No se ejecutan callbacks onNext/onPrevious
Causa:
- Botones no habilitados correctamente
- BroadcastReceiver no registrado
Solución:
// Verificar que los botones estén habilitados:
config.notificationHasNext = true
config.notificationHasPrevious = true
miniPlayerConfig.setStateNext = true
miniPlayerConfig.setStatePrev = true
// Implementar callbacks:
override fun onNext() {
Log.d(TAG, "Next presionado desde notificación")
// Tu lógica aquí
}
override fun onPrevious() {
Log.d(TAG, "Previous presionado desde notificación")
// Tu lógica aquí
}Síntomas:
- App crashea al iniciar servicio en Android 13
- SecurityException en logs
Causa:
- Falta permiso POST_NOTIFICATIONS
Solución:
if (Build.VERSION.SDK_INT >= 33) {
if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED) {
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
REQUEST_NOTIFICATIONS
)
return // Esperar permiso antes de iniciar servicio
}
}
// Iniciar servicio solo después de tener permiso
startServiceWithPermission()Síntomas:
- Notificación sin imagen
- Icono por defecto en lugar de carátula
Causa:
- URL inválida o red lenta
- Glide no puede cargar imagen
Solución:
// Verificar URL válida
miniPlayerConfig.imageUrl = "https://valid-url.com/cover.jpg"
// Fallback con imagen local:
if (miniPlayerConfig.imageUrl.isNullOrEmpty()) {
miniPlayerConfig.imageUrl = "android.resource://${packageName}/${R.drawable.default_cover}"
}
// Verificar en logs:
[MediastreamPlayerService] getCurrentLargeIcon: <url>| Característica | MediastreamPlayerService | MediastreamPlayerServiceWithSync |
|---|---|---|
| Reproducción background | ✅ Sí | ✅ Sí |
| Notificación multimedia | ✅ Sí | ✅ Sí |
| Controles sistema | ✅ Básico | ✅ Completo |
| MediaSession | ✅ Básico | ✅ Avanzado |
| Sincronización UI | ❌ Manual | ✅ Automática |
| MediaController | ❌ No | ✅ Sí |
| Android Auto | ❌ No | ✅ Sí |
| EventBus | ❌ No | ✅ Sí |
| Library browsing | ❌ No | ✅ Sí |
| Comandos custom | ❌ Limitado | ✅ Completo |
| Complejidad | 🟢 Baja | 🟡 Media |
| Uso recomendado | Radio simple, audio básico | Podcasts, música, apps complejas |
Usa MediastreamPlayerService si:
- App simple de radio/audio
- No necesitas sincronización compleja
- No planeas soportar Android Auto
- Quieres implementación rápida
Usa MediastreamPlayerServiceWithSync si:
- App profesional de audio
- Necesitas UI sincronizada automáticamente
- Quieres soporte Android Auto
- Requieres navegación por contenido
- App con múltiples activities/fragments
- Siempre inicializar antes de iniciar servicio
- Manejar permisos en Android 13+
- Implementar todos los callbacks necesarios
- Proporcionar artwork de calidad
- Testear en diferentes versiones de Android
- Verificar comportamiento con interrupciones (llamadas, alarmas)
- Optimizar carga de imágenes (Glide cache)
- Manejar errores de red gracefully
- Considerar battery optimization
- Documentar configuración para otros desarrolladores
Los servicios de reproducción en background del SDK de Mediastream proporcionan:
✅ Reproducción continua independiente del ciclo de vida de la Activity
✅ Notificaciones multimedia profesionales con controles
✅ Integración completa con el sistema operativo
✅ Soporte Android Auto para apps de audio en vehículos
✅ Sincronización automática entre UI y servicio (con sync)
✅ Experiencia de usuario comparable a apps líderes del mercado
Estos servicios son esenciales para cualquier app de audio profesional y proporcionan todas las herramientas necesarias para crear experiencias de reproducción de clase mundial.
Documento preparado para el equipo de desarrollo
Fecha: Febrero 2026
Versión: 1.0
Estado: Implementado y documentado