mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
External player follow up fixes (#1294)
## Description Fixes some issues when using an external player: - Send playback started before launching so the last played date is updated - Only use VLC's position if >0 since it returns 0 when playback completes ### Related issues Follow up to #1256 ### Testing Emulator & nvidia shield ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
0e37084a16
commit
ab5c884072
3 changed files with 76 additions and 50 deletions
|
|
@ -54,6 +54,7 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
||||||
import com.github.damontecres.wholphin.ui.launchDefault
|
import com.github.damontecres.wholphin.ui.launchDefault
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.wholphin.ui.playback.PlayExternalViewModel
|
||||||
import com.github.damontecres.wholphin.ui.showToast
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
import com.github.damontecres.wholphin.ui.util.ProvideLocalClock
|
import com.github.damontecres.wholphin.ui.util.ProvideLocalClock
|
||||||
|
|
@ -82,6 +83,7 @@ import javax.inject.Inject
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private val viewModel: MainActivityViewModel by viewModels()
|
private val viewModel: MainActivityViewModel by viewModels()
|
||||||
|
private val playExternalViewModel: PlayExternalViewModel by viewModels()
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var userPreferencesDataStore: DataStore<AppPreferences>
|
lateinit var userPreferencesDataStore: DataStore<AppPreferences>
|
||||||
|
|
@ -152,12 +154,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (backStackStr != null) {
|
if (backStackStr != null) {
|
||||||
Timber.d("Restoring back stack")
|
Timber.d("Restoring back stack")
|
||||||
var backStack = json.decodeFromString<List<Destination>>(backStackStr)
|
var backStack = json.decodeFromString<List<Destination>>(backStackStr)
|
||||||
if (!savedInstanceState.getBoolean(KEY_EXTERNAL_PLAYER)) {
|
|
||||||
|
if (!playExternalViewModel.launched.value) {
|
||||||
val lastDest = backStack.lastOrNull()
|
val lastDest = backStack.lastOrNull()
|
||||||
if (lastDest is Destination.Playback ||
|
if (lastDest.isPlayback) {
|
||||||
lastDest is Destination.PlaybackList ||
|
|
||||||
lastDest is Destination.Slideshow
|
|
||||||
) {
|
|
||||||
Timber.v("Restoring back stack with playback")
|
Timber.v("Restoring back stack with playback")
|
||||||
backStack = backStack.toMutableList().apply { removeAt(lastIndex) }
|
backStack = backStack.toMutableList().apply { removeAt(lastIndex) }
|
||||||
}
|
}
|
||||||
|
|
@ -271,6 +271,14 @@ class MainActivity : AppCompatActivity() {
|
||||||
super.onRestart()
|
super.onRestart()
|
||||||
Timber.d("onRestart")
|
Timber.d("onRestart")
|
||||||
viewModel.appStart()
|
viewModel.appStart()
|
||||||
|
if (!playExternalViewModel.launched.value) {
|
||||||
|
// If restarting during playback that is not external, go back a page
|
||||||
|
val lastDest = navigationManager.backStack.lastOrNull()
|
||||||
|
if (lastDest.isPlayback) {
|
||||||
|
Timber.v("onRestart: go back from playback")
|
||||||
|
navigationManager.goBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
|
@ -471,3 +479,9 @@ class MainActivityViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val Destination?.isPlayback: Boolean
|
||||||
|
get() =
|
||||||
|
this is Destination.Playback ||
|
||||||
|
this is Destination.PlaybackList ||
|
||||||
|
this is Destination.Slideshow
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,6 @@ class PlaybackLifecycleObserver
|
||||||
private var wasPlaying: Boolean? = null
|
private var wasPlaying: Boolean? = null
|
||||||
|
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
// TODO
|
|
||||||
// val lastDest = navigationManager.backStack.lastOrNull()
|
|
||||||
// if (lastDest is Destination.Playback ||
|
|
||||||
// lastDest is Destination.PlaybackList ||
|
|
||||||
// lastDest is Destination.Slideshow
|
|
||||||
// ) {
|
|
||||||
// navigationManager.goBack()
|
|
||||||
// }
|
|
||||||
wasPlaying = null
|
wasPlaying = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,13 @@ import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
|
@ -33,6 +32,7 @@ import com.github.damontecres.wholphin.services.StreamChoiceService
|
||||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
|
import com.github.damontecres.wholphin.ui.findActivity
|
||||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.launchDefault
|
import com.github.damontecres.wholphin.ui.launchDefault
|
||||||
|
|
@ -40,9 +40,6 @@ import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.preferences.getExternalPlayers
|
import com.github.damontecres.wholphin.ui.preferences.getExternalPlayers
|
||||||
import com.github.damontecres.wholphin.ui.showToast
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
@ -54,17 +51,22 @@ import org.jellyfin.sdk.api.client.extensions.subtitleApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.videosApi
|
import org.jellyfin.sdk.api.client.extensions.videosApi
|
||||||
import org.jellyfin.sdk.model.api.MediaStream
|
import org.jellyfin.sdk.model.api.MediaStream
|
||||||
|
import org.jellyfin.sdk.model.api.PlayMethod
|
||||||
|
import org.jellyfin.sdk.model.api.PlaybackOrder
|
||||||
|
import org.jellyfin.sdk.model.api.PlaybackStartInfo
|
||||||
import org.jellyfin.sdk.model.api.PlaybackStopInfo
|
import org.jellyfin.sdk.model.api.PlaybackStopInfo
|
||||||
|
import org.jellyfin.sdk.model.api.RepeatMode
|
||||||
import org.jellyfin.sdk.model.extensions.inWholeTicks
|
import org.jellyfin.sdk.model.extensions.inWholeTicks
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@HiltViewModel(assistedFactory = PlayExternalViewModel.Factory::class)
|
@HiltViewModel
|
||||||
class PlayExternalViewModel
|
class PlayExternalViewModel
|
||||||
@AssistedInject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val savedStateHandle: SavedStateHandle,
|
private val savedStateHandle: SavedStateHandle,
|
||||||
@param:ApplicationContext private val context: Context,
|
@param:ApplicationContext private val context: Context,
|
||||||
|
|
@ -75,16 +77,13 @@ class PlayExternalViewModel
|
||||||
private val streamChoiceService: StreamChoiceService,
|
private val streamChoiceService: StreamChoiceService,
|
||||||
private val navigationManager: NavigationManager,
|
private val navigationManager: NavigationManager,
|
||||||
private val userPreferencesService: UserPreferencesService,
|
private val userPreferencesService: UserPreferencesService,
|
||||||
@Assisted val destination: Destination,
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@AssistedFactory
|
val launched = savedStateHandle.getMutableStateFlow("launched", false)
|
||||||
interface Factory {
|
|
||||||
fun create(destination: Destination): PlayExternalViewModel
|
|
||||||
}
|
|
||||||
|
|
||||||
val state = MutableStateFlow(PlayExternalState())
|
val state = MutableStateFlow(PlayExternalState())
|
||||||
|
|
||||||
fun init() {
|
fun init(destination: Destination) {
|
||||||
|
Timber.v("init called: %s", destination)
|
||||||
|
state.update { it.copy(loading = LoadingState.Loading) }
|
||||||
viewModelScope.launchDefault {
|
viewModelScope.launchDefault {
|
||||||
val prefs = userPreferencesService.getCurrent()
|
val prefs = userPreferencesService.getCurrent()
|
||||||
val positionMs: Long
|
val positionMs: Long
|
||||||
|
|
@ -252,7 +251,17 @@ class PlayExternalViewModel
|
||||||
putExtra("forcedsrt", subtitleUrls[it])
|
putExtra("forcedsrt", subtitleUrls[it])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
api.playStateApi.reportPlaybackStart(
|
||||||
|
PlaybackStartInfo(
|
||||||
|
canSeek = false,
|
||||||
|
itemId = itemId,
|
||||||
|
isPaused = false,
|
||||||
|
playMethod = PlayMethod.DIRECT_PLAY,
|
||||||
|
repeatMode = RepeatMode.REPEAT_NONE,
|
||||||
|
playbackOrder = PlaybackOrder.DEFAULT,
|
||||||
|
isMuted = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
state.update {
|
state.update {
|
||||||
PlayExternalState(
|
PlayExternalState(
|
||||||
loading = LoadingState.Success,
|
loading = LoadingState.Success,
|
||||||
|
|
@ -278,10 +287,10 @@ class PlayExternalViewModel
|
||||||
return@launchDefault
|
return@launchDefault
|
||||||
}
|
}
|
||||||
Timber.v(
|
Timber.v(
|
||||||
"Result: result=%s, itemId=%s action=%s",
|
"Result: result=%s, action=%s, itemId=%s",
|
||||||
result.resultCode,
|
result.resultCode,
|
||||||
itemId,
|
|
||||||
result.data?.action,
|
result.data?.action,
|
||||||
|
itemId,
|
||||||
)
|
)
|
||||||
if (result.resultCode == Activity.RESULT_OK || result.resultCode == Activity.RESULT_CANCELED ||
|
if (result.resultCode == Activity.RESULT_OK || result.resultCode == Activity.RESULT_CANCELED ||
|
||||||
// Vimu return 1 for video completion
|
// Vimu return 1 for video completion
|
||||||
|
|
@ -295,7 +304,7 @@ class PlayExternalViewModel
|
||||||
position =
|
position =
|
||||||
data
|
data
|
||||||
.getLongExtra("extra_position", Long.MIN_VALUE)
|
.getLongExtra("extra_position", Long.MIN_VALUE)
|
||||||
.takeIf { it >= 0 }
|
.takeIf { it > 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
// mpv-android: https://mpv-android.github.io/mpv-android/intent.html
|
// mpv-android: https://mpv-android.github.io/mpv-android/intent.html
|
||||||
|
|
@ -326,19 +335,27 @@ class PlayExternalViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Timber.v("Result position: %s", position?.milliseconds)
|
Timber.v("Result position: %s", position?.milliseconds)
|
||||||
api.playStateApi.reportPlaybackStopped(
|
if (position != null || result.data?.action != null) {
|
||||||
PlaybackStopInfo(
|
api.playStateApi.reportPlaybackStopped(
|
||||||
itemId = itemId,
|
PlaybackStopInfo(
|
||||||
mediaSourceId = mediaSourceId,
|
itemId = itemId,
|
||||||
positionTicks = position?.milliseconds?.inWholeTicks,
|
mediaSourceId = mediaSourceId,
|
||||||
failed = false,
|
positionTicks = position?.milliseconds?.inWholeTicks,
|
||||||
),
|
failed = false,
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Activity result: %s", result.resultCode)
|
Timber.w(
|
||||||
|
"Activity result: %s, action=%s",
|
||||||
|
result.resultCode,
|
||||||
|
result.data?.action,
|
||||||
|
)
|
||||||
showToast(context, "Unknown result from external player")
|
showToast(context, "Unknown result from external player")
|
||||||
}
|
}
|
||||||
navigationManager.goBack()
|
navigationManager.goBack()
|
||||||
|
state.update { PlayExternalState() }
|
||||||
|
launched.update { false }
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Error during external playback of %s", itemId)
|
Timber.e(ex, "Error during external playback of %s", itemId)
|
||||||
|
|
@ -359,7 +376,7 @@ class PlayExternalViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PlayExternalState(
|
data class PlayExternalState(
|
||||||
val loading: LoadingState = LoadingState.Loading,
|
val loading: LoadingState = LoadingState.Pending,
|
||||||
val intent: Intent = Intent(),
|
val intent: Intent = Intent(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -369,8 +386,8 @@ fun PlayExternalPage(
|
||||||
destination: Destination,
|
destination: Destination,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: PlayExternalViewModel =
|
viewModel: PlayExternalViewModel =
|
||||||
hiltViewModel<PlayExternalViewModel, PlayExternalViewModel.Factory>(
|
hiltViewModel(
|
||||||
creationCallback = { it.create(destination) },
|
viewModelStoreOwner = LocalContext.current.findActivity() as AppCompatActivity,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
val launcher =
|
val launcher =
|
||||||
|
|
@ -380,15 +397,18 @@ fun PlayExternalPage(
|
||||||
)
|
)
|
||||||
|
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
var launched by rememberSaveable { mutableStateOf(false) }
|
val launched by viewModel.launched.collectAsState()
|
||||||
if (!launched) {
|
LaunchedEffect(Unit) {
|
||||||
LaunchedEffect(Unit) {
|
if (!launched) {
|
||||||
viewModel.init()
|
viewModel.init(destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (val l = state.loading) {
|
when (val l = state.loading) {
|
||||||
LoadingState.Pending,
|
LoadingState.Pending -> {
|
||||||
|
LoadingPage(modifier, false)
|
||||||
|
}
|
||||||
|
|
||||||
LoadingState.Loading,
|
LoadingState.Loading,
|
||||||
-> {
|
-> {
|
||||||
LoadingPage(modifier)
|
LoadingPage(modifier)
|
||||||
|
|
@ -403,7 +423,7 @@ fun PlayExternalPage(
|
||||||
if (!launched) {
|
if (!launched) {
|
||||||
LifecycleStartEffect(Unit) {
|
LifecycleStartEffect(Unit) {
|
||||||
Timber.i("Launching external playback")
|
Timber.i("Launching external playback")
|
||||||
launched = true
|
viewModel.launched.update { true }
|
||||||
try {
|
try {
|
||||||
launcher.launch(state.intent)
|
launcher.launch(state.intent)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue