Files
Noxely/app/src/main/java/fr/tuxgyver/cachette/playback/PlaybackService.kt
T
tuxgyver 992fe88cc4 Première version : lecteur audio et vidéo hors connexion (1.0.0)
Cachette met des médias de côté pendant qu'il y a du réseau, pour les lire
quand il n'y en a plus.

- Lecture audio et vidéo en arrière-plan (Media3 MediaSessionService)
- Mise en cache explicite, reprise automatique après coupure (WorkManager)
- Sources : appareil (MediaStore), lien direct, flux RSS de podcast
- Catalogues libres de droits : Internet Archive et LibriVox
- Parcours d'une source avec sélection multiple et téléchargement en lot
- Interface Jetpack Compose Material 3, thèmes clair et sombre
- Makefile catégorisé, publication Gitea déléguée à BorisSkill

Le fichier VERSION est la source unique de la version : versionCode en est
déduit et la publication s'y réfère.
2026-09-11 19:55:08 +02:00

87 lines
3.1 KiB
Kotlin

package fr.tuxgyver.cachette.playback
import android.app.PendingIntent
import android.content.Intent
import androidx.annotation.OptIn
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import fr.tuxgyver.cachette.CachetteApp
import fr.tuxgyver.cachette.MainActivity
/** Lecture audio et vidéo en arrière-plan, avec notification et commandes système. */
@OptIn(UnstableApi::class)
class PlaybackService : MediaSessionService() {
private var session: MediaSession? = null
override fun onCreate() {
super.onCreate()
val container = (application as CachetteApp).container
val player = ExoPlayer.Builder(this)
.setMediaSourceFactory(DefaultMediaSourceFactory(container.offline.playbackDataSourceFactory()))
.setAudioAttributes(
AudioAttributes.Builder()
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.setUsage(C.USAGE_MEDIA)
.build(),
/* handleAudioFocus = */ true,
)
.setHandleAudioBecomingNoisy(true)
.build()
val openApp = PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
session = MediaSession.Builder(this, player)
.setSessionActivity(openApp)
.setCallback(ResolvingCallback())
.build()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = session
override fun onTaskRemoved(rootIntent: Intent?) {
val player = session?.player
if (player == null || !player.playWhenReady || player.mediaItemCount == 0) {
stopSelf()
}
}
override fun onDestroy() {
session?.run {
player.release()
release()
}
session = null
super.onDestroy()
}
/** Rend leur URI aux éléments reçus du contrôleur, à partir de la bibliothèque. */
private inner class ResolvingCallback : MediaSession.Callback {
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: MutableList<MediaItem>,
): ListenableFuture<MutableList<MediaItem>> {
val library = (application as CachetteApp).container.library
val resolved = mediaItems.mapTo(mutableListOf()) { item ->
library.entry(item.mediaId)?.toMediaItem() ?: item
}
return Futures.immediateFuture(resolved)
}
}
}