feat: Add CustomDefaultTrackNameProvider

This commit is contained in:
tapframe 2026-05-05 12:17:50 +05:30
parent fa7c8068b3
commit 0b824ff32a
3 changed files with 210 additions and 40 deletions

View file

@ -0,0 +1,91 @@
package com.nuvio.app.features.player
import android.content.res.Resources
import androidx.media3.common.Format
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.DefaultTrackNameProvider
@UnstableApi
class CustomDefaultTrackNameProvider(resources: Resources) : DefaultTrackNameProvider(resources) {
override fun getTrackName(format: Format): String {
var trackName = super.getTrackName(format)
if (format.sampleMimeType != null) {
var sampleFormat = formatNameFromMime(format.sampleMimeType)
if (sampleFormat == null) {
sampleFormat = formatNameFromMime(format.codecs)
}
if (sampleFormat == null) {
sampleFormat = format.sampleMimeType
}
if (sampleFormat != null) {
trackName += " ($sampleFormat)"
}
}
if (format.label != null) {
if (!trackName.startsWith(format.label!!)) {
trackName += " - ${format.label}"
}
}
return trackName
}
companion object {
fun formatNameFromMime(mimeType: String?): String? {
if (mimeType == null) return null
return when (mimeType) {
MimeTypes.AUDIO_DTS -> "DTS"
MimeTypes.AUDIO_DTS_HD -> "DTS-HD"
MimeTypes.AUDIO_DTS_EXPRESS -> "DTS Express"
MimeTypes.AUDIO_TRUEHD -> "TrueHD"
MimeTypes.AUDIO_AC3 -> "AC-3"
MimeTypes.AUDIO_E_AC3 -> "E-AC-3"
MimeTypes.AUDIO_E_AC3_JOC -> "E-AC-3-JOC"
MimeTypes.AUDIO_AC4 -> "AC-4"
MimeTypes.AUDIO_AAC -> "AAC"
MimeTypes.AUDIO_MPEG -> "MP3"
MimeTypes.AUDIO_MPEG_L2 -> "MP2"
MimeTypes.AUDIO_VORBIS -> "Vorbis"
MimeTypes.AUDIO_OPUS -> "Opus"
MimeTypes.AUDIO_FLAC -> "FLAC"
MimeTypes.AUDIO_ALAC -> "ALAC"
MimeTypes.AUDIO_WAV -> "WAV"
MimeTypes.AUDIO_AMR -> "AMR"
MimeTypes.AUDIO_AMR_NB -> "AMR-NB"
MimeTypes.AUDIO_AMR_WB -> "AMR-WB"
MimeTypes.AUDIO_IAMF -> "IAMF"
MimeTypes.AUDIO_MPEGH_MHA1 -> "MPEG-H"
MimeTypes.AUDIO_MPEGH_MHM1 -> "MPEG-H"
MimeTypes.VIDEO_H264 -> "AVC"
MimeTypes.VIDEO_H265 -> "HEVC"
MimeTypes.VIDEO_AV1 -> "AV1"
MimeTypes.VIDEO_VP8 -> "VP8"
MimeTypes.VIDEO_VP9 -> "VP9"
MimeTypes.VIDEO_DOLBY_VISION -> "Dolby Vision"
"application/pgs" -> "PGS"
MimeTypes.APPLICATION_SUBRIP -> "SRT"
MimeTypes.TEXT_SSA -> "SSA"
MimeTypes.TEXT_VTT -> "VTT"
MimeTypes.APPLICATION_TTML -> "TTML"
MimeTypes.APPLICATION_TX3G -> "TX3G"
MimeTypes.APPLICATION_DVBSUBS -> "DVB"
else -> null
}
}
fun getChannelLayoutName(channelCount: Int): String? {
return when (channelCount) {
1 -> "Mono"
2 -> "Stereo"
6 -> "5.1"
8 -> "7.1"
else -> if (channelCount > 0) "${channelCount}ch" else null
}
}
}
}

View file

@ -58,7 +58,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.HttpURLConnection
import java.net.URL
import java.util.Locale
private const val TAG = "NuvioPlayer"
@ -298,10 +297,10 @@ actual fun PlatformPlayerSurface(
}
override fun getAudioTracks(): List<AudioTrack> =
exoPlayer.extractAudioTracks()
exoPlayer.extractAudioTracks(context)
override fun getSubtitleTracks(): List<SubtitleTrack> {
val tracks = exoPlayer.extractSubtitleTracks()
val tracks = exoPlayer.extractSubtitleTracks(context)
Log.d(TAG, "getSubtitleTracks: found ${tracks.size} tracks")
tracks.forEach { t ->
Log.d(TAG, " track idx=${t.index} id=${t.id} label='${t.label}' lang=${t.language} selected=${t.isSelected}")
@ -559,47 +558,20 @@ private fun PlayerView.applySubtitleStyle(style: SubtitleStyleState) {
}
}
private fun ExoPlayer.extractAudioTracks(): List<AudioTrack> {
private fun ExoPlayer.extractAudioTracks(context: Context): List<AudioTrack> {
val tracks = mutableListOf<AudioTrack>()
val trackNameProvider = CustomDefaultTrackNameProvider(context.resources)
var idx = 0
for (group in currentTracks.groups) {
if (group.type != C.TRACK_TYPE_AUDIO) continue
val format = group.mediaTrackGroup.getFormat(0)
val channelLabel = when {
format.channelCount == 1 -> "Mono"
format.channelCount == 2 -> "Stereo"
format.channelCount == 6 -> "5.1"
format.channelCount == 8 -> "7.1"
format.channelCount > 0 -> "${format.channelCount}ch"
else -> null
}
val mime = format.sampleMimeType?.lowercase()
val codecLabel = when {
mime == null -> null
mime.contains("eac3-joc") -> "Dolby Atmos"
mime.contains("truehd") && format.channelCount >= 8 -> "Dolby Atmos"
mime.contains("truehd") -> "Dolby TrueHD"
mime.contains("eac3") -> "Dolby Digital Plus"
mime.contains("ac3") -> "Dolby Digital"
mime.contains("opus") -> "Opus"
mime.contains("aac") -> "AAC"
mime.contains("dts-hd") -> "DTS-HD"
mime.contains("dts") -> "DTS"
else -> null
}
val resolvedLanguage = format.language?.let { lang -> Locale(lang).displayLanguage.takeIf { name -> name.isNotBlank() && name != lang } }
val baseName = format.label?.takeIf { it.isNotBlank() }
?: resolvedLanguage
?: format.language
val label = trackNameProvider.getTrackName(format).takeIf { it.isNotBlank() }
?: runBlocking { getString(Res.string.compose_player_track_number, idx + 1) }
val suffix = listOfNotNull(channelLabel, codecLabel)
.joinToString(" ")
.let { if (it.isNotBlank()) " ($it)" else "" }
tracks.add(
AudioTrack(
index = idx,
id = format.id ?: idx.toString(),
label = "$baseName$suffix",
label = label,
language = format.language,
isSelected = group.isSelected,
)
@ -609,8 +581,9 @@ private fun ExoPlayer.extractAudioTracks(): List<AudioTrack> {
return tracks
}
private fun ExoPlayer.extractSubtitleTracks(): List<SubtitleTrack> {
private fun ExoPlayer.extractSubtitleTracks(context: Context): List<SubtitleTrack> {
val tracks = mutableListOf<SubtitleTrack>()
val trackNameProvider = CustomDefaultTrackNameProvider(context.resources)
var idx = 0
for (group in currentTracks.groups) {
if (group.type != C.TRACK_TYPE_TEXT) continue
@ -620,7 +593,7 @@ private fun ExoPlayer.extractSubtitleTracks(): List<SubtitleTrack> {
SubtitleTrack(
index = idx,
id = format.id ?: idx.toString(),
label = format.label ?: "",
label = trackNameProvider.getTrackName(format),
language = format.language,
isSelected = group.isSelected,
isForced = inferForcedSubtitleTrack(

View file

@ -579,15 +579,29 @@ final class MPVPlayerViewController: UIViewController {
for i in 0..<count {
let type = getString("track-list/\(i)/type") ?? ""
let id = getInt("track-list/\(i)/id")
let title = getString("track-list/\(i)/title") ?? ""
let lang = getString("track-list/\(i)/lang") ?? ""
let title = getTrackString(i, "title")
let lang = getTrackString(i, "lang")
let codec = getTrackString(i, "codec")
let decoderDescription = getTrackString(i, "decoder-desc")
let channels = getTrackString(i, "demux-channels")
let channelCount = getInt("track-list/\(i)/demux-channel-count")
let selected = getFlag("track-list/\(i)/selected")
let displayTitle = formatTrackTitle(
type: type,
index: type == "audio" ? audioIdx : subIdx,
title: title,
lang: lang,
codec: codec,
decoderDescription: decoderDescription,
channels: channels,
channelCount: channelCount
)
if type == "audio" {
audio.append(TrackInfo(index: audioIdx, id: id, type: type, title: title, lang: lang, selected: selected))
audio.append(TrackInfo(index: audioIdx, id: id, type: type, title: displayTitle, lang: lang, selected: selected))
audioIdx += 1
} else if type == "sub" {
subs.append(TrackInfo(index: subIdx, id: id, type: type, title: title, lang: lang, selected: selected))
subs.append(TrackInfo(index: subIdx, id: id, type: type, title: displayTitle, lang: lang, selected: selected))
subIdx += 1
}
}
@ -595,6 +609,98 @@ final class MPVPlayerViewController: UIViewController {
subtitleTracks = subs
}
private func getTrackString(_ index: Int, _ field: String) -> String {
(getString("track-list/\(index)/\(field)") ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
}
private func formatTrackTitle(
type: String,
index: Int,
title: String,
lang: String,
codec: String,
decoderDescription: String,
channels: String,
channelCount: Int
) -> String {
let base = ifNotBlank(title)
?? localizedLanguageName(lang)
?? (type == "sub" ? "Subtitle \(index + 1)" : "Track \(index + 1)")
let codecName = codecDisplayName(codec) ?? codecDisplayName(decoderDescription)
let channelName = type == "audio" ? channelLayoutName(channels: channels, channelCount: channelCount) : nil
let details = [channelName, codecName]
.compactMap { $0 }
.filter { detail in !base.localizedCaseInsensitiveContains(detail) }
return details.isEmpty ? base : "\(base) (\(details.joined(separator: ", ")))"
}
private func ifNotBlank(_ value: String) -> String? {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
private func localizedLanguageName(_ languageCode: String) -> String? {
guard let code = ifNotBlank(languageCode) else { return nil }
return Locale.current.localizedString(forLanguageCode: code) ?? code
}
private func channelLayoutName(channels: String, channelCount: Int) -> String? {
if let normalized = ifNotBlank(channels), normalized != "unknown" {
let lower = normalized.lowercased()
if lower == "mono" { return "Mono" }
if lower == "stereo" { return "Stereo" }
return normalized
}
switch channelCount {
case 1:
return "Mono"
case 2:
return "Stereo"
case 6:
return "5.1"
case 8:
return "7.1"
case let count where count > 0:
return "\(count)ch"
default:
return nil
}
}
private func codecDisplayName(_ value: String) -> String? {
guard let raw = ifNotBlank(value) else { return nil }
let codec = raw.lowercased()
if codec.contains("eac3") || codec.contains("e-ac-3") || codec.contains("e ac-3") {
return codec.contains("joc") || codec.contains("atmos") ? "E-AC-3-JOC" : "E-AC-3"
}
if codec.contains("truehd") || codec.contains("true hd") { return "TrueHD" }
if codec.contains("ac3") || codec.contains("ac-3") { return "AC-3" }
if codec.contains("dts-hd") || codec.contains("dtshd") || codec.contains("dts hd") { return "DTS-HD" }
if codec.contains("dts") || codec == "dca" { return "DTS" }
if codec.contains("aac") { return "AAC" }
if codec.contains("mp3") || codec.contains("mpeg audio") { return "MP3" }
if codec.contains("mp2") { return "MP2" }
if codec.contains("opus") { return "Opus" }
if codec.contains("vorbis") { return "Vorbis" }
if codec.contains("flac") { return "FLAC" }
if codec.contains("alac") { return "ALAC" }
if codec.contains("pcm") || codec.contains("wav") { return "WAV" }
if codec.contains("amr_wb") || codec.contains("amr-wb") { return "AMR-WB" }
if codec.contains("amr_nb") || codec.contains("amr-nb") { return "AMR-NB" }
if codec.contains("amr") { return "AMR" }
if codec.contains("iamf") { return "IAMF" }
if codec.contains("mpegh") || codec.contains("mpeg-h") { return "MPEG-H" }
if codec.contains("pgs") || codec.contains("hdmv") { return "PGS" }
if codec.contains("subrip") || codec == "srt" { return "SRT" }
if codec.contains("ass") || codec.contains("ssa") { return "SSA" }
if codec.contains("webvtt") || codec == "vtt" { return "VTT" }
if codec.contains("ttml") { return "TTML" }
if codec.contains("mov_text") || codec.contains("tx3g") { return "TX3G" }
if codec.contains("dvb") { return "DVB" }
return raw
}
private func clearPlaybackError() {
errorStateLock.lock()
recentPlaybackLogs.removeAll(keepingCapacity: true)