mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 15:01:59 +00:00
feat: Add CustomDefaultTrackNameProvider
This commit is contained in:
parent
fa7c8068b3
commit
0b824ff32a
3 changed files with 210 additions and 40 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue