Skip to content

Commit

Permalink
优化 PlayerLauncher 加载视频的逻辑, 修复视频状态同步问题
Browse files Browse the repository at this point in the history
- Fix #784 BT 播放时显示"加载失败: 已取消", 但实际上正常下载
- 修复选择数据源后可能仍然显示"请选择"
  • Loading branch information
Him188 committed Aug 26, 2024
1 parent 732fe55 commit 29a0e43
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ package me.him188.ani.app.ui.subject.episode.video
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformLatest
import me.him188.ani.app.data.models.episode.EpisodeInfo
Expand All @@ -21,7 +19,6 @@ import me.him188.ani.app.data.source.media.resolver.VideoSourceResolver
import me.him188.ani.app.data.source.media.selector.MediaSelector
import me.him188.ani.app.ui.foundation.BackgroundScope
import me.him188.ani.app.ui.foundation.HasBackgroundScope
import me.him188.ani.app.ui.foundation.launchInBackground
import me.him188.ani.app.ui.subject.episode.statistics.DelegateVideoStatistics
import me.him188.ani.app.ui.subject.episode.statistics.VideoLoadingState
import me.him188.ani.app.ui.subject.episode.statistics.VideoStatistics
Expand Down Expand Up @@ -64,81 +61,71 @@ class PlayerLauncher(
videoLoadingState = videoLoadingStateFlow.produceState(VideoLoadingState.Initial),
)

/**
* The [VideoSource] selected to play.
*
* `null` has two possible meanings:
* - List of video sources are still downloading so user has nothing to select.
* - The sources are available but user has not yet selected one.
*/
private val videoSource: SharedFlow<VideoSource<*>?> = mediaSelector.selected
.transformLatest { media ->
emit(null)
if (media == null) return@transformLatest
init {
mediaSelector.selected.transformLatest { media ->
videoLoadingStateFlow.value = VideoLoadingState.Initial // 避免一直显示已取消 (.Cancelled)
playerState.clearVideoSource() // 只要 media 换了就清空
if (media == null) {
return@transformLatest
}

try {
val info = episodeInfo.filterNotNull().first()
videoLoadingStateFlow.value = VideoLoadingState.ResolvingSource
emit(
videoSourceResolver.resolve(
media,
EpisodeMetadata(
title = info.displayName,
ep = info.ep,
sort = info.sort,
),
val source = videoSourceResolver.resolve(
media,
EpisodeMetadata(
title = info.displayName,
ep = info.ep,
sort = info.sort,
),
)
videoLoadingStateFlow.compareAndSet(
VideoLoadingState.ResolvingSource,
VideoLoadingState.DecodingData(isBt = media.kind == MediaSourceKind.BitTorrent),
)
playerState.setVideoSource(source)
logger.info { "playerState.applySourceToPlayer with source = $source" }
videoLoadingStateFlow.value = VideoLoadingState.Succeed(isBt = source is TorrentVideoSource)
} catch (e: UnsupportedMediaException) {
logger.error { IllegalStateException("Failed to resolve video source, unsupported media", e) }
videoLoadingStateFlow.value = VideoLoadingState.UnsupportedMedia
emit(null)
} catch (e: VideoSourceResolutionException) {
logger.error { IllegalStateException("Failed to resolve video source with known error", e) }
playerState.clearVideoSource()
} catch (e: VideoSourceOpenException) { // during playerState.setVideoSource
logger.error {
IllegalStateException(
"Failed to resolve video source due to VideoSourceOpenException",
e,
)
}
videoLoadingStateFlow.value = when (e.reason) {
OpenFailures.NO_MATCHING_FILE -> VideoLoadingState.NoMatchingFile
OpenFailures.UNSUPPORTED_VIDEO_SOURCE -> VideoLoadingState.UnsupportedMedia
OpenFailures.ENGINE_DISABLED -> VideoLoadingState.UnsupportedMedia
}
playerState.clearVideoSource()
} catch (e: VideoSourceResolutionException) { // during videoSourceResolver.resolve
logger.error {
IllegalStateException(
"Failed to resolve video source due to VideoSourceResolutionException",
e,
)
}
videoLoadingStateFlow.value = when (e.reason) {
ResolutionFailures.FETCH_TIMEOUT -> VideoLoadingState.ResolutionTimedOut
ResolutionFailures.ENGINE_ERROR -> VideoLoadingState.UnknownError(e)
ResolutionFailures.NETWORK_ERROR -> VideoLoadingState.NetworkError
}
emit(null)
} catch (e: CancellationException) {
playerState.clearVideoSource()
} catch (e: CancellationException) { // 切换数据源
videoLoadingStateFlow.value = VideoLoadingState.Cancelled
throw e
} catch (e: Throwable) {
logger.error { IllegalStateException("Failed to resolve video source with unknown error", e) }
videoLoadingStateFlow.value = VideoLoadingState.UnknownError(e)
playerState.clearVideoSource()
emit(null)
}
}.shareInBackground(SharingStarted.Lazily)

init {
launchInBackground {
videoSource.collectLatest { source ->
logger.info { "Got new video source: $source, updating playerState" }
try {
playerState.setVideoSource(source)
if (source != null) {
logger.info { "playerState.setVideoSource success" }
videoLoadingStateFlow.value = VideoLoadingState.Succeed(isBt = source is TorrentVideoSource)
}
} catch (e: VideoSourceOpenException) {
videoLoadingStateFlow.value = when (e.reason) {
OpenFailures.NO_MATCHING_FILE -> VideoLoadingState.NoMatchingFile
OpenFailures.UNSUPPORTED_VIDEO_SOURCE -> VideoLoadingState.UnsupportedMedia
OpenFailures.ENGINE_DISABLED -> VideoLoadingState.UnsupportedMedia
}
} catch (_: CancellationException) {
videoLoadingStateFlow.value = VideoLoadingState.Cancelled
// ignore
return@collectLatest
} catch (e: Throwable) {
logger.error(e) { "Failed to set video source" }
videoLoadingStateFlow.value = VideoLoadingState.UnknownError(e)
}
}
}
}.launchIn(backgroundScope)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ interface PlayerState {
* @throws VideoSourceOpenException 当打开失败时抛出, 包含原因
*/
@Throws(VideoSourceOpenException::class, CancellationException::class)
suspend fun setVideoSource(source: VideoSource<*>?)
suspend fun setVideoSource(source: VideoSource<*>)

/**
* 停止播放并清除上次[设置][setVideoSource]的视频源. 之后还可以通过 [setVideoSource] 恢复播放.
*/
suspend fun clearVideoSource()

/**
* Properties of the video being played.
Expand Down Expand Up @@ -257,15 +262,7 @@ abstract class AbstractPlayerState<D : AbstractPlayerState.Data>(
}
}

final override suspend fun setVideoSource(source: VideoSource<*>?) {
if (source == null) {
logger.info { "setVideoSource: Cleaning up player since source is null" }
cleanupPlayer()
this.videoSource.value = null
this.openResource.value = null
return
}

final override suspend fun setVideoSource(source: VideoSource<*>) {
val previousResource = openResource.value
if (source == previousResource?.videoSource) {
return
Expand Down Expand Up @@ -299,6 +296,13 @@ abstract class AbstractPlayerState<D : AbstractPlayerState.Data>(
this.openResource.value = opened
}

final override suspend fun clearVideoSource() {
logger.info { "clearVideoSource: Cleaning up player" }
cleanupPlayer()
this.videoSource.value = null
this.openResource.value = null
}

fun closeVideoSource() {
synchronized(this) {
val value = openResource.value
Expand Down

0 comments on commit 29a0e43

Please sign in to comment.