@@ -21,7 +21,6 @@ import androidx.annotation.OptIn
2121import androidx.compose.runtime.Stable
2222import androidx.compose.runtime.derivedStateOf
2323import androidx.compose.runtime.getValue
24- import androidx.compose.runtime.mutableStateMapOf
2524import androidx.compose.runtime.mutableStateOf
2625import androidx.compose.runtime.setValue
2726import androidx.compose.runtime.snapshotFlow
@@ -71,8 +70,6 @@ class ExoplayerController(
7170) : VideoPlayerController,
7271 Player .Listener {
7372
74- override var isMuted: Boolean by mutableStateOf(true )
75-
7673 /* *
7774 * A [Job] for diffing the [ExoPlayer] playlist such that changing the active video does not discard buffered
7875 * videos.
@@ -85,12 +82,9 @@ class ExoplayerController(
8582 */
8683 private val mediaItemMutationsChannel = Channel < (List <MediaItem >) -> List <MediaItem >>()
8784
88- /* *
89- * A map of ids registered in this [VideoPlayerController] and its associated
90- * [ExoPlayerState]s. Note that the presence of an id in this map is _not_ an indication
91- * that the backing media is available to play. For that information, reference [currentPlaylistIds].
92- */
93- private val idsToStates = mutableStateMapOf<String , ExoPlayerState >()
85+ private val states = VideoPlayerStates <ExoPlayerState >()
86+
87+ override var isMuted: Boolean by states::isMuted
9488
9589 /* *
9690 * The ids of media available to play in the available [player] instance.
@@ -104,23 +98,17 @@ class ExoplayerController(
10498
10599 private var player: ExoPlayer ? by mutableStateOf(null )
106100
107- private var activeVideoId: String by mutableStateOf(" " )
108-
109101 // TODO: Revisit this. The Coroutine should be launched lazily instead of in an init block.
110102 init {
111103 player = exoPlayer(context = context).apply {
112104 // The first listener should always be the ExoplayerManager
113105 addListener(this @ExoplayerController)
114106 // Bind active video to restore its properties and attach its listener
115- val hasActiveVideo = idsToStates[activeVideoId]?.let (::bind) != null
116- // Restore the players playlist if it was previously saved
117- val restoredMediaItems = idsToStates.values.map { it.toMediaItem() }
118- addMediaItems(restoredMediaItems)
119- currentPlaylistIds = restoredMediaItems.map(MediaItem ::mediaId).toSet()
120- playWhenReady = getVideoStateById(activeVideoId)?.autoplay == true
107+ val hasActiveVideo = states.activeState?.let (::bind) != null
108+ playWhenReady = states.activeState?.autoplay == true
121109 if (playWhenReady) {
122110 prepare()
123- if (playWhenReady && hasActiveVideo) play(activeVideoId)
111+ if (playWhenReady && hasActiveVideo) play(states. activeVideoId)
124112 }
125113 }
126114 diffingJob?.cancel()
@@ -141,7 +129,7 @@ class ExoplayerController(
141129 // TODO this should also be governed by coroutine launch semantics
142130 // as the one used for list diffing
143131 // Pause playback when nothing is visible to play
144- snapshotFlow { idsToStates[activeVideoId] ?.status }
132+ snapshotFlow { states.activeState ?.status }
145133 .map { it == null || it is PlayerStatus .Idle }
146134 .filter(true ::equals)
147135 .onEach { player?.pause() }
@@ -156,10 +144,10 @@ class ExoplayerController(
156144 videoId : String ,
157145 autoplay : Boolean ,
158146 ) {
159- idsToStates [videoId]?.let {
147+ states [videoId]?.let {
160148 it.autoplay = autoplay
161149 }
162- if (videoId != activeVideoId) return
150+ if (videoId != states. activeVideoId) return
163151
164152 if (! autoplay) {
165153 player?.pause()
@@ -172,38 +160,41 @@ class ExoplayerController(
172160 videoId : String? ,
173161 seekToMs : Long? ,
174162 ) {
175- val playerIdToPlay = videoId ? : activeVideoId
163+ val playerIdToPlay = videoId ? : states. activeVideoId
176164
177165 // Video has not been previously registered
178- val stateToPlay = idsToStates [playerIdToPlay] ? : return
166+ val stateToPlay = states [playerIdToPlay] ? : return
179167
180168 setActiveVideo(playerIdToPlay)
181169
182170 // Already playing and not seeking, do nothing
183171 if (stateToPlay.status is PlayerStatus .Play .Confirmed && seekToMs == null ) return
184172
185173 // Diffing is async. Suspend until the video to play is registered in the player
186- playAsync(playerIdToPlay, seekToMs)
174+ playAsync(
175+ playerIdToPlay = playerIdToPlay,
176+ seekToMs = seekToMs,
177+ )
187178 }
188179
189180 override fun pauseActiveVideo () {
190- activeVideoId. let (idsToStates::get) ?.apply {
181+ states.activeState ?.apply {
191182 status = PlayerStatus .Pause .Requested
192183 }
193184 player?.pause()
194185 }
195186
196187 private fun setActiveVideo (videoId : String ) {
197188 // Video has not been previously registered
198- val activeState = idsToStates [videoId] ? : return
189+ val activeState = states [videoId] ? : return
199190
200- val previousId = activeVideoId
201- activeVideoId = videoId
191+ val previousId = states. activeVideoId
192+ states. activeVideoId = videoId
202193
203- if (previousId == activeVideoId) return
194+ if (previousId == states. activeVideoId) return
204195
205196 player?.apply {
206- idsToStates [previousId]?.let (::unbind)
197+ states [previousId]?.let (::unbind)
207198 bind(activeState)
208199 }
209200 // NOTE: Play must be called on the manager and not on the exoplayer instance itself.
@@ -221,15 +212,18 @@ class ExoplayerController(
221212 }
222213 }
223214
224- override fun getVideoStateById (videoId : String ): VideoPlayerState ? = idsToStates [videoId]
215+ override fun getVideoStateById (videoId : String ): VideoPlayerState ? = states [videoId]
225216
226217 override fun retry (videoId : String ) {
227218 setActiveVideo(videoId)
228219 player?.prepare()
229220 }
230221
231222 override fun seekTo (position : Long ) {
232- play(videoId = activeVideoId, seekToMs = position)
223+ play(
224+ videoId = states.activeVideoId,
225+ seekToMs = position,
226+ )
233227 }
234228
235229 override fun onPlaybackStateChanged (playbackState : Int ) {
@@ -255,24 +249,24 @@ class ExoplayerController(
255249 isLooping : Boolean ,
256250 autoplay : Boolean ,
257251 ): VideoPlayerState {
258- idsToStates[videoId]?.let { return it }
259-
260- trim()
261- val videoPlayerState = ExoPlayerState (
262- videoUrl = videoUrl,
263- videoId = videoId,
264- thumbnail = thumbnail,
265- autoplay = autoplay,
266- isLooping = isLooping,
267- isMuted = derivedStateOf {
268- isMuted
269- },
270- exoPlayerState = derivedStateOf {
271- player.takeIf { isCurrentMediaItem(videoId) }
272- },
273- )
252+ states[videoId]?.let { return it }
253+
254+ val videoPlayerState = states.registerOrGet(videoId = videoId) {
255+ ExoPlayerState (
256+ videoUrl = videoUrl,
257+ videoId = videoId,
258+ thumbnail = thumbnail,
259+ autoplay = autoplay,
260+ isLooping = isLooping,
261+ isMuted = derivedStateOf {
262+ isMuted
263+ },
264+ exoPlayerState = derivedStateOf {
265+ player.takeIf { isCurrentMediaItem(videoId) }
266+ },
267+ )
268+ }
274269
275- idsToStates[videoId] = videoPlayerState
276270 val mediaItem = videoPlayerState.toMediaItem()
277271
278272 // Add the new media item to the ExoPlayer playlist.
@@ -291,30 +285,11 @@ class ExoplayerController(
291285 return videoPlayerState
292286 }
293287
294- override fun unregisterAll (retainedVideoIds : Set <String >): Set <String > {
295- idsToStates
296- .filterNot { retainedVideoIds.contains(it.key) }
297- .forEach { (id, videoState) ->
298- if (activeVideoId == id) {
299- player?.pause()
300- }
301- player?.unbind(videoState)
302- idsToStates.remove(id)
303- }
304- // Remove all videos that have not been retained from the playlist.In
305- scope.launch {
306- mediaItemMutationsChannel.send { existingItems ->
307- existingItems.filter { retainedVideoIds.contains(it.mediaId) }
308- }
309- }
310- return retainedVideoIds - idsToStates.keys
311- }
312-
313288 internal fun teardown () {
314289 diffingJob?.cancel()
315290 player?.apply {
316291 removeListener(this @ExoplayerController)
317- idsToStates[activeVideoId] ?.let {
292+ states.activeState ?.let {
318293 removeListener(it.playerListener)
319294 it.updateFromPlayer()
320295 }
@@ -332,7 +307,7 @@ class ExoplayerController(
332307 ) {
333308 scope.launch {
334309 // Do this only while playerId is the activeVideo
335- snapshotFlow { activeVideoId == playerIdToPlay }
310+ snapshotFlow { states. activeVideoId == playerIdToPlay }
336311 .flatMapLatest { isActiveVideo ->
337312 if (isActiveVideo) {
338313 snapshotFlow { currentPlaylistIds.contains(playerIdToPlay) }
@@ -356,16 +331,6 @@ class ExoplayerController(
356331 }
357332 }
358333
359- private fun trim () {
360- val size = idsToStates.size
361- if (size >= MaxVideoStates ) idsToStates.keys.filter {
362- val state = idsToStates[it]
363- state?.status is PlayerStatus .Idle .Evicted
364- }
365- .take(size - MaxVideoStates )
366- .forEach(idsToStates::remove)
367- }
368-
369334 /* *
370335 * Diffs media items and inserts them in the ExoPlayer playlist.
371336 * This ensures that a previously buffered video does not need to re-buffer when it is swapped
@@ -392,7 +357,7 @@ class ExoplayerController(
392357 }
393358
394359 private fun isCurrentMediaItem (videoId : String ) =
395- activeVideoId == videoId && currentMediaItem?.mediaId == videoId
360+ states. activeVideoId == videoId && currentMediaItem?.mediaId == videoId
396361
397362 private val Player .currentMediaItems: List <MediaItem >
398363 get() = List (mediaItemCount, ::getMediaItemAt)
@@ -445,5 +410,3 @@ private fun VideoPlayerState.toMediaItem() =
445410 .setUri(videoUrl)
446411 .setMediaId(videoId)
447412 .build()
448-
449- private const val MaxVideoStates = 30
0 commit comments