mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Initial skip segments implementation
This commit is contained in:
parent
165fd80c0e
commit
405faa388e
11 changed files with 262 additions and 9 deletions
|
|
@ -6,6 +6,7 @@ import androidx.annotation.StringRes
|
||||||
import com.github.damontecres.dolphin.R
|
import com.github.damontecres.dolphin.R
|
||||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||||
import com.github.damontecres.dolphin.ui.preferences.PreferenceGroup
|
import com.github.damontecres.dolphin.ui.preferences.PreferenceGroup
|
||||||
|
import com.github.damontecres.dolphin.ui.preferences.PreferenceScreenOption
|
||||||
import com.github.damontecres.dolphin.ui.preferences.PreferenceValidation
|
import com.github.damontecres.dolphin.ui.preferences.PreferenceValidation
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
@ -360,6 +361,77 @@ sealed interface AppPreference<T> {
|
||||||
title = R.string.license_info,
|
title = R.string.license_info,
|
||||||
destination = Destination.License,
|
destination = Destination.License,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val AdvancedSettings =
|
||||||
|
AppDestinationPreference(
|
||||||
|
title = R.string.advanced_settings,
|
||||||
|
destination = Destination.Settings(PreferenceScreenOption.ADVANCED),
|
||||||
|
)
|
||||||
|
|
||||||
|
val SkipIntros =
|
||||||
|
AppChoicePreference<SkipSegmentBehavior>(
|
||||||
|
title = R.string.skip_intro_behavior,
|
||||||
|
defaultValue = SkipSegmentBehavior.ASK_TO_SKIP,
|
||||||
|
getter = { it.playbackPreferences.skipIntros },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updatePlaybackPreferences { skipIntros = value }
|
||||||
|
},
|
||||||
|
displayValues = R.array.skip_behaviors,
|
||||||
|
indexToValue = { SkipSegmentBehavior.forNumber(it) },
|
||||||
|
valueToIndex = { it.number },
|
||||||
|
)
|
||||||
|
|
||||||
|
val SkipOutros =
|
||||||
|
AppChoicePreference<SkipSegmentBehavior>(
|
||||||
|
title = R.string.skip_outro_behavior,
|
||||||
|
defaultValue = SkipSegmentBehavior.ASK_TO_SKIP,
|
||||||
|
getter = { it.playbackPreferences.skipOutros },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updatePlaybackPreferences { skipOutros = value }
|
||||||
|
},
|
||||||
|
displayValues = R.array.skip_behaviors,
|
||||||
|
indexToValue = { SkipSegmentBehavior.forNumber(it) },
|
||||||
|
valueToIndex = { it.number },
|
||||||
|
)
|
||||||
|
|
||||||
|
val SkipCommercials =
|
||||||
|
AppChoicePreference<SkipSegmentBehavior>(
|
||||||
|
title = R.string.skip_comercials_behavior,
|
||||||
|
defaultValue = SkipSegmentBehavior.ASK_TO_SKIP,
|
||||||
|
getter = { it.playbackPreferences.skipCommercials },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updatePlaybackPreferences { skipCommercials = value }
|
||||||
|
},
|
||||||
|
displayValues = R.array.skip_behaviors,
|
||||||
|
indexToValue = { SkipSegmentBehavior.forNumber(it) },
|
||||||
|
valueToIndex = { it.number },
|
||||||
|
)
|
||||||
|
|
||||||
|
val SkipPreviews =
|
||||||
|
AppChoicePreference<SkipSegmentBehavior>(
|
||||||
|
title = R.string.skip_previews_behavior,
|
||||||
|
defaultValue = SkipSegmentBehavior.IGNORE,
|
||||||
|
getter = { it.playbackPreferences.skipPreviews },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updatePlaybackPreferences { skipPreviews = value }
|
||||||
|
},
|
||||||
|
displayValues = R.array.skip_behaviors,
|
||||||
|
indexToValue = { SkipSegmentBehavior.forNumber(it) },
|
||||||
|
valueToIndex = { it.number },
|
||||||
|
)
|
||||||
|
|
||||||
|
val SkipRecaps =
|
||||||
|
AppChoicePreference<SkipSegmentBehavior>(
|
||||||
|
title = R.string.skip_recap_behavior,
|
||||||
|
defaultValue = SkipSegmentBehavior.IGNORE,
|
||||||
|
getter = { it.playbackPreferences.skipRecaps },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updatePlaybackPreferences { skipRecaps = value }
|
||||||
|
},
|
||||||
|
displayValues = R.array.skip_behaviors,
|
||||||
|
indexToValue = { SkipSegmentBehavior.forNumber(it) },
|
||||||
|
valueToIndex = { it.number },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,8 +458,6 @@ val basicPreferences =
|
||||||
AppPreference.AutoPlayNextUp,
|
AppPreference.AutoPlayNextUp,
|
||||||
AppPreference.AutoPlayNextDelay,
|
AppPreference.AutoPlayNextDelay,
|
||||||
AppPreference.SkipBackOnResume,
|
AppPreference.SkipBackOnResume,
|
||||||
AppPreference.MaxBitrate,
|
|
||||||
AppPreference.PlaybackDebugInfo,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PreferenceGroup(
|
PreferenceGroup(
|
||||||
|
|
@ -398,11 +468,33 @@ val basicPreferences =
|
||||||
AppPreference.OssLicenseInfo,
|
AppPreference.OssLicenseInfo,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
PreferenceGroup(
|
||||||
|
title = R.string.more,
|
||||||
|
preferences =
|
||||||
|
listOf(
|
||||||
|
AppPreference.AdvancedSettings,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val uiPreferences = listOf<PreferenceGroup>()
|
val uiPreferences = listOf<PreferenceGroup>()
|
||||||
|
|
||||||
val advancedPreferences = listOf<PreferenceGroup>()
|
val advancedPreferences =
|
||||||
|
listOf(
|
||||||
|
PreferenceGroup(
|
||||||
|
title = R.string.playback,
|
||||||
|
preferences =
|
||||||
|
listOf(
|
||||||
|
AppPreference.SkipIntros,
|
||||||
|
AppPreference.SkipOutros,
|
||||||
|
AppPreference.SkipCommercials,
|
||||||
|
AppPreference.SkipPreviews,
|
||||||
|
AppPreference.SkipRecaps,
|
||||||
|
AppPreference.MaxBitrate,
|
||||||
|
AppPreference.PlaybackDebugInfo,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
data class AppSwitchPreference(
|
data class AppSwitchPreference(
|
||||||
@get:StringRes override val title: Int,
|
@get:StringRes override val title: Int,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,11 @@ class AppPreferencesSerializer
|
||||||
skipBackOnResumeSeconds =
|
skipBackOnResumeSeconds =
|
||||||
AppPreference.SkipBackOnResume.defaultValue.seconds.inWholeMilliseconds
|
AppPreference.SkipBackOnResume.defaultValue.seconds.inWholeMilliseconds
|
||||||
maxBitrate = AppPreference.DEFAULT_BITRATE
|
maxBitrate = AppPreference.DEFAULT_BITRATE
|
||||||
|
skipIntros = AppPreference.SkipIntros.defaultValue
|
||||||
|
skipOutros = AppPreference.SkipOutros.defaultValue
|
||||||
|
skipCommercials = AppPreference.SkipCommercials.defaultValue
|
||||||
|
skipPreviews = AppPreference.SkipPreviews.defaultValue
|
||||||
|
skipRecaps = AppPreference.SkipRecaps.defaultValue
|
||||||
}.build()
|
}.build()
|
||||||
homePagePreferences =
|
homePagePreferences =
|
||||||
HomePagePreferences
|
HomePagePreferences
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ package com.github.damontecres.dolphin.ui.nav
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.ui.detail.series.SeasonEpisode
|
import com.github.damontecres.dolphin.ui.detail.series.SeasonEpisode
|
||||||
|
import com.github.damontecres.dolphin.ui.preferences.PreferenceScreenOption
|
||||||
import com.github.damontecres.dolphin.util.UuidSerializer
|
import com.github.damontecres.dolphin.util.UuidSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
|
|
@ -28,7 +29,9 @@ sealed class Destination(
|
||||||
) : Destination()
|
) : Destination()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Settings : Destination(true)
|
data class Settings(
|
||||||
|
val screen: PreferenceScreenOption,
|
||||||
|
) : Destination(true)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Search : Destination()
|
data object Search : Destination()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import com.github.damontecres.dolphin.ui.detail.series.SeriesOverview
|
||||||
import com.github.damontecres.dolphin.ui.main.HomePage
|
import com.github.damontecres.dolphin.ui.main.HomePage
|
||||||
import com.github.damontecres.dolphin.ui.main.SearchPage
|
import com.github.damontecres.dolphin.ui.main.SearchPage
|
||||||
import com.github.damontecres.dolphin.ui.playback.PlaybackContent
|
import com.github.damontecres.dolphin.ui.playback.PlaybackContent
|
||||||
import com.github.damontecres.dolphin.ui.preferences.PreferenceScreenOption
|
|
||||||
import com.github.damontecres.dolphin.ui.preferences.PreferencesPage
|
import com.github.damontecres.dolphin.ui.preferences.PreferencesPage
|
||||||
import com.github.damontecres.dolphin.ui.setup.SwitchServerContent
|
import com.github.damontecres.dolphin.ui.setup.SwitchServerContent
|
||||||
import com.github.damontecres.dolphin.ui.setup.SwitchUserContent
|
import com.github.damontecres.dolphin.ui.setup.SwitchUserContent
|
||||||
|
|
@ -49,10 +48,10 @@ fun DestinationContent(
|
||||||
Destination.ServerList -> SwitchServerContent(modifier)
|
Destination.ServerList -> SwitchServerContent(modifier)
|
||||||
Destination.UserList -> SwitchUserContent(modifier)
|
Destination.UserList -> SwitchUserContent(modifier)
|
||||||
|
|
||||||
Destination.Settings ->
|
is Destination.Settings ->
|
||||||
PreferencesPage(
|
PreferencesPage(
|
||||||
preferences.appPreferences,
|
preferences.appPreferences,
|
||||||
PreferenceScreenOption.BASIC,
|
destination.screen,
|
||||||
modifier,
|
modifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.FontAwesome
|
import com.github.damontecres.dolphin.ui.FontAwesome
|
||||||
import com.github.damontecres.dolphin.ui.ifElse
|
import com.github.damontecres.dolphin.ui.ifElse
|
||||||
|
import com.github.damontecres.dolphin.ui.preferences.PreferenceScreenOption
|
||||||
import com.github.damontecres.dolphin.ui.spacedByWithFooter
|
import com.github.damontecres.dolphin.ui.spacedByWithFooter
|
||||||
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.dolphin.util.ExceptionHandler
|
import com.github.damontecres.dolphin.util.ExceptionHandler
|
||||||
|
|
@ -218,7 +219,11 @@ fun NavDrawer(
|
||||||
icon = Icons.Default.Settings,
|
icon = Icons.Default.Settings,
|
||||||
selected = false,
|
selected = false,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.navigationManager.navigateTo(Destination.Settings)
|
viewModel.navigationManager.navigateTo(
|
||||||
|
Destination.Settings(
|
||||||
|
PreferenceScreenOption.BASIC,
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,9 @@ import androidx.media3.ui.compose.state.rememberNextButtonState
|
||||||
import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
|
import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
|
||||||
import androidx.media3.ui.compose.state.rememberPresentationState
|
import androidx.media3.ui.compose.state.rememberPresentationState
|
||||||
import androidx.media3.ui.compose.state.rememberPreviousButtonState
|
import androidx.media3.ui.compose.state.rememberPreviousButtonState
|
||||||
|
import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.preferences.skipBackOnResume
|
import com.github.damontecres.dolphin.preferences.skipBackOnResume
|
||||||
|
|
@ -65,6 +67,7 @@ import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.dolphin.util.seasonEpisode
|
import com.github.damontecres.dolphin.util.seasonEpisode
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
|
@ -94,6 +97,7 @@ fun PlaybackContent(
|
||||||
val trickplay by viewModel.trickplay.observeAsState(null)
|
val trickplay by viewModel.trickplay.observeAsState(null)
|
||||||
val chapters by viewModel.chapters.observeAsState(listOf())
|
val chapters by viewModel.chapters.observeAsState(listOf())
|
||||||
val currentPlayback by viewModel.currentPlayback.observeAsState(null)
|
val currentPlayback by viewModel.currentPlayback.observeAsState(null)
|
||||||
|
val currentSegment by viewModel.currentSegment.observeAsState(null)
|
||||||
|
|
||||||
var cues by remember { mutableStateOf<List<Cue>>(listOf()) }
|
var cues by remember { mutableStateOf<List<Cue>>(listOf()) }
|
||||||
var showDebugInfo by remember { mutableStateOf(prefs.showDebugInfo) }
|
var showDebugInfo by remember { mutableStateOf(prefs.showDebugInfo) }
|
||||||
|
|
@ -315,6 +319,29 @@ fun PlaybackContent(
|
||||||
.background(Color.Transparent),
|
.background(Color.Transparent),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
currentSegment != null && !controllerViewState.controlsVisible && skipIndicatorDuration == 0L,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(40.dp)
|
||||||
|
.align(Alignment.BottomEnd),
|
||||||
|
) {
|
||||||
|
currentSegment?.let { segment ->
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
player.seekTo(segment.endTicks.ticks.inWholeMilliseconds)
|
||||||
|
},
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Skip ${segment.type.serialName}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(nextUp != null) {
|
BackHandler(nextUp != null) {
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,13 @@ import androidx.media3.exoplayer.ExoPlayer
|
||||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.data.model.Chapter
|
import com.github.damontecres.dolphin.data.model.Chapter
|
||||||
import com.github.damontecres.dolphin.preferences.AppPreference
|
import com.github.damontecres.dolphin.preferences.AppPreference
|
||||||
|
import com.github.damontecres.dolphin.preferences.SkipSegmentBehavior
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.DefaultItemFields
|
import com.github.damontecres.dolphin.ui.DefaultItemFields
|
||||||
import com.github.damontecres.dolphin.ui.indexOfFirstOrNull
|
import com.github.damontecres.dolphin.ui.indexOfFirstOrNull
|
||||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||||
import com.github.damontecres.dolphin.util.ApiRequestPager
|
import com.github.damontecres.dolphin.util.ApiRequestPager
|
||||||
|
import com.github.damontecres.dolphin.util.EqualityMutableLiveData
|
||||||
import com.github.damontecres.dolphin.util.ExceptionHandler
|
import com.github.damontecres.dolphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.dolphin.util.GetEpisodesRequestHandler
|
import com.github.damontecres.dolphin.util.GetEpisodesRequestHandler
|
||||||
import com.github.damontecres.dolphin.util.TrackActivityPlaybackListener
|
import com.github.damontecres.dolphin.util.TrackActivityPlaybackListener
|
||||||
|
|
@ -33,28 +35,36 @@ import com.github.damontecres.dolphin.util.subtitleMimeTypes
|
||||||
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.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.mediaSegmentsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.trickplayApi
|
import org.jellyfin.sdk.api.client.extensions.trickplayApi
|
||||||
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.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
|
import org.jellyfin.sdk.model.api.MediaSegmentDto
|
||||||
|
import org.jellyfin.sdk.model.api.MediaSegmentType
|
||||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||||
import org.jellyfin.sdk.model.api.SubtitlePlaybackMode
|
import org.jellyfin.sdk.model.api.SubtitlePlaybackMode
|
||||||
import org.jellyfin.sdk.model.api.TrickplayInfo
|
import org.jellyfin.sdk.model.api.TrickplayInfo
|
||||||
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
||||||
|
import org.jellyfin.sdk.model.extensions.inWholeTicks
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
enum class TranscodeType {
|
enum class TranscodeType {
|
||||||
DIRECT_PLAY,
|
DIRECT_PLAY,
|
||||||
|
|
@ -93,17 +103,20 @@ class PlaybackViewModel
|
||||||
val currentPlayback = MutableLiveData<CurrentPlayback?>(null)
|
val currentPlayback = MutableLiveData<CurrentPlayback?>(null)
|
||||||
val trickplay = MutableLiveData<TrickplayInfo?>(null)
|
val trickplay = MutableLiveData<TrickplayInfo?>(null)
|
||||||
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
||||||
|
val currentSegment = EqualityMutableLiveData<MediaSegmentDto?>(null)
|
||||||
|
|
||||||
private lateinit var preferences: UserPreferences
|
private lateinit var preferences: UserPreferences
|
||||||
private lateinit var deviceProfile: DeviceProfile
|
private lateinit var deviceProfile: DeviceProfile
|
||||||
private lateinit var itemId: UUID
|
private lateinit var itemId: UUID
|
||||||
private lateinit var dto: BaseItemDto
|
private lateinit var dto: BaseItemDto
|
||||||
|
private var activityListener: TrackActivityPlaybackListener? = null
|
||||||
|
|
||||||
private val episodes = MutableLiveData<ApiRequestPager<GetEpisodesRequest>>()
|
private val episodes = MutableLiveData<ApiRequestPager<GetEpisodesRequest>>()
|
||||||
private var currentEpisodeIndex = Int.MAX_VALUE
|
private var currentEpisodeIndex = Int.MAX_VALUE
|
||||||
val nextUpEpisode = MutableLiveData<BaseItem?>()
|
val nextUpEpisode = MutableLiveData<BaseItem?>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
addCloseable { this@PlaybackViewModel.activityListener?.release() }
|
||||||
addCloseable { player.release() }
|
addCloseable { player.release() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,10 +244,15 @@ class PlaybackViewModel
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@PlaybackViewModel.audioStreams.value = audioStreams
|
this@PlaybackViewModel.audioStreams.value = audioStreams
|
||||||
this@PlaybackViewModel.subtitleStreams.value = subtitleStreams
|
this@PlaybackViewModel.subtitleStreams.value = subtitleStreams
|
||||||
|
this@PlaybackViewModel.activityListener?.let {
|
||||||
|
it.release()
|
||||||
|
player.removeListener(it)
|
||||||
|
}
|
||||||
val activityListener =
|
val activityListener =
|
||||||
TrackActivityPlaybackListener(api, itemId, player)
|
TrackActivityPlaybackListener(api, itemId, player)
|
||||||
addCloseable { activityListener.release() }
|
|
||||||
player.addListener(activityListener)
|
player.addListener(activityListener)
|
||||||
|
this@PlaybackViewModel.activityListener = activityListener
|
||||||
|
|
||||||
changeStreams(
|
changeStreams(
|
||||||
itemId,
|
itemId,
|
||||||
audioIndex,
|
audioIndex,
|
||||||
|
|
@ -249,6 +267,7 @@ class PlaybackViewModel
|
||||||
if (base.type == BaseItemKind.EPISODE) {
|
if (base.type == BaseItemKind.EPISODE) {
|
||||||
base.seriesId?.let(::getEpisodes)
|
base.seriesId?.let(::getEpisodes)
|
||||||
}
|
}
|
||||||
|
listenForSegments()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -485,6 +504,59 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var segmentJob: Job? = null
|
||||||
|
|
||||||
|
private fun listenForSegments() {
|
||||||
|
segmentJob?.cancel()
|
||||||
|
segmentJob =
|
||||||
|
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
|
val prefs = preferences.appPreferences.playbackPreferences
|
||||||
|
val segments by api.mediaSegmentsApi.getItemSegments(itemId)
|
||||||
|
if (segments.items.isNotEmpty()) {
|
||||||
|
while (isActive) {
|
||||||
|
delay(500L)
|
||||||
|
val currentTicks = player.currentPosition.milliseconds.inWholeTicks
|
||||||
|
val currentSegment =
|
||||||
|
segments.items
|
||||||
|
.firstOrNull {
|
||||||
|
it.type != MediaSegmentType.UNKNOWN && currentTicks >= it.startTicks && currentTicks < it.endTicks
|
||||||
|
}
|
||||||
|
if (currentSegment != null) {
|
||||||
|
val behavior =
|
||||||
|
when (currentSegment.type) {
|
||||||
|
MediaSegmentType.COMMERCIAL -> prefs.skipCommercials
|
||||||
|
MediaSegmentType.PREVIEW -> prefs.skipPreviews
|
||||||
|
MediaSegmentType.RECAP -> prefs.skipRecaps
|
||||||
|
MediaSegmentType.OUTRO -> prefs.skipIntros
|
||||||
|
MediaSegmentType.INTRO -> prefs.skipOutros
|
||||||
|
MediaSegmentType.UNKNOWN -> SkipSegmentBehavior.IGNORE
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
when (behavior) {
|
||||||
|
SkipSegmentBehavior.AUTO_SKIP -> {
|
||||||
|
this@PlaybackViewModel.currentSegment.value = null
|
||||||
|
player.seekTo(currentSegment.endTicks.ticks.inWholeMilliseconds + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
SkipSegmentBehavior.ASK_TO_SKIP -> {
|
||||||
|
this@PlaybackViewModel.currentSegment.value = currentSegment
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
this@PlaybackViewModel.currentSegment.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
this@PlaybackViewModel.currentSegment.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun playUpNextEpisode() {
|
fun playUpNextEpisode() {
|
||||||
nextUpEpisode.value?.let {
|
nextUpEpisode.value?.let {
|
||||||
init(Destination.Playback(it.id, 0, it), deviceProfile, preferences)
|
init(Destination.Playback(it.id, 0, it), deviceProfile, preferences)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.github.damontecres.dolphin.util
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [MutableLiveData] that only notifies observers if the value does not equal the previous value
|
||||||
|
*/
|
||||||
|
class EqualityMutableLiveData<T> : MutableLiveData<T> {
|
||||||
|
constructor() : super()
|
||||||
|
constructor(value: T) : super(value)
|
||||||
|
|
||||||
|
override fun setValue(value: T?) {
|
||||||
|
if (value != getValue()) {
|
||||||
|
super.setValue(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValueNoCheck(value: T?) {
|
||||||
|
super.setValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun postValue(value: T?) {
|
||||||
|
if (value != getValue()) {
|
||||||
|
super.postValue(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,12 @@ syntax = "proto3";
|
||||||
option java_package = "com.github.damontecres.dolphin.preferences";
|
option java_package = "com.github.damontecres.dolphin.preferences";
|
||||||
option java_multiple_files = true;
|
option java_multiple_files = true;
|
||||||
|
|
||||||
|
enum SkipSegmentBehavior{
|
||||||
|
IGNORE = 0;
|
||||||
|
AUTO_SKIP = 1;
|
||||||
|
ASK_TO_SKIP = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message PlaybackPreferences {
|
message PlaybackPreferences {
|
||||||
int64 skip_forward_ms = 1;
|
int64 skip_forward_ms = 1;
|
||||||
int64 skip_back_ms = 2;
|
int64 skip_back_ms = 2;
|
||||||
|
|
@ -13,6 +19,12 @@ message PlaybackPreferences {
|
||||||
int64 auto_play_next_delay_seconds = 7;
|
int64 auto_play_next_delay_seconds = 7;
|
||||||
int64 skip_back_on_resume_seconds = 8;
|
int64 skip_back_on_resume_seconds = 8;
|
||||||
int64 max_bitrate = 9;
|
int64 max_bitrate = 9;
|
||||||
|
|
||||||
|
SkipSegmentBehavior skip_intros = 10;
|
||||||
|
SkipSegmentBehavior skip_outros = 11;
|
||||||
|
SkipSegmentBehavior skip_commercials = 12;
|
||||||
|
SkipSegmentBehavior skip_recaps = 13;
|
||||||
|
SkipSegmentBehavior skip_previews = 14;
|
||||||
}
|
}
|
||||||
|
|
||||||
message HomePagePreferences{
|
message HomePagePreferences{
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,10 @@
|
||||||
<item>Orange</item>
|
<item>Orange</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="skip_behaviors">
|
||||||
|
<item>Ignore</item>
|
||||||
|
<item>Skip automatically</item>
|
||||||
|
<item>Ask to skip</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -63,5 +63,10 @@
|
||||||
<string name="director">Director</string>
|
<string name="director">Director</string>
|
||||||
<string name="max_bitrate">Max bitrate</string>
|
<string name="max_bitrate">Max bitrate</string>
|
||||||
<string name="app_theme">Application Theme</string>
|
<string name="app_theme">Application Theme</string>
|
||||||
|
<string name="skip_recap_behavior">Skip recap behavior</string>
|
||||||
|
<string name="skip_previews_behavior">Skip previews behavior</string>
|
||||||
|
<string name="skip_comercials_behavior">Skip commercials behavior</string>
|
||||||
|
<string name="skip_outro_behavior">Skip outro behavior</string>
|
||||||
|
<string name="skip_intro_behavior">Skip intro behavior</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue