This commit is contained in:
Damontecres 2025-09-23 20:03:25 -04:00
parent bd75539cb6
commit 913334b43d
No known key found for this signature in database
24 changed files with 41 additions and 117 deletions

View file

@ -0,0 +1,56 @@
package com.github.damontecres.dolphin.ui
import androidx.compose.runtime.Composable
import coil3.ImageLoader
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.setSingletonImageLoaderFactory
import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.network.cachecontrol.CacheControlCacheStrategy
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.crossfade
import coil3.util.DebugLogger
import com.github.damontecres.dolphin.data.ServerRepository
import okhttp3.Call
import okhttp3.OkHttpClient
import kotlin.time.ExperimentalTime
@OptIn(ExperimentalTime::class, ExperimentalCoilApi::class)
@Composable
fun CoilConfig(
serverRepository: ServerRepository,
okHttpClient: OkHttpClient,
debugLogging: Boolean,
) {
setSingletonImageLoaderFactory { ctx ->
ImageLoader
.Builder(ctx)
.diskCache(
DiskCache
.Builder()
.directory(ctx.cacheDir.resolve("coil3_image_cache"))
.maxSizeBytes(100L * 1024 * 1024)
.build(),
).crossfade(true)
.logger(if (debugLogging) DebugLogger() else null)
.components {
add(
OkHttpNetworkFetcherFactory(
cacheStrategy = { CacheControlCacheStrategy() },
callFactory = {
Call.Factory { request ->
// Ref: https://gist.github.com/nielsvanvelzen/ea047d9028f676185832e51ffaf12a6f
val token = serverRepository.currentUser?.accessToken
okHttpClient.newCall(
request
.newBuilder()
.addHeader("Authorization", "MediaBrowser Token=\"$token\"")
.build(),
)
}
},
),
)
}.build()
}
}

View file

@ -0,0 +1,124 @@
package com.github.damontecres.dolphin.ui
import android.view.KeyEvent
import androidx.compose.foundation.MarqueeAnimationMode
import androidx.compose.foundation.basicMarquee
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.focus.FocusRequester
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import timber.log.Timber
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
fun CharSequence?.isNotNullOrBlank(): Boolean {
contract {
returns(true) implies (this@isNotNullOrBlank != null)
}
return !this.isNullOrBlank()
}
/**
* Try to call [FocusRequester.requestFocus], but catch & log the exception if something is not configured properly
*/
fun FocusRequester.tryRequestFocus(): Boolean =
try {
requestFocus()
true
} catch (ex: IllegalStateException) {
Timber.w(ex, "Failed to request focus")
false
}
/**
* Used to apply modifiers conditionally.
*/
fun Modifier.ifElse(
condition: () -> Boolean,
ifTrueModifier: Modifier,
ifFalseModifier: Modifier = Modifier,
): Modifier = then(if (condition()) ifTrueModifier else ifFalseModifier)
/**
* Used to apply modifiers conditionally.
*/
fun Modifier.ifElse(
condition: Boolean,
ifTrueModifier: Modifier,
ifFalseModifier: Modifier = Modifier,
): Modifier = ifElse({ condition }, ifTrueModifier, ifFalseModifier)
/**
* Handles horizontal (Left & Right) D-Pad Keys and consumes the event(s) so that the focus doesn't
* accidentally move to another element.
* */
fun Modifier.handleDPadKeyEvents(
onLeft: (() -> Unit)? = null,
onRight: (() -> Unit)? = null,
onCenter: (() -> Unit)? = null,
triggerOnAction: Int = KeyEvent.ACTION_UP,
) = onPreviewKeyEvent {
fun onActionUp(block: () -> Unit) {
if (it.nativeKeyEvent.action == triggerOnAction) block()
}
when (it.nativeKeyEvent.keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT -> {
onLeft?.apply {
onActionUp(::invoke)
return@onPreviewKeyEvent true
}
}
KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT -> {
onRight?.apply {
onActionUp(::invoke)
return@onPreviewKeyEvent true
}
}
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> {
onCenter?.apply {
onActionUp(::invoke)
return@onPreviewKeyEvent true
}
}
}
false
}
/**
* Run a [LaunchedEffect] exactly once even with multiple recompositions.
*
* If the composition is removed from the navigation back stack and "re-added", this will run again
*/
@Composable
fun OneTimeLaunchedEffect(runOnceBlock: suspend CoroutineScope.() -> Unit) {
var hasRun by rememberSaveable { mutableStateOf(false) }
if (!hasRun) {
LaunchedEffect(Unit) {
hasRun = true
runOnceBlock.invoke(this)
}
}
}
fun Modifier.enableMarquee(focused: Boolean) =
if (focused) {
basicMarquee(
initialDelayMillis = 250,
animationMode = MarqueeAnimationMode.Immediately,
velocity = 40.dp,
)
} else {
basicMarquee(animationMode = MarqueeAnimationMode.WhileFocused)
}

View file

@ -1,31 +0,0 @@
package com.github.damontecres.dolphin.ui.cards
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.data.model.Library
import com.github.damontecres.dolphin.data.model.Video
import org.jellyfin.sdk.model.api.BaseItemDto
@Composable
fun DolphinCard(
item: Any?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
when (item) {
is Video ->
VideoCard(
item = item,
onClick = onClick,
modifier = modifier,
)
is Library -> TODO()
is BaseItemDto -> {
Text(item.id.toString())
}
null -> NullCard(modifier = modifier)
}
}

View file

@ -41,8 +41,8 @@ import androidx.tv.material3.Text
import coil3.compose.AsyncImage
import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.enableMarquee
import com.github.damontecres.dolphin.ui.FontAwesome
import com.github.damontecres.dolphin.ui.enableMarquee
import kotlinx.coroutines.delay
import org.jellyfin.sdk.model.api.BaseItemKind

View file

@ -1,48 +0,0 @@
package com.github.damontecres.dolphin.ui.cards
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Card
import androidx.tv.material3.Text
import coil3.compose.AsyncImage
import com.github.damontecres.dolphin.data.model.Video
@Composable
fun VideoCard(
item: Video,
onClick: () -> Unit,
modifier: Modifier = Modifier,
cardWidth: Dp = 150.dp,
cardHeight: Dp = 200.dp,
) {
Card(
modifier = modifier,
onClick = onClick,
) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.width(cardWidth),
) {
AsyncImage(
model = item.imageUrl,
contentDescription = item.name,
contentScale = ContentScale.Fit,
modifier =
Modifier
.fillMaxWidth()
.height(cardHeight),
)
Text(
text = item.name ?: "",
)
}
}
}

View file

@ -42,16 +42,16 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.ifElse
import com.github.damontecres.dolphin.tryRequestFocus
import com.github.damontecres.dolphin.ui.AppColors
import com.github.damontecres.dolphin.ui.FontAwesome
import com.github.damontecres.dolphin.ui.cards.ItemCard
import com.github.damontecres.dolphin.ui.ifElse
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.ui.playback.isBackwardButton
import com.github.damontecres.dolphin.ui.playback.isForwardButton
import com.github.damontecres.dolphin.ui.playback.isPlayKeyUp
import com.github.damontecres.dolphin.util.DolphinPager
import com.github.damontecres.dolphin.ui.tryRequestFocus
import com.github.damontecres.dolphin.util.ItemPager
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.math.max
@ -60,7 +60,7 @@ private const val DEBUG = false
@Composable
fun CardGrid(
pager: DolphinPager,
pager: ItemPager,
itemOnClick: (BaseItem) -> Unit,
longClicker: (BaseItem) -> Unit,
letterPosition: suspend (Char) -> Int,

View file

@ -10,13 +10,13 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.OneTimeLaunchedEffect
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.data.model.Library
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.util.DolphinPager
import com.github.damontecres.dolphin.util.ItemPager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@ -37,7 +37,7 @@ class CollectionFolderViewModel
constructor(
api: ApiClient,
) : ItemViewModel<Library>(api) {
val pager = MutableLiveData<DolphinPager?>()
val pager = MutableLiveData<ItemPager?>()
override fun init(
itemId: UUID,
@ -63,7 +63,7 @@ class CollectionFolderViewModel
sortOrder = listOf(SortOrder.ASCENDING),
fields = listOf(ItemFields.PRIMARY_IMAGE_ASPECT_RATIO),
)
val newPager = DolphinPager(api, request, viewModelScope)
val newPager = ItemPager(api, request, viewModelScope)
newPager.init()
pager.value = newPager
}
@ -124,7 +124,7 @@ fun TVShowCollectionDetails(
navigationManager: NavigationManager,
library: Library,
item: BaseItem,
pager: DolphinPager,
pager: ItemPager,
modifier: Modifier = Modifier,
) {
val gridFocusRequester = remember { FocusRequester() }

View file

@ -17,7 +17,7 @@ import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.cards.ItemRow
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.util.DolphinPager
import com.github.damontecres.dolphin.util.ItemPager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@ -54,7 +54,8 @@ class SeasonViewModel
sortOrder = listOf(SortOrder.ASCENDING),
fields = listOf(ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, ItemFields.CHILD_COUNT),
)
val pager = DolphinPager(api, request, viewModelScope, itemCount = item.data.childCount)
val pager =
ItemPager(api, request, viewModelScope, itemCount = item.data.childCount)
pager.init()
episodes.value = pager
}

View file

@ -17,7 +17,7 @@ import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.cards.ItemRow
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.util.DolphinPager
import com.github.damontecres.dolphin.util.ItemPager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@ -59,7 +59,8 @@ class SeriesViewModel
ItemFields.SEASON_USER_DATA,
),
)
val pager = DolphinPager(api, request, viewModelScope, itemCount = item.data.childCount)
val pager =
ItemPager(api, request, viewModelScope, itemCount = item.data.childCount)
pager.init()
seasons.value = pager
}

View file

@ -17,10 +17,10 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.isNotNullOrBlank
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.DefaultItemFields
import com.github.damontecres.dolphin.ui.cards.ItemRow
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import dagger.hilt.android.lifecycle.HiltViewModel

View file

@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.debounce
class ControllerViewState internal constructor(
@IntRange(from = 0)
@param:IntRange(from = 0)
private val hideMilliseconds: Int,
val controlsEnabled: Boolean,
) {
@ -31,7 +31,6 @@ class ControllerViewState internal constructor(
}
fun pulseControls(milliseconds: Int = hideMilliseconds) {
// Log.i("PlaybackPageContent", "pulseControls=$milliseconds")
channel.trySend(milliseconds)
}
@ -41,7 +40,6 @@ class ControllerViewState internal constructor(
.consumeAsFlow()
.debounce { it.toLong() }
.collect {
// Log.i("PlaybackPageContent", "collect")
_controlsVisible = false
}
}

View file

@ -45,9 +45,9 @@ import androidx.media3.ui.compose.state.rememberPresentationState
import androidx.media3.ui.compose.state.rememberPreviousButtonState
import androidx.tv.material3.MaterialTheme
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.tryRequestFocus
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.ui.tryRequestFocus
import com.github.damontecres.stashapp.ui.components.playback.SkipIndicator
import com.github.damontecres.stashapp.ui.components.playback.rememberSeekBarState
import org.jellyfin.sdk.model.api.DeviceProfile

View file

@ -57,8 +57,9 @@ import androidx.tv.material3.ListItem
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.tryRequestFocus
import com.github.damontecres.dolphin.ui.AppColors
import com.github.damontecres.dolphin.ui.tryRequestFocus
import com.github.damontecres.dolphin.util.TrackSupport
import com.github.damontecres.stashapp.ui.components.playback.SeekBarImpl
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.media3.common.Player
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.util.TrackSupport
@Composable
fun PlaybackOverlay(

View file

@ -9,6 +9,7 @@ import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.util.TrackActivityPlaybackListener
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers

View file

@ -41,7 +41,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme
import com.github.damontecres.dolphin.handleDPadKeyEvents
import com.github.damontecres.dolphin.ui.handleDPadKeyEvents
import com.github.damontecres.dolphin.ui.playback.ControllerViewState
@Composable

View file

@ -22,7 +22,7 @@ import androidx.compose.ui.unit.sp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.ifElse
import com.github.damontecres.dolphin.ui.ifElse
import kotlin.math.abs
@Composable

View file

@ -1,133 +0,0 @@
package com.github.damontecres.dolphin.ui.playback
import androidx.annotation.OptIn
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.playStateApi
import org.jellyfin.sdk.model.api.PlayMethod
import org.jellyfin.sdk.model.api.PlaybackOrder
import org.jellyfin.sdk.model.api.PlaybackProgressInfo
import org.jellyfin.sdk.model.api.PlaybackStopInfo
import org.jellyfin.sdk.model.api.RepeatMode
import org.jellyfin.sdk.model.extensions.inWholeTicks
import timber.log.Timber
import java.util.Timer
import java.util.TimerTask
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/**
* Listens to playback and periodically saves playback activity to the server
*/
@OptIn(UnstableApi::class)
class TrackActivityPlaybackListener(
private val api: ApiClient,
private val itemId: UUID,
private val player: Player,
) : Player.Listener {
private val coroutineScope = CoroutineScope(Dispatchers.Main)
private val task: TimerTask
private var totalPlayDurationMilliseconds = AtomicLong(0)
private var currentDurationMilliseconds = AtomicLong(0)
private var isPlaying = AtomicBoolean(false)
private var incrementedPlayCount = AtomicBoolean(false)
init {
val saveActivityInterval = 10.seconds.inWholeMilliseconds
val delay = 1.seconds.inWholeMilliseconds
// Every x seconds, check if the video is playing
task =
object : TimerTask() {
private var timestamp = System.currentTimeMillis()
override fun run() {
try {
val now = System.currentTimeMillis()
if (isPlaying.get()) {
val diffTime = now - timestamp
// If it is playing, add the interval to currently tracked duration
val current = currentDurationMilliseconds.addAndGet(diffTime)
// TODO currentDuration.getAndUpdate would be better, but requires API 24+
if (current >= saveActivityInterval) {
// If the accumulated currently tracked duration > threshold, reset it and save activity
totalPlayDurationMilliseconds.addAndGet(current)
saveActivity(-1L)
}
}
timestamp = now
} catch (ex: Exception) {
Timber.w(ex, "Exception during track activity timer")
}
}
}
TIMER.schedule(task, delay, delay)
}
fun release() {
task.cancel()
TIMER.purge()
coroutineScope.launch {
api.playStateApi.reportPlaybackStopped(
PlaybackStopInfo(
itemId = itemId,
positionTicks = player.currentPosition.milliseconds.inWholeTicks,
failed = false,
),
)
}
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
this.isPlaying.set(isPlaying)
if (!isPlaying) {
val diff = currentDurationMilliseconds.getAndSet(0)
if (diff > 0) {
totalPlayDurationMilliseconds.addAndGet(diff)
saveActivity(-1)
}
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == Player.STATE_ENDED) {
Timber.v("onPlaybackStateChanged STATE_ENDED")
// TODO mark as watched
}
}
private fun saveActivity(position: Long) {
coroutineScope.launch {
val totalDuration = totalPlayDurationMilliseconds.get()
val calcPosition =
(if (position >= 0) position else player.currentPosition).milliseconds
Timber.v("saveActivity: itemId=$itemId, pos=$calcPosition")
// TODO parameters
api.playStateApi.reportPlaybackProgress(
PlaybackProgressInfo(
itemId = itemId,
positionTicks = calcPosition.inWholeTicks,
canSeek = true,
isPaused = !player.isPlaying,
isMuted = false,
playMethod = PlayMethod.DIRECT_PLAY,
repeatMode = RepeatMode.REPEAT_NONE,
playbackOrder = PlaybackOrder.DEFAULT,
),
)
}
}
companion object {
private const val TAG = "TrackActivityPlaybackListener"
private val TIMER by lazy { Timer("$TAG-timer", true) }
}
}

View file

@ -1,147 +0,0 @@
package com.github.damontecres.dolphin.ui.playback
import android.content.Context
import androidx.annotation.OptIn
import androidx.media3.common.C
import androidx.media3.common.Format
import androidx.media3.common.MimeTypes
import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi
import timber.log.Timber
import java.util.Locale
data class TrackSupport(
val id: String?,
val type: TrackType,
val supported: TrackSupportReason,
val selected: Boolean,
val labels: List<String>,
val codecs: String?,
val format: Format,
) {
@OptIn(UnstableApi::class)
fun displayString(context: Context): String =
if (labels.isNotEmpty()) {
labels.joinToString(", ")
} else {
val type =
when (codecs) {
MimeTypes.TEXT_VTT -> "vtt"
MimeTypes.APPLICATION_VOBSUB -> "vobsub"
MimeTypes.APPLICATION_SUBRIP -> "srt"
MimeTypes.TEXT_SSA -> "ssa"
MimeTypes.APPLICATION_PGS -> "pgs"
MimeTypes.APPLICATION_DVBSUBS -> "dvd"
MimeTypes.APPLICATION_TTML -> "ttml"
MimeTypes.TEXT_UNKNOWN -> "unknown"
null -> "unknown"
else -> {
val split = codecs.split("/")
if (split.size > 1) split[1] else codecs
}
}
val language = languageName(context, format.language)
"$language ($type)"
}
}
enum class TrackSupportReason {
HANDLED,
EXCEEDS_CAPABILITIES,
UNSUPPORTED_DRM,
UNSUPPORTED_SUBTYPE,
UNSUPPORTED_TYPE,
UNKNOWN,
;
companion object {
@OptIn(UnstableApi::class)
fun fromInt(
@C.FormatSupport value: Int,
): TrackSupportReason =
when (value) {
C.FORMAT_HANDLED -> HANDLED
C.FORMAT_EXCEEDS_CAPABILITIES -> EXCEEDS_CAPABILITIES
C.FORMAT_UNSUPPORTED_DRM -> UNSUPPORTED_DRM
C.FORMAT_UNSUPPORTED_SUBTYPE -> UNSUPPORTED_SUBTYPE
C.FORMAT_UNSUPPORTED_TYPE -> UNSUPPORTED_TYPE
else -> UNKNOWN
}
}
}
enum class TrackType {
UNKNOWN,
DEFAULT,
AUDIO,
VIDEO,
TEXT,
IMAGE,
METADATA,
CAMERA_MOTION,
NONE,
;
companion object {
@OptIn(UnstableApi::class)
fun fromInt(value: Int): TrackType =
when (value) {
C.TRACK_TYPE_UNKNOWN -> UNKNOWN
C.TRACK_TYPE_DEFAULT -> DEFAULT
C.TRACK_TYPE_AUDIO -> AUDIO
C.TRACK_TYPE_VIDEO -> VIDEO
C.TRACK_TYPE_TEXT -> TEXT
C.TRACK_TYPE_IMAGE -> IMAGE
C.TRACK_TYPE_METADATA -> METADATA
C.TRACK_TYPE_CAMERA_MOTION -> CAMERA_MOTION
C.TRACK_TYPE_NONE -> NONE
else -> UNKNOWN
}
}
}
@OptIn(UnstableApi::class)
fun checkForSupport(tracks: Tracks): List<TrackSupport> =
tracks.groups.flatMap {
buildList {
val type = TrackType.fromInt(it.type)
for (i in 0..<it.length) {
val format = it.getTrackFormat(i)
val labels =
format.labels
.map {
if (it.language != null) {
"${it.value} (${it.language})"
} else {
it.value
}
}
val reason = TrackSupportReason.fromInt(it.getTrackSupport(i))
add(
TrackSupport(
format.id,
type,
reason,
it.isSelected,
labels,
format.codecs,
format,
),
)
}
}
}
fun languageName(
context: Context,
code: String?,
) = if (code != null && code != "00") {
try {
Locale(code).displayLanguage
} catch (ex: Exception) {
Timber.w(ex, "Error in locale for '$code'")
code.uppercase()
}
} else {
"Unknown"
}