Skip to content

Commit 0844ec7

Browse files
Fixes #253 external player state management by saving and restoring playback data
1 parent 46f13bd commit 0844ec7

2 files changed

Lines changed: 56 additions & 15 deletions

File tree

app/src/main/java/org/jellyfin/androidtv/ui/browsing/MainActivity.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ class MainActivity : FragmentActivity() {
156156
override fun onResume() {
157157
super.onResume()
158158

159+
// Skip auth check while session is still restoring — onCreate handles it once READY.
160+
// Prevents false bounce to StartupActivity when returning from external player after process death.
161+
if (sessionRepository.state.value != SessionRepositoryState.READY) return
162+
159163
if (!validateAuthentication()) return
160164

161165
applyTheme()

app/src/main/java/org/jellyfin/androidtv/ui/playback/ExternalPlayerActivity.kt

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import org.koin.android.ext.android.inject
3535
import timber.log.Timber
3636
import java.io.File
3737
import java.time.Instant
38+
import java.util.UUID
3839
import kotlin.time.Duration
3940
import kotlin.time.Duration.Companion.milliseconds
4041

@@ -70,6 +71,10 @@ class ExternalPlayerActivity : FragmentActivity() {
7071

7172
// The extra keys used by various video players to read the end position
7273
private val resultPositionExtras = arrayOf(API_MX_RESULT_POSITION, API_VLC_RESULT_POSITION)
74+
75+
private const val STATE_ITEM_ID = "state_item_id"
76+
private const val STATE_MEDIA_SOURCE_ID = "state_media_source_id"
77+
private const val STATE_RUNTIME_TICKS = "state_runtime_ticks"
7378
}
7479

7580
private val videoQueueManager by inject<VideoQueueManager>()
@@ -96,13 +101,37 @@ class ExternalPlayerActivity : FragmentActivity() {
96101

97102
private var currentItem: Pair<BaseItemDto, MediaSourceInfo>? = null
98103

104+
private var savedItemId: UUID? = null
105+
private var savedMediaSourceId: String? = null
106+
private var savedRuntimeTicks: Long? = null
107+
99108
override fun onCreate(savedInstanceState: Bundle?) {
100109
super.onCreate(savedInstanceState)
101110

111+
if (savedInstanceState != null) {
112+
savedItemId = savedInstanceState.getString(STATE_ITEM_ID)?.toUUIDOrNull()
113+
savedMediaSourceId = savedInstanceState.getString(STATE_MEDIA_SOURCE_ID)
114+
savedRuntimeTicks = if (savedInstanceState.containsKey(STATE_RUNTIME_TICKS))
115+
savedInstanceState.getLong(STATE_RUNTIME_TICKS) else null
116+
Timber.i("Restored external player state: itemId=$savedItemId")
117+
return
118+
}
119+
102120
val position = intent.getLongExtra(EXTRA_POSITION, 0).milliseconds
103121
playNext(position)
104122
}
105123

124+
override fun onSaveInstanceState(outState: Bundle) {
125+
super.onSaveInstanceState(outState)
126+
127+
val (item, mediaSource) = currentItem ?: return
128+
outState.putString(STATE_ITEM_ID, item.id.toString())
129+
outState.putString(STATE_MEDIA_SOURCE_ID, mediaSource.id)
130+
(mediaSource.runTimeTicks ?: item.runTimeTicks)?.let { ticks ->
131+
outState.putLong(STATE_RUNTIME_TICKS, ticks)
132+
}
133+
}
134+
106135
private fun playNext(position: Duration = Duration.ZERO) {
107136
val currentPosition = videoQueueManager.getCurrentMediaPosition()
108137
val item = videoQueueManager.getCurrentVideoQueue().getOrNull(currentPosition) ?: return finish()
@@ -184,6 +213,9 @@ class ExternalPlayerActivity : FragmentActivity() {
184213

185214
try {
186215
currentItem = item to mediaSource
216+
savedItemId = item.id
217+
savedMediaSourceId = mediaSource.id
218+
savedRuntimeTicks = mediaSource.runTimeTicks ?: item.runTimeTicks
187219
playVideoLauncher.launch(playIntent)
188220
} catch (_: ActivityNotFoundException) {
189221
Toast.makeText(this, R.string.no_player_message, Toast.LENGTH_LONG).show()
@@ -193,13 +225,6 @@ class ExternalPlayerActivity : FragmentActivity() {
193225

194226

195227
private fun onItemFinished(result: Intent?) {
196-
if (currentItem == null) {
197-
Toast.makeText(this@ExternalPlayerActivity, R.string.video_error_unknown_error, Toast.LENGTH_LONG).show()
198-
finish()
199-
return
200-
}
201-
202-
val (item, mediaSource) = currentItem!!
203228
val extras = result?.extras ?: Bundle.EMPTY
204229

205230
val endPosition = resultPositionExtras.firstNotNullOfOrNull { key ->
@@ -208,16 +233,27 @@ class ExternalPlayerActivity : FragmentActivity() {
208233
else null
209234
}
210235

211-
val runtime = (mediaSource.runTimeTicks ?: item.runTimeTicks)?.ticks
212-
val shouldPlayNext = runtime != null && endPosition != null && endPosition >= (runtime * 0.9)
236+
val itemId = currentItem?.first?.id ?: savedItemId
237+
val mediaSourceId = currentItem?.second?.id ?: savedMediaSourceId
238+
val runtimeTicks = currentItem?.second?.runTimeTicks ?: currentItem?.first?.runTimeTicks ?: savedRuntimeTicks
239+
240+
if (itemId == null || mediaSourceId == null) {
241+
Timber.w("Cannot report playback stop: no item data available")
242+
Toast.makeText(this@ExternalPlayerActivity, R.string.video_error_unknown_error, Toast.LENGTH_LONG).show()
243+
finish()
244+
return
245+
}
246+
247+
val runtime = runtimeTicks?.ticks
248+
val shouldPlayNext = currentItem != null && runtime != null && endPosition != null && endPosition >= (runtime * 0.9)
213249

214250
lifecycleScope.launch {
215251
runCatching {
216252
withContext(Dispatchers.IO) {
217253
api.playStateApi.reportPlaybackStopped(
218254
PlaybackStopInfo(
219-
itemId = item.id,
220-
mediaSourceId = mediaSource.id,
255+
itemId = itemId,
256+
mediaSourceId = mediaSourceId,
221257
positionTicks = endPosition?.inWholeTicks,
222258
failed = false,
223259
)
@@ -228,10 +264,11 @@ class ExternalPlayerActivity : FragmentActivity() {
228264
Toast.makeText(this@ExternalPlayerActivity, R.string.video_error_unknown_error, Toast.LENGTH_LONG).show()
229265
}
230266

231-
dataRefreshService.lastPlayback = Instant.now()
232-
when (item.type) {
233-
BaseItemKind.MOVIE -> dataRefreshService.lastMoviePlayback = Instant.now()
234-
BaseItemKind.EPISODE -> dataRefreshService.lastTvPlayback = Instant.now()
267+
val now = Instant.now()
268+
dataRefreshService.lastPlayback = now
269+
when (currentItem?.first?.type) {
270+
BaseItemKind.MOVIE -> dataRefreshService.lastMoviePlayback = now
271+
BaseItemKind.EPISODE -> dataRefreshService.lastTvPlayback = now
235272
else -> Unit
236273
}
237274

0 commit comments

Comments
 (0)