package com.suno.android.omniplayer import android.content.Context import androidx.annotation.OptIn import androidx.lifecycle.ViewModel import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.cache.CacheDataSource import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import com.suno.android.common_core_utils.SunoLogger import com.suno.android.common_core_utils.model.Url import com.suno.android.gating.Feature import com.suno.android.gating.FeatureManager import com.suno.android.media.MediaManager import com.suno.android.media.VideoPreview import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject @OptIn(UnstableApi::class) @HiltViewModel class AudioPlayerViewModel @Inject constructor( loggerFactory: SunoLogger.Factory, private val mediaManager: MediaManager, @VideoPreview private val exoCacheDataSourceFactory: CacheDataSource.Factory, featureManager: FeatureManager, ) : ViewModel() { private val logger = loggerFactory.create(this@AudioPlayerViewModel) private val videoCoverPlayerStateList = mutableMapOf>() private fun getVideoCoverPlayerMutableState( page: Int, ): MutableStateFlow = videoCoverPlayerStateList.getOrPut(page) { MutableStateFlow(null) } fun getVideoCoverPlayerState( page: Int, ): StateFlow = getVideoCoverPlayerMutableState(page).asStateFlow() private var currentPage: Int = 0 private val audioPlayerListener = object : Player.Listener { override fun onPositionDiscontinuity( oldPosition: Player.PositionInfo, newPosition: Player.PositionInfo, reason: Int, ) { getVideoCoverPlayerState(currentPage).value?.let { currentVideoPlayer -> // The video cover is a short video on repeat val targetSeek = newPosition.positionMs % currentVideoPlayer.duration currentVideoPlayer.seekTo(targetSeek) } } } val isVideoCoverGateEnabled = featureManager.hasFeature(Feature.VideoCoverPlayer) init { mediaManager.getPlayer().addListener(audioPlayerListener) } override fun onCleared() { mediaManager.getPlayer().removeListener(audioPlayerListener) } fun setVideoCoverUrl( page: Int, videoCoverUrl: Url?, context: Context, ) { if (videoCoverUrl == null) { return } val flow = getVideoCoverPlayerMutableState(page) val player = flow.value ?: createVideoPlayer(context).also { flow.value = it } player.setMediaItem(MediaItem.fromUri(videoCoverUrl.toUri())) } fun handlePageFocused( page: Int, ) { currentPage = page videoCoverPlayerStateList.forEach { entry -> if (entry.key == page) { entry.value.value?.play() } else { entry.value.value?.pause() } } } private fun createVideoPlayer( context: Context, ) = ExoPlayer.Builder( context, ).setMediaSourceFactory( DefaultMediaSourceFactory(context).setDataSourceFactory(exoCacheDataSourceFactory), ).build().apply { prepare() repeatMode = Player.REPEAT_MODE_ONE addListener( object : Player.Listener { override fun onPlayerError( error: PlaybackException, ) { logger.e(error) } }, ) } fun handleDispose( page: Int, ) { val flow = getVideoCoverPlayerMutableState(page) flow.value?.release() flow.value = null } }