Skip to content

Commit

Permalink
update: save player progress
Browse files Browse the repository at this point in the history
  • Loading branch information
muedsa committed Nov 10, 2023
1 parent cb75017 commit 1004333
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 119 deletions.
23 changes: 17 additions & 6 deletions app/src/main/kotlin/com/muedsa/agetv/PlaybackActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import com.muedsa.agetv.ui.features.playback.PlaybackScreen
import com.muedsa.compose.tv.theme.TvTheme
import com.muedsa.compose.tv.widget.AppCloseHandler
import com.muedsa.compose.tv.widget.ErrorMessageBox
import com.muedsa.compose.tv.widget.ErrorMessageBoxState
import com.muedsa.compose.tv.widget.FillTextScreen
import dagger.hilt.android.AndroidEntryPoint

Expand All @@ -34,12 +38,19 @@ class PlaybackActivity : ComponentActivity() {
if (aid <= 0 || episodeTitle.isNullOrEmpty() || mediaUrl.isNullOrEmpty()) {
FillTextScreen(context = "视频地址错误")
} else {
PlaybackScreen(
aid = aid,
episodeTitle = episodeTitle,
mediaUrl = mediaUrl,
danEpisodeId = episodeId
)
val errorMsgBoxState = remember { ErrorMessageBoxState() }
AppCloseHandler {
errorMsgBoxState.error("再次点击返回键退出")
}
ErrorMessageBox(state = errorMsgBoxState) {
PlaybackScreen(
aid = aid,
episodeTitle = episodeTitle,
mediaUrl = mediaUrl,
danEpisodeId = episodeId,
errorMsgBoxState = errorMsgBoxState
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ interface EpisodeProgressDao {
@Query("SELECT * FROM episode_progress WHERE aid = :aid")
suspend fun getListByAid(aid: Int): List<EpisodeProgressModel>

@Query("SELECT * FROM episode_progress WHERE aid = :aid and title_hash = :titleHash")
suspend fun getOneByAidAndTitleHash(aid: Int, titleHash: Int): EpisodeProgressModel?

@Upsert
suspend fun upsert(model: EpisodeProgressModel)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ data class EpisodeProgressModel(
defaultValue = "(CURRENT_TIMESTAMP)",
index = true
) var updateAt: Long = 0
)
) {
companion object {
val Empty = EpisodeProgressModel(0, 0, "", 0, 0, 0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,24 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import com.muedsa.agetv.BuildConfig
import com.muedsa.agetv.model.LazyType
import com.muedsa.agetv.viewmodel.PlaybackViewModel
import com.muedsa.compose.tv.widget.ErrorMessageBox
import com.muedsa.compose.tv.widget.ErrorMessageBoxState
import com.muedsa.compose.tv.widget.player.DanmakuVideoPlayer
import com.muedsa.uitl.LogUtil
import kotlinx.coroutines.delay
import kotlin.math.max
import kotlin.time.DurationUnit
import kotlin.time.toDuration

@Composable
@OptIn(UnstableApi::class)
Expand All @@ -31,100 +36,124 @@ fun PlaybackScreen(
mediaUrl: String,
danEpisodeId: Long = 0,
playbackViewModel: PlaybackViewModel = hiltViewModel(),
errorMsgBoxState: ErrorMessageBoxState
) {
val activity = (LocalContext.current as? Activity)
val activity = LocalContext.current as? Activity

val errorMsgBoxState = remember { ErrorMessageBoxState() }
ErrorMessageBox(state = errorMsgBoxState) {
val danmakuSettingLD by playbackViewModel.danmakuSettingLDSF.collectAsState()
val danmakuListLD by playbackViewModel.danmakuListLDSF.collectAsState()
val episodeProgress by playbackViewModel.episodeProgressSF.collectAsState()

val danmakuSettingLD by playbackViewModel.danmakuSettingLDSF.collectAsState()
val danmakuListLD by playbackViewModel.danmakuListLDSF.collectAsState()
var exoplayerHolder by remember { mutableStateOf<ExoPlayer?>(null) }

LaunchedEffect(key1 = danmakuSettingLD) {
if (danmakuListLD.type == LazyType.FAILURE) {
errorMsgBoxState.error(danmakuListLD.error)
}
var playerEnd by remember { mutableStateOf(false) }

LaunchedEffect(key1 = danmakuSettingLD) {
if (danmakuListLD.type == LazyType.FAILURE) {
errorMsgBoxState.error(danmakuListLD.error)
}
}

LaunchedEffect(key1 = danmakuListLD) {
if (danmakuListLD.type == LazyType.FAILURE) {
errorMsgBoxState.error(danmakuListLD.error)
}
}

LaunchedEffect(key1 = danmakuListLD) {
if (danmakuListLD.type == LazyType.FAILURE) {
errorMsgBoxState.error(danmakuListLD.error)
LaunchedEffect(key1 = playerEnd) {
if (playerEnd) {
errorMsgBoxState.error("播放结束,即将返回")
delay(3_000)
activity?.finish()
}
}

LaunchedEffect(key1 = exoplayerHolder) {
if (exoplayerHolder != null) {
val exoPlayer = exoplayerHolder!!
delay(10_000)
if (episodeProgress.aid == aid) {
episodeProgress.progress = exoPlayer.currentPosition
episodeProgress.duration = exoPlayer.duration
if (episodeProgress.progress + 5_000 > episodeProgress.duration) {
episodeProgress.progress = max(episodeProgress.duration - 5_000, 0)
}
episodeProgress.updateAt = System.currentTimeMillis()
playbackViewModel.saveEpisodeProgress(episodeProgress)
}
}
}

if (danmakuListLD.type == LazyType.SUCCESS && danmakuSettingLD.type == LazyType.SUCCESS) {
val danmakuSetting = danmakuSettingLD.data!!

DanmakuVideoPlayer(
debug = BuildConfig.DEBUG,
danmakuConfigSetting = {
textSizeScale = danmakuSetting.danmakuSizeScale / 100f
alpha = danmakuSetting.danmakuAlpha / 100f
screenPart = danmakuSetting.danmakuScreenPart / 100f
},
danmakuPlayerInit = {
if (!danmakuListLD.data.isNullOrEmpty()) {
updateData(danmakuListLD.data!!)
}
if (danmakuListLD.type == LazyType.SUCCESS && danmakuSettingLD.type == LazyType.SUCCESS && episodeProgress.aid == aid) {
val danmakuSetting = danmakuSettingLD.data!!

DanmakuVideoPlayer(
debug = BuildConfig.DEBUG,
danmakuConfigSetting = {
textSizeScale = danmakuSetting.danmakuSizeScale / 100f
alpha = danmakuSetting.danmakuAlpha / 100f
screenPart = danmakuSetting.danmakuScreenPart / 100f
},
danmakuPlayerInit = {
if (!danmakuListLD.data.isNullOrEmpty()) {
updateData(danmakuListLD.data!!)
}
) {
addListener(object : Player.Listener {
private val stopState = mutableStateOf(true)

override fun onPlayerErrorChanged(error: PlaybackException?) {
errorMsgBoxState.error(error, SnackbarDuration.Long)
error?.let {
LogUtil.fb(it, "exoplayer mediaUrl: $mediaUrl")
}
}
}
) {
addListener(object : Player.Listener {

override fun onRenderedFirstFrame() {
stopState.value = false
playbackViewModel.registerPlayerPositionSaver(
aid,
episodeTitle,
this@DanmakuVideoPlayer,
stopState
)
override fun onPlayerErrorChanged(error: PlaybackException?) {
errorMsgBoxState.error(error, SnackbarDuration.Long)
error?.let {
LogUtil.fb(it, "exoplayer mediaUrl: $mediaUrl")
}
}

override fun onPlaybackStateChanged(playbackState: Int) {
LogUtil.d("onPlaybackStateChanged: $playbackState")
when (playbackState) {
Player.STATE_READY -> {

}

Player.STATE_ENDED -> {
stopState.value = true
activity?.finish()
}

Player.STATE_IDLE -> {
stopState.value = true
}

Player.STATE_BUFFERING -> {

}
override fun onRenderedFirstFrame() {
if (exoplayerHolder == null) {
exoplayerHolder = this@DanmakuVideoPlayer
if (episodeProgress.progress > 0) {
seekTo(episodeProgress.progress)
val progressStr = episodeProgress.progress
.toDuration(DurationUnit.MILLISECONDS)
.toComponents { hours, minutes, seconds, _ ->
String.format(
"%02d:%02d:%02d",
hours,
minutes,
seconds,
)
}
errorMsgBoxState.error("跳转到上次播放位置: $progressStr")
}
}
})
playWhenReady = true
setMediaItem(MediaItem.fromUri(mediaUrl))
prepare()
}
}

override fun onPlaybackStateChanged(playbackState: Int) {
val ended = playbackState == Player.STATE_ENDED
if (playerEnd != ended) {
playerEnd = ended
}
}
})
playWhenReady = true
setMediaItem(MediaItem.fromUri(mediaUrl))
prepare()
}
}

if (danmakuSettingLD.type == LazyType.SUCCESS) {
val danmakuSetting = danmakuSettingLD.data!!
LaunchedEffect(key1 = danEpisodeId) {
playbackViewModel.loadDanmakuList(
if (danmakuSetting.danmakuEnable)
danEpisodeId
else 0
)
}
if (danmakuSettingLD.type == LazyType.SUCCESS) {
val danmakuSetting = danmakuSettingLD.data!!
LaunchedEffect(key1 = danEpisodeId) {
playbackViewModel.loadDanmakuList(
if (danmakuSetting.danmakuEnable)
danEpisodeId
else 0
)
}
}

LaunchedEffect(key1 = aid, key2 = episodeTitle) {
playbackViewModel.loadEpisodeProgress(aid, episodeTitle)
}
}
49 changes: 15 additions & 34 deletions app/src/main/kotlin/com/muedsa/agetv/viewmodel/PlaybackViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.muedsa.agetv.viewmodel

import androidx.compose.runtime.State
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.media3.exoplayer.ExoPlayer
import com.kuaishou.akdanmaku.data.DanmakuItemData
import com.muedsa.agetv.KEY_DANMAKU_ALPHA
import com.muedsa.agetv.KEY_DANMAKU_ENABLE
Expand All @@ -12,11 +10,12 @@ import com.muedsa.agetv.KEY_DANMAKU_SIZE_SCALE
import com.muedsa.agetv.model.AppSettingModel
import com.muedsa.agetv.model.LazyData
import com.muedsa.agetv.repository.DataStoreRepo
import com.muedsa.agetv.room.dao.EpisodeProgressDao
import com.muedsa.agetv.room.model.EpisodeProgressModel
import com.muedsa.agetv.service.DanDanPlayApiService
import com.muedsa.uitl.LogUtil
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
Expand All @@ -30,7 +29,8 @@ import javax.inject.Inject
@HiltViewModel
class PlaybackViewModel @Inject constructor(
private val danDanPlayApiService: DanDanPlayApiService,
private val dateStoreRepo: DataStoreRepo
dateStoreRepo: DataStoreRepo,
private val episodeProgressDao: EpisodeProgressDao
) : ViewModel() {

val danmakuSettingLDSF: StateFlow<LazyData<AppSettingModel>> = dateStoreRepo.dataStore.data
Expand All @@ -57,7 +57,8 @@ class PlaybackViewModel @Inject constructor(
private val _danmakuListLDSF = MutableStateFlow(LazyData.init<List<DanmakuItemData>>())
val danmakuListLDSF: StateFlow<LazyData<List<DanmakuItemData>>> = _danmakuListLDSF

private val _saverIdSet: MutableSet<String> = mutableSetOf()
private val _episodeProgressSF = MutableStateFlow(EpisodeProgressModel.Empty)
val episodeProgressSF: StateFlow<EpisodeProgressModel> = _episodeProgressSF

fun loadDanmakuList(episodeId: Long) {
viewModelScope.launch {
Expand Down Expand Up @@ -111,41 +112,21 @@ class PlaybackViewModel @Inject constructor(
}
}

fun registerPlayerPositionSaver(
fun loadEpisodeProgress(
aid: Int,
episodeTitle: String,
exoPlayer: ExoPlayer,
stopState: State<Boolean>
) {
val id = "$aid:$episodeTitle"
synchronized(_saverIdSet) {
if (!_saverIdSet.contains(id)) {
_saverIdSet.add(id)
saverRunning(id, aid, episodeTitle, exoPlayer, stopState)
}
viewModelScope.launch(Dispatchers.IO) {
val titleHash = episodeTitle.hashCode()
_episodeProgressSF.value = episodeProgressDao.getOneByAidAndTitleHash(aid, titleHash)
?: EpisodeProgressModel(aid, titleHash, episodeTitle, 0, 0, 0)
}
}

private fun saverRunning(
id: String,
aid: Int,
episodeTitle: String,
exoPlayer: ExoPlayer,
stopState: State<Boolean>
) {
viewModelScope.launch(Dispatchers.Unconfined) {
LogUtil.d("[PlayerPositionSaver-${id}] running for $aid-$episodeTitle")
while (!stopState.value) {
delay(15 * 1000)
val pos = withContext(Dispatchers.Main) {
exoPlayer.currentPosition
}
LogUtil.d("[PlayerPositionSaver-${id}] save pos: $pos for $aid-$episodeTitle")
}
LogUtil.d("[PlayerPositionSaver-${id}] stop for $aid-$episodeTitle")
synchronized(_saverIdSet) {
_saverIdSet.remove(id)
}
fun saveEpisodeProgress(model: EpisodeProgressModel) {
viewModelScope.launch(Dispatchers.IO) {
LogUtil.d("save episode progress: ${model.progress}/${model.duration}")
episodeProgressDao.upsert(model)
}
}
}

0 comments on commit 1004333

Please sign in to comment.