Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,10 @@ class AudioPlayer {
fun getDuration(): Long {
var duration = 0L

// For some reason the tests tend to call this from outside the Main thread, this
// ensures we are on the main thread to access player.duration
if (Looper.myLooper() == Looper.getMainLooper()) {
try {
duration = player.duration
} else {
Handler(Looper.getMainLooper()).post { duration = player.duration }
} catch (_: UninitializedPropertyAccessException) {
// ignore, 0 will be returned by default
}

return duration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import coil3.compose.AsyncImage
* @param enabled If the image is enabled, defaults to `true`
*/
@Composable
fun FadeInAsyncImage(image: Any, enabled: Boolean = true) {
fun FadeInAsyncImage(image: Any?, enabled: Boolean = true) {
if (image == null) return

var isVisible by rememberSaveable { mutableStateOf(false) }

// This prevents the background from "flashing" due to recompositions (probably)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
Expand Down
17 changes: 10 additions & 7 deletions app/src/main/java/cc/wordview/app/extractor/VideoStream.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,16 @@ class VideoStream : VideoStreamInterface {
return info.audioStreams[0].content
}

override fun getHQThumbnail(): Any {
// we try to return a preloaded and cached version of the
// thumbnail so the animation can be guaranteed to run smoothly,
// if this is not available fallbacking to the URL, even if the
// animation is slightly broken is better than having no image
return ImageCacheManager.getDiskCachedImage("${info.id}-background") ?:
info.thumbnails.last().url
override fun getHQThumbnail(): Any? {
return try {
// we try to return a preloaded and cached version of the
// thumbnail so the animation can be guaranteed to run smoothly,
// if this is not available fallbacking to the URL, even if the
// animation is slightly broken is better than having no image
ImageCacheManager.getDiskCachedImage("${info.id}-background") ?: info.thumbnails.last().url
} catch (e: NoSuchElementException) {
null
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ interface VideoStreamInterface {

fun init(id: String, context: Context)
fun getStreamURL(): String
fun getHQThumbnail(): Any
fun getHQThumbnail(): Any?
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ val HistoryScreen: NavDestination<Unit> by navDestination {
result = it
) {
context.openActivity<PlayerActivity>(
"id" to it.id
"id" to it.id,
"title" to it.title,
"artist" to it.artist,
)
}
Spacer(Modifier.size(16.dp))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ fun LearnTab(
viewedVideo = lastWatchedVideo!!,
onClick = {
context.openActivity<PlayerActivity>(
"id" to lastWatchedVideo!!.id
"id" to lastWatchedVideo!!.id,
"title" to lastWatchedVideo!!.title,
"artist" to lastWatchedVideo!!.artist
)
}
)
Expand Down Expand Up @@ -176,7 +178,9 @@ fun LearnTab(
)
)
context.openActivity<PlayerActivity>(
"id" to it.id
"id" to it.id,
"title" to it.title,
"artist" to it.artist,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,9 @@ val SearchScreen: NavDestination<Unit> by navDestination {
) {
viewModel.saveVideoToHistory(it)
context.openActivity<PlayerActivity>(
"id" to it.id
"id" to it.id,
"title" to it.title,
"artist" to it.artist,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import cc.wordview.app.settings.AppSettings
import cc.wordview.app.ui.activities.WordViewActivity
import cc.wordview.app.ui.activities.player.composables.ErrorScreen
import cc.wordview.app.ui.activities.player.composables.Player
import cc.wordview.app.ui.activities.player.viewmodel.LoadState
import cc.wordview.app.ui.activities.player.viewmodel.Display
import cc.wordview.app.ui.activities.player.viewmodel.PlayerViewModel
import cc.wordview.app.components.ui.OneTimeEffect
import cc.wordview.app.ui.activities.player.viewmodel.PlayerErrorState
Expand All @@ -63,6 +63,12 @@ class PlayerActivity : WordViewActivity() {

val videoId: String = intent.getStringExtra("id")!!

// Because having the title and artist empty seems weird in
// the player we take these as temporary values from the place
// that has opened the player to use while the stream is not ready yet
val title: String = intent.getStringExtra("title")!!
val artist: String = intent.getStringExtra("artist")!!

setOrientationSensorLandscape()
setupWindowInsets()
enableEdgeToEdge()
Expand Down Expand Up @@ -96,21 +102,20 @@ class PlayerActivity : WordViewActivity() {

WordViewTheme(darkTheme = true) {
Scaffold { innerPadding ->
when (uiState.loadState) {
LoadState.READY -> Player(videoId, viewModel, innerPadding)

LoadState.ERROR -> ErrorScreen(viewModel) {
when (uiState.display) {
Display.PLAYER -> Player(
videoId,
viewModel,
title,
artist,
innerPadding
)

Display.ERROR -> ErrorScreen(viewModel) {
Timber.d("Refreshing player")
viewModel.setLoadState(LoadState.LOADING)
viewModel.setDisplay(Display.PLAYER)
start()
}

LoadState.LOADING -> Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(64.dp)
}
}
}
}
Expand All @@ -121,9 +126,14 @@ class PlayerActivity : WordViewActivity() {
override fun onPause() {
super.onPause()

val playerState = viewModel.uiState.value.loadState
// For some reason, in some devices onPause seems to be called
// when starting an activity, at that point the player is not available
if (!viewModel.isReady())
return

val playerState = viewModel.uiState.value.display

if (playerState == LoadState.READY) {
if (playerState == Display.PLAYER) {
val player = viewModel.uiState.value.player
player.pause()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,13 @@ import cc.wordview.app.ui.components.TextCue

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Player(videoId: String, viewModel: PlayerViewModel, innerPadding: PaddingValues) {
fun Player(
videoId: String,
viewModel: PlayerViewModel,
tempTitle: String,
tempArtist: String,
innerPadding: PaddingValues
) {
val currentCue by viewModel.currentCue.collectAsStateWithLifecycle()
val isBuffering by viewModel.isBuffering.collectAsStateWithLifecycle()
val currentPosition by viewModel.currentPosition.collectAsStateWithLifecycle()
Expand All @@ -91,19 +97,39 @@ fun Player(videoId: String, viewModel: PlayerViewModel, innerPadding: PaddingVal
var captionsEnabled by remember { mutableStateOf(true) }
var showSettings by remember { mutableStateOf(false) }

fun back() {
uiState.player.stop()
activity.finish()
}

fun getTitle(): String {
return if (viewModel.isReady()) {
uiState.videoStream.info.name ?: tempTitle
} else {
tempTitle
}
}

fun getUploaderName(): String {
return if (viewModel.isReady()) {
uiState.videoStream.info.getCleanUploaderName()
} else {
tempArtist
}
}

LaunchedEffect(uiState.finalized) {
if (uiState.finalized) {
uiState.player.stop()
}
}

fun back() {
uiState.player.stop()
activity.finish()
LaunchedEffect(viewModel.isReady()) {
if (viewModel.isReady())
uiState.player.togglePlay(playbackSpeed)
}

BackHandler { back() }
OneTimeEffect { uiState.player.togglePlay(playbackSpeed) }

Box(
modifier = Modifier
Expand Down Expand Up @@ -139,7 +165,7 @@ fun Player(videoId: String, viewModel: PlayerViewModel, innerPadding: PaddingVal
}

Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
if (isBuffering) CircularProgressIndicator(64.dp)
if (isBuffering || !viewModel.isReady()) CircularProgressIndicator(64.dp)
}

FadeOutBox(
Expand Down Expand Up @@ -181,11 +207,11 @@ fun Player(videoId: String, viewModel: PlayerViewModel, innerPadding: PaddingVal
}
Column {
Text(
text = uiState.videoStream.info.name,
text = getTitle(),
fontSize = 18.sp
)
Text(
text = uiState.videoStream.info.getCleanUploaderName(),
text = getUploaderName(),
fontSize = 12.sp
)
}
Expand Down Expand Up @@ -237,21 +263,21 @@ fun Player(videoId: String, viewModel: PlayerViewModel, innerPadding: PaddingVal
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
CrossfadeIconButton(
if (viewModel.isReady()) CrossfadeIconButton(
modifier = Modifier.testTag("skip-back"),
icon = Icons.Filled.SkipPrevious,
size = 72.dp,
onClick = { uiState.player.skipBack() }
)
CrossfadeIconButton(
if (viewModel.isReady()) CrossfadeIconButton(
modifier = Modifier
.testTag("toggle-play")
.alpha(if (isBuffering) 0.0f else 1.0f),
icon = uiState.playIcon,
size = 80.dp,
onClick = { uiState.player.togglePlay() }
)
CrossfadeIconButton(
if (viewModel.isReady()) CrossfadeIconButton(
modifier = Modifier.testTag("skip-forward"),
icon = Icons.Filled.SkipNext,
size = 72.dp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

package cc.wordview.app.ui.activities.player.viewmodel

enum class LoadState {
ERROR, LOADING, READY
/**
* What should the player be showing
*/
enum class Display {
ERROR, PLAYER
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ data class PlayerUIState(
val playIcon: ImageVector = Icons.Filled.PlayArrow,
val lyrics: Lyrics = Lyrics("", Parser(Language.ENGLISH)),
val player: AudioPlayer = AudioPlayer(),
val loadState: LoadState = LoadState.LOADING,
val display: Display = Display.PLAYER,
val finalized: Boolean = false,
val videoStream: VideoStream = VideoStream(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ class PlayerViewModel @Inject constructor(
var playerReady: Boolean = false
var imagesReady: Boolean = false

private fun checkReady() {
if (lyricsReady && playerReady && imagesReady)
setLoadState(LoadState.READY)
/**
* If everything needed to reproduce the media is ready
*/
fun isReady(): Boolean {
return lyricsReady && playerReady && imagesReady
}

fun getLyrics(
Expand All @@ -97,7 +99,6 @@ class PlayerViewModel @Inject constructor(
parseLyrics(lyrics)

lyricsReady = true
checkReady()

preloadImages()
}
Expand All @@ -115,7 +116,6 @@ class PlayerViewModel @Inject constructor(
CoroutineScope(Dispatchers.IO).launch {
ImageCacheManager.onQueueCompleted = {
imagesReady = true
checkReady()
}
ImageCacheManager.executeAllInQueue()
}
Expand Down Expand Up @@ -155,10 +155,9 @@ class PlayerViewModel @Inject constructor(
_currentPosition.update { pos.toLong() }
_bufferedPercentage.update { bufferedPercentage }
}
onInitializeFail = { setLoadState(LoadState.ERROR) }
onInitializeFail = { setDisplay(Display.ERROR) }
onPrepared = {
playerReady = true
checkReady()
}

initialize(videoStreamUrl, appContext, listener)
Expand Down Expand Up @@ -197,16 +196,16 @@ class PlayerViewModel @Inject constructor(
_currentCue.update { cue }
}

fun setLoadState(loadState: LoadState) {
_uiState.update { it.copy(loadState = loadState) }
fun setDisplay(display: Display) {
_uiState.update { it.copy(display = display) }
}

/**
* Declares the error that has happened and directions the player to show it
*/
fun declarePlayerError(errorState: PlayerErrorState) {
_errorState.update { errorState }
_uiState.update { it.copy(loadState = LoadState.ERROR) }
_uiState.update { it.copy(display = Display.ERROR) }
}

/**
Expand Down
Loading