/* * Copyright (c) Double Symmetry GmbH * Commercial use requires a license. See https://rntp.dev/pricing */ package com.doublesymmetry.trackplayer import androidx.media3.common.MediaItem import androidx.media3.exoplayer.source.ForwardingMediaSourceFactory import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.WrappingMediaSource import java.util.IdentityHashMap /** * Forces an explicitly marked item replacement to create a new source holder. * * A URI fragment or custom cache key would also make some Media3 sources reject * an in-place update, but would change app-visible request/cache semantics and * does not work consistently across source types. This identity-scoped token * leaves every [MediaItem] field untouched and uses Media3's update contract. */ internal class LiveRefreshMediaSourceFactory( delegate: MediaSource.Factory, ) : ForwardingMediaSourceFactory(delegate) { override fun createMediaSource(mediaItem: MediaItem): MediaSource = TokenAwareMediaSource(super.createMediaSource(mediaItem), LiveRefreshTokens.consume(mediaItem)) private class TokenAwareMediaSource( source: MediaSource, private val token: Long?, ) : WrappingMediaSource(source) { override fun canUpdateMediaItem(mediaItem: MediaItem): Boolean { val requestedToken = LiveRefreshTokens.peek(mediaItem) return (requestedToken == null || requestedToken == token) && super.canUpdateMediaItem(mediaItem) } } } internal object LiveRefreshTokens { private val tokens = IdentityHashMap() fun mark(mediaItem: MediaItem, generation: Long) = synchronized(tokens) { tokens[mediaItem] = generation } fun peek(mediaItem: MediaItem): Long? = synchronized(tokens) { tokens[mediaItem] } fun consume(mediaItem: MediaItem): Long? = synchronized(tokens) { tokens.remove(mediaItem) } fun discard(mediaItem: MediaItem) = synchronized(tokens) { tokens.remove(mediaItem) Unit } }