Skip to content

Commit c838cc5

Browse files
author
Arvin
committed
fix(iptv): hide unknown quality and show measured playback resolution
1 parent 02e19b9 commit c838cc5

10 files changed

Lines changed: 220 additions & 16 deletions

File tree

app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/ChannelRow.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ fun ChannelRow(
8888
variantCount: Int = 1,
8989
rowHeight: androidx.compose.ui.unit.Dp = LiveDims.EpgRowHeight,
9090
forceFocused: Boolean = false,
91+
displayQuality: Quality = channel.quality,
9192
modifier: Modifier = Modifier,
9293
) {
9394
var focused by remember { mutableStateOf(false) }
@@ -279,7 +280,11 @@ fun ChannelRow(
279280
verticalArrangement = Arrangement.spacedBy(4.dp),
280281
horizontalAlignment = Alignment.End,
281282
) {
282-
SmallPillBadge(if (variantCount > 1) stringResource(R.string.live_label_quality_variants, channel.quality.label, variantCount) else channel.quality.label)
283+
if (displayQuality != Quality.UNKNOWN) {
284+
SmallPillBadge(if (variantCount > 1) stringResource(R.string.live_label_quality_variants, displayQuality.label, variantCount) else displayQuality.label)
285+
} else if (variantCount > 1) {
286+
SmallPillBadge(stringResource(R.string.live_label_sources, variantCount))
287+
}
283288
SmallPillBadge(channel.lang)
284289
}
285290
}

app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/EpgGrid.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ fun EpgGrid(
122122
onRequestNextChannels: () -> Unit = {},
123123
onVisibleChannelRange: (Int, Int) -> Unit = { _, _ -> },
124124
channelColumnWidthOverride: Dp? = null,
125+
playbackQuality: LivePlaybackQuality? = null,
125126
modifier: Modifier = Modifier,
126127
) {
127128
val density = LocalDensity.current
@@ -543,6 +544,7 @@ fun EpgGrid(
543544
// 1. Channel item (fixed width, doesn't scroll horizontally)
544545
ChannelRow(
545546
channel = ch,
547+
displayQuality = ch.displayQuality(playbackQuality),
546548
isActive = ch.id == selectedChannelId || (gridFocused && locallyFocused),
547549
clockTickMillis = clockTickMillis,
548550
nowNext = nowNext[ch.id],

app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/FullscreenGuideOverlay.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,9 @@ private fun FullscreenGuideContent(
328328
)
329329
Row(horizontalArrangement = Arrangement.spacedBy(if (isTouchDevice) 5.dp else 6.dp)) {
330330
GuideChip(stringResource(R.string.live_label_ch, channel.number), LiveColors.FgDim, Color.White.copy(alpha = 0.08f))
331-
GuideChip(channel.quality.label, LiveColors.FgDim, Color.White.copy(alpha = 0.08f))
331+
if (channel.quality != Quality.UNKNOWN) {
332+
GuideChip(channel.quality.label, LiveColors.FgDim, Color.White.copy(alpha = 0.08f))
333+
}
332334
if (catchupSupported) {
333335
GuideChip(stringResource(R.string.live_label_catchup), LiveColors.Bg, LiveColors.Accent)
334336
}

app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/LiveCategory.kt

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ enum class Genre {
1414

1515
/** Picture quality tier derived from channel name / group. */
1616
enum class Quality(val label: String) {
17-
SD("SD"), HD("HD"), FHD("FHD"), K4("4K");
17+
SD("SD"), HD("HD"), FHD("FHD"), K4("4K"), UNKNOWN("");
1818
}
1919

2020
/**
@@ -171,23 +171,30 @@ fun genreFromText(text: String): Genre {
171171
}
172172
}
173173

174-
/** Parse a quality tier out of channel/group name. */
174+
private val SD_QUALITY_TOKEN = Regex("""\b(?:SD|480[PI]?|576[PI]?)\b""")
175+
176+
/** Parse a quality tier out of channel/group name, without guessing missing quality. */
175177
fun qualityFromText(text: String): Quality {
176178
val t = text.uppercase()
177179
return when {
178180
"4K" in t || "UHD" in t || "2160" in t -> Quality.K4
179181
"FHD" in t || "1080" in t -> Quality.FHD
180182
"HD" in t || "720" in t -> Quality.HD
181-
else -> Quality.SD
183+
SD_QUALITY_TOKEN.containsMatchIn(t) -> Quality.SD
184+
else -> Quality.UNKNOWN
182185
}
183186
}
184187

185188
private fun qualityFromLabel(label: String?): Quality? {
186189
val normalized = label?.trim()?.takeIf { it.isNotBlank() } ?: return null
187190
val quality = qualityFromText(normalized)
188-
return if (quality != Quality.SD || normalized.equals("SD", ignoreCase = true)) quality else null
191+
return quality.takeUnless { it == Quality.UNKNOWN }
189192
}
190193

194+
private fun IptvChannel.metadataQuality(): Quality = qualityFromLabel(qualityLabel)
195+
?: qualityFromText(name).takeUnless { it == Quality.UNKNOWN }
196+
?: qualityFromText(group)
197+
191198
/** Extract a 2-letter bucket code from a group/channel name. Accepts ISO
192199
* country codes or language prefixes (EN, JA, …) commonly used in IPTV. */
193200
fun countryFromText(text: String): String? {
@@ -234,9 +241,7 @@ private fun IptvChannel.traits(): ChannelTraits {
234241
?: countryFromText(group)
235242
?: countryFromText(name)
236243
val genre = genreFromText(combined)
237-
val quality = qualityFromLabel(qualityLabel)
238-
?: qualityFromText(name).takeUnless { it == Quality.SD }
239-
?: qualityFromText(group)
244+
val quality = metadataQuality()
240245
val lang = this.language
241246
?.trim()
242247
?.uppercase()
@@ -288,7 +293,7 @@ fun IptvChannel.enrichForFastStartup(number: Int): EnrichedChannel {
288293
?.takeIf { it.length == 2 }
289294
?: rawCountry
290295
?: "EN"
291-
val quality = qualityFromLabel(qualityLabel) ?: qualityFromText(name)
296+
val quality = metadataQuality()
292297
val brand = LiveColors.BrandGeneral
293298
return EnrichedChannel(
294299
source = this,
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.arflix.tv.ui.screens.tv.live
2+
3+
import androidx.media3.common.MediaItem
4+
import androidx.media3.common.Player
5+
import androidx.media3.common.VideoSize
6+
7+
data class LivePlaybackQuality(val channelId: String, val quality: Quality)
8+
9+
internal fun qualityFromVideoSize(width: Int, height: Int): Quality = when {
10+
width <= 0 || height <= 0 -> Quality.UNKNOWN
11+
width >= 3840 || height >= 2160 -> Quality.K4
12+
width >= 1920 || height >= 1080 -> Quality.FHD
13+
width >= 1280 || height >= 720 -> Quality.HD
14+
else -> Quality.SD
15+
}
16+
17+
internal fun EnrichedChannel.displayQuality(playback: LivePlaybackQuality?): Quality =
18+
playback?.takeIf { it.channelId == id && it.quality != Quality.UNKNOWN }?.quality ?: quality
19+
20+
/** Observes only the stream already playing; never probes other provider streams. */
21+
internal class LivePlaybackQualityListener(
22+
private val player: Player,
23+
private val onQuality: (LivePlaybackQuality?) -> Unit,
24+
) : Player.Listener {
25+
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
26+
onQuality(null)
27+
}
28+
29+
override fun onVideoSizeChanged(videoSize: VideoSize) {
30+
update(videoSize)
31+
}
32+
33+
override fun onRenderedFirstFrame() {
34+
// A new channel can have the same dimensions as the previous one.
35+
update(player.videoSize)
36+
}
37+
38+
private fun update(size: VideoSize) {
39+
val channelId = player.currentMediaItem?.mediaId?.takeIf { it.isNotBlank() }
40+
val quality = qualityFromVideoSize(size.width, size.height)
41+
onQuality(
42+
if (channelId != null && quality != Quality.UNKNOWN) LivePlaybackQuality(channelId, quality)
43+
else null
44+
)
45+
}
46+
}

app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/LiveTvEnhancements.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ private fun Quality.rank(): Int = when (this) {
254254
Quality.FHD -> 3
255255
Quality.HD -> 2
256256
Quality.SD -> 1
257+
Quality.UNKNOWN -> 0
257258
}
258259

259260
@OptIn(ExperimentalTvMaterial3Api::class)
@@ -628,7 +629,7 @@ private fun VariantRow(
628629
overflow = TextOverflow.Ellipsis,
629630
)
630631
}
631-
Text(
632+
if (channel.quality != Quality.UNKNOWN) Text(
632633
text = channel.quality.label,
633634
style = LiveType.Badge.copy(color = LiveColors.Fg),
634635
modifier = Modifier

app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/LiveTvScreen.kt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ private fun catchupQualityRank(channel: EnrichedChannel): Int = when (channel.qu
494494
Quality.FHD -> 3
495495
Quality.HD -> 2
496496
Quality.SD -> 1
497+
Quality.UNKNOWN -> 0
497498
}
498499

499500
private fun catchupPlaybackVariant(
@@ -2369,6 +2370,16 @@ fun LiveTvScreen(
23692370

23702371
DisposableEffect(Unit) { onDispose { exoPlayer.release() } }
23712372

2373+
var playbackQuality by remember(exoPlayer) { mutableStateOf<LivePlaybackQuality?>(null) }
2374+
DisposableEffect(exoPlayer) {
2375+
val listener = LivePlaybackQualityListener(exoPlayer) { playbackQuality = it }
2376+
exoPlayer.addListener(listener)
2377+
onDispose { exoPlayer.removeListener(listener) }
2378+
}
2379+
val playingDisplayChannel = remember(playingChannel, playbackQuality) {
2380+
playingChannel?.let { it.copy(quality = it.displayQuality(playbackQuality)) }
2381+
}
2382+
23722383
var playerPositionMs by remember { mutableLongStateOf(0L) }
23732384
var playerDurationMs by remember { mutableLongStateOf(0L) }
23742385
var playerIsPlaying by remember { mutableStateOf(false) }
@@ -2437,6 +2448,7 @@ fun LiveTvScreen(
24372448

24382449
if (!forcePrepare &&
24392450
stream == lastPreparedStreamUrl &&
2451+
exoPlayer.currentMediaItem?.mediaId == playingChannelId.orEmpty() &&
24402452
isHls == lastPreparedIsHls &&
24412453
headers == lastPreparedHeaders &&
24422454
(playingCatchupProgram == null || catchupUrlAnchorOffsetMs == lastPreparedCatchupOffsetMs)
@@ -2449,6 +2461,7 @@ fun LiveTvScreen(
24492461
exoPlayer.clearMediaItems()
24502462
val mediaItem = MediaItem.Builder()
24512463
.setUri(stream)
2464+
.setMediaId(playingChannelId.orEmpty())
24522465
.apply {
24532466
if (isHls) {
24542467
setMimeType(MimeTypes.APPLICATION_M3U8)
@@ -3101,7 +3114,7 @@ fun LiveTvScreen(
31013114
}
31023115
MiniPlayerRow(
31033116
exoPlayer = exoPlayer,
3104-
channel = playingChannel,
3117+
channel = playingDisplayChannel,
31053118
clockTickMillis = guideClockMillis,
31063119
nowNext = currentNowNext,
31073120
onFavoriteToggle = { viewModel.toggleFavoriteChannel(it) },
@@ -3125,6 +3138,7 @@ fun LiveTvScreen(
31253138
)
31263139
EpgGrid(
31273140
channels = filteredChannels,
3141+
playbackQuality = playbackQuality,
31283142
totalChannelCount = selectedCategoryTotalCount,
31293143
clockTickMillis = guideClockMillis,
31303144
nowNext = effectiveGuideNowNext,
@@ -3249,7 +3263,7 @@ fun LiveTvScreen(
32493263
}
32503264
MiniPlayerRow(
32513265
exoPlayer = exoPlayer,
3252-
channel = playingChannel,
3266+
channel = playingDisplayChannel,
32533267
clockTickMillis = guideClockMillis,
32543268
nowNext = currentNowNext,
32553269
onFavoriteToggle = { viewModel.toggleFavoriteChannel(it) },
@@ -3262,6 +3276,7 @@ fun LiveTvScreen(
32623276
)
32633277
EpgGrid(
32643278
channels = filteredChannels,
3279+
playbackQuality = playbackQuality,
32653280
totalChannelCount = selectedCategoryTotalCount,
32663281
clockTickMillis = guideClockMillis,
32673282
nowNext = effectiveGuideNowNext,
@@ -3471,7 +3486,7 @@ fun LiveTvScreen(
34713486
?: selectedCategoryId
34723487

34733488
FullscreenHud(
3474-
channel = playingChannel,
3489+
channel = playingDisplayChannel,
34753490
nowNext = currentNowNext,
34763491
pokeSignal = hudPokeSignal,
34773492
categoryName = categoryTitle,
@@ -3569,7 +3584,7 @@ fun LiveTvScreen(
35693584
}
35703585
FullscreenGuideOverlay(
35713586
visible = isFullScreen && fullscreenGuideOpen,
3572-
channel = guideChannel ?: playingChannel,
3587+
channel = (guideChannel ?: playingChannel)?.let { it.copy(quality = it.displayQuality(playbackQuality)) },
35733588
guide = guideForChannel(guideChannel ?: playingChannel),
35743589
selectedProgram = playingCatchupProgram,
35753590
clockTickMillis = guideClockMillis,

app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/MiniPlayer.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ private fun SourceBadge(count: Int, onOpenVariants: (() -> Unit)?) {
422422
@OptIn(ExperimentalTvMaterial3Api::class)
423423
@Composable
424424
private fun QualityBadge(q: Quality) {
425+
if (q == Quality.UNKNOWN) return
425426
Box(
426427
modifier = Modifier
427428
.clip(RoundedCornerShape(4.dp))

app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/SearchOverlay.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ private fun SearchResultRow(
385385
)
386386
}
387387
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
388-
Box(
388+
if (channel.quality != Quality.UNKNOWN) Box(
389389
modifier = Modifier
390390
.clip(RoundedCornerShape(4.dp))
391391
.background(LiveColors.Panel)

0 commit comments

Comments
 (0)