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.
87 lines
3.1 KiB
Kotlin
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)
|
|
}
|
|
}
|
|
}
|