mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +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 kotlinx.coroutines.withContext
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
private const val TAG = "NuvioPlayer"
|
private const val TAG = "NuvioPlayer"
|
||||||
|
|
||||||
|
|
@ -298,10 +297,10 @@ actual fun PlatformPlayerSurface(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAudioTracks(): List<AudioTrack> =
|
override fun getAudioTracks(): List<AudioTrack> =
|
||||||
exoPlayer.extractAudioTracks()
|
exoPlayer.extractAudioTracks(context)
|
||||||
|
|
||||||
override fun getSubtitleTracks(): List<SubtitleTrack> {
|
override fun getSubtitleTracks(): List<SubtitleTrack> {
|
||||||
val tracks = exoPlayer.extractSubtitleTracks()
|
val tracks = exoPlayer.extractSubtitleTracks(context)
|
||||||
Log.d(TAG, "getSubtitleTracks: found ${tracks.size} tracks")
|
Log.d(TAG, "getSubtitleTracks: found ${tracks.size} tracks")
|
||||||
tracks.forEach { t ->
|
tracks.forEach { t ->
|
||||||
Log.d(TAG, " track idx=${t.index} id=${t.id} label='${t.label}' lang=${t.language} selected=${t.isSelected}")
|
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 tracks = mutableListOf<AudioTrack>()
|
||||||
|
val trackNameProvider = CustomDefaultTrackNameProvider(context.resources)
|
||||||
var idx = 0
|
var idx = 0
|
||||||
for (group in currentTracks.groups) {
|
for (group in currentTracks.groups) {
|
||||||
if (group.type != C.TRACK_TYPE_AUDIO) continue
|
if (group.type != C.TRACK_TYPE_AUDIO) continue
|
||||||
val format = group.mediaTrackGroup.getFormat(0)
|
val format = group.mediaTrackGroup.getFormat(0)
|
||||||
val channelLabel = when {
|
val label = trackNameProvider.getTrackName(format).takeIf { it.isNotBlank() }
|
||||||
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
|
|
||||||
?: runBlocking { getString(Res.string.compose_player_track_number, idx + 1) }
|
?: runBlocking { getString(Res.string.compose_player_track_number, idx + 1) }
|
||||||
val suffix = listOfNotNull(channelLabel, codecLabel)
|
|
||||||
.joinToString(" ")
|
|
||||||
.let { if (it.isNotBlank()) " ($it)" else "" }
|
|
||||||
tracks.add(
|
tracks.add(
|
||||||
AudioTrack(
|
AudioTrack(
|
||||||
index = idx,
|
index = idx,
|
||||||
id = format.id ?: idx.toString(),
|
id = format.id ?: idx.toString(),
|
||||||
label = "$baseName$suffix",
|
label = label,
|
||||||
language = format.language,
|
language = format.language,
|
||||||
isSelected = group.isSelected,
|
isSelected = group.isSelected,
|
||||||
)
|
)
|
||||||
|
|
@ -609,8 +581,9 @@ private fun ExoPlayer.extractAudioTracks(): List<AudioTrack> {
|
||||||
return tracks
|
return tracks
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ExoPlayer.extractSubtitleTracks(): List<SubtitleTrack> {
|
private fun ExoPlayer.extractSubtitleTracks(context: Context): List<SubtitleTrack> {
|
||||||
val tracks = mutableListOf<SubtitleTrack>()
|
val tracks = mutableListOf<SubtitleTrack>()
|
||||||
|
val trackNameProvider = CustomDefaultTrackNameProvider(context.resources)
|
||||||
var idx = 0
|
var idx = 0
|
||||||
for (group in currentTracks.groups) {
|
for (group in currentTracks.groups) {
|
||||||
if (group.type != C.TRACK_TYPE_TEXT) continue
|
if (group.type != C.TRACK_TYPE_TEXT) continue
|
||||||
|
|
@ -620,7 +593,7 @@ private fun ExoPlayer.extractSubtitleTracks(): List<SubtitleTrack> {
|
||||||
SubtitleTrack(
|
SubtitleTrack(
|
||||||
index = idx,
|
index = idx,
|
||||||
id = format.id ?: idx.toString(),
|
id = format.id ?: idx.toString(),
|
||||||
label = format.label ?: "",
|
label = trackNameProvider.getTrackName(format),
|
||||||
language = format.language,
|
language = format.language,
|
||||||
isSelected = group.isSelected,
|
isSelected = group.isSelected,
|
||||||
isForced = inferForcedSubtitleTrack(
|
isForced = inferForcedSubtitleTrack(
|
||||||
|
|
|
||||||
|
|
@ -579,15 +579,29 @@ final class MPVPlayerViewController: UIViewController {
|
||||||
for i in 0..<count {
|
for i in 0..<count {
|
||||||
let type = getString("track-list/\(i)/type") ?? ""
|
let type = getString("track-list/\(i)/type") ?? ""
|
||||||
let id = getInt("track-list/\(i)/id")
|
let id = getInt("track-list/\(i)/id")
|
||||||
let title = getString("track-list/\(i)/title") ?? ""
|
let title = getTrackString(i, "title")
|
||||||
let lang = getString("track-list/\(i)/lang") ?? ""
|
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 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" {
|
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
|
audioIdx += 1
|
||||||
} else if type == "sub" {
|
} 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
|
subIdx += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -595,6 +609,98 @@ final class MPVPlayerViewController: UIViewController {
|
||||||
subtitleTracks = subs
|
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() {
|
private func clearPlaybackError() {
|
||||||
errorStateLock.lock()
|
errorStateLock.lock()
|
||||||
recentPlaybackLogs.removeAll(keepingCapacity: true)
|
recentPlaybackLogs.removeAll(keepingCapacity: true)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue