sound 播放逻辑+1

This commit is contained in:
renhaoting 2025-11-06 14:46:07 +08:00
parent 1eb8b02ef0
commit 2d967c52a0
5 changed files with 118 additions and 350 deletions

View File

@ -14,6 +14,7 @@ data class ChatSound(
val nameLanguage: String = "",
val headPortrait: String = "",
var isSelected: Boolean = false,
var isPlaying: Boolean = false,
// Other needed
var sampleUrl: String = ""

View File

@ -5,31 +5,32 @@ import android.animation.Animator
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.net.Uri
import android.os.Environment
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.LinearLayout
import android.widget.Toast
import com.remax.visualnovel.R
import com.remax.visualnovel.configs.NovelApplication
import com.remax.visualnovel.databinding.LayoutExpandSelectViewBinding
import com.remax.visualnovel.entity.response.ChatSound
import com.remax.visualnovel.utils.media.AudioPlayableView
import java.io.File
import com.remax.visualnovel.utils.media.AudioPlayListener
import com.remax.visualnovel.utils.media.AudioPlayState
import com.remax.visualnovel.utils.media.GlobalAudioPlayerManager
class ExpandSoundSelectView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AudioPlayableView(context, attrs, defStyleAttr) {
) : LinearLayout(context, attrs, defStyleAttr) {
private lateinit var mBinding: LayoutExpandSelectViewBinding
private lateinit var mExpandView : ExpandSoundSubView
private var isExpanded = false
private var animationDuration = 300
private val globalAudioPlayer = GlobalAudioPlayerManager.getInstance()
private var mSampleIsPlaying = false
private lateinit var mEventListener: IEventListener
interface IEventListener {
@ -161,87 +162,37 @@ class ExpandSoundSelectView @JvmOverloads constructor(
private fun playSoundSample(chatSound: ChatSound) {
if (audioPlayer.isPlaying()) {
pauseAudio()
if (chatSound.sampleUrl.isEmpty()) {
// return
}
if (!chatSound.sampleUrl.isEmpty()) {
playAudio( chatSound.sampleUrl)
} else {
playAudio(NovelApplication.appContext().getExternalFilesDir(null)?.path + "/ringtone.mp3")
with (globalAudioPlayer) {
globalAudioPlayer.play(NovelApplication.appContext().getExternalFilesDir(null)?.path + "/ringtone.mp3",
object : AudioPlayListener {
override fun onStateChanged(state: AudioPlayState) {
mSampleIsPlaying = state == AudioPlayState.Playing
mExpandView.onItemPlayStateChanged(chatSound, mSampleIsPlaying)
}
override fun onProgressChanged(position: Int, duration: Int) {
}
override fun onBufferingUpdate(percent: Int) {
}
})
}
}
override fun onAudioStarted() {
post {
// 更新UI为暂停状态
}
}
override fun onAudioPaused() {
post {
// 更新UI为播放状态
}
}
override fun onAudioError(errorMessage: String) {
post {
Toast.makeText(context, "播放失败: $errorMessage", Toast.LENGTH_SHORT).show()
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
if (mSampleIsPlaying) {
globalAudioPlayer.stop()
}
}
}
/**
// 在Activity中使用
class MainActivity : AppCompatActivity() {
private lateinit var audioPlayer: AudioPlayerManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
audioPlayer = AudioPlayerManager.getInstance(this)
setupAudioViews()
}
private fun setupAudioViews() {
// 按钮1播放网络音频
findViewById<View>(R.id.btnPlay1).setOnClickListener {
audioPlayer.play("https://example.com/audio1.mp3", object : AudioPlayListener {
override fun onStateChanged(state: AudioPlayState, source: String) {
// 只监听这个音频源的状态
updateButton1State(state)
}
override fun onProgressChanged(position: Int, duration: Int, source: String) {}
override fun onBufferingUpdate(percent: Int, source: String) {}
})
}
// 按钮2播放本地文件
findViewById<View>(R.id.btnPlay2).setOnClickListener {
val file = File(getExternalFilesDir("audio"), "local.mp3")
audioPlayer.play(file, object : AudioPlayListener {
override fun onStateChanged(state: AudioPlayState, source: String) {
updateButton2State(state)
}
override fun onProgressChanged(position: Int, duration: Int, source: String) {}
override fun onBufferingUpdate(percent: Int, source: String) {}
})
}
}
override fun onDestroy() {
super.onDestroy()
// 根据需求决定是否释放单例
// AudioPlayerManager.releaseInstance()
}
}
*/

View File

@ -3,6 +3,7 @@ package com.remax.visualnovel.ui.chat.setting.customui.expandableSelector
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import com.drake.brv.annotaion.DividerOrientation
import com.drake.brv.utils.bindingAdapter
@ -18,6 +19,7 @@ import com.remax.visualnovel.extension.glide.load
import com.remax.visualnovel.utils.ResUtil
import com.remax.visualnovel.widget.uitoken.setBgColorDirectly
private const val FILTER_ALL:Int = 0
private const val FILTER_MALE:Int = 1
private const val FILTER_FEMALE:Int = 2
@ -112,6 +114,7 @@ class ExpandSoundSubView @JvmOverloads constructor(
tvSoundDescrible.setTextColor(ResUtil.getColor(if (item.gender == 1) R.color.male_text_color else R.color.female_text_color))
tvSoundDescrible.text = item.desLanguage
ivSelect.setImageResource(if (item.isSelected) R.drawable.sound_item_selected else R.drawable.sound_item_unselected)
ivPlaySample.setImageResource(if (!item.isPlaying) R.mipmap.setting_sound_play else R.mipmap.icon_diamond)
}
}
}
@ -122,5 +125,10 @@ class ExpandSoundSubView @JvmOverloads constructor(
mBinding.itemsRv.models = items
}
fun onItemPlayStateChanged(chatSound: ChatSound, isPlaying: Boolean) {
chatSound.isPlaying = isPlaying
mBinding.itemsRv.bindingAdapter.notifyDataSetChanged()
}
}

View File

@ -1,128 +0,0 @@
package com.remax.visualnovel.utils.media
import android.content.Context
import android.net.Uri
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import java.io.File
abstract class AudioPlayableView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr), AudioPlayListener {
protected val audioPlayer = GlobalAudioPlayerManager.getInstance()
protected var currentAudioSource: String = ""
private var lifecycleObserver: LifecycleEventObserver? = null
fun playAudio(source: Any, autoStopOnDetach: Boolean = true) {
currentAudioSource = when (source) {
is String -> source
is Uri -> source.toString()
is File -> source.absolutePath
else -> return
}
// Just listen current source
audioPlayer.addListener(currentAudioSource, this, "view_${hashCode()}")
if (!audioPlayer.play(source, this, "view_${hashCode()}")) {
onPlayError("Play Error")
}
}
/**
* bind lifecycle from fragment or activity
*/
fun bindLifecycle(lifecycleOwner: LifecycleOwner) {
lifecycleObserver = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_PAUSE -> pauseAudio()
Lifecycle.Event.ON_RESUME -> { /* 可选的恢复逻辑 */ }
Lifecycle.Event.ON_DESTROY -> releaseAudio()
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(lifecycleObserver!!)
}
/**
* pause
*/
fun pauseAudio() {
if (audioPlayer.getCurrentSource() == currentAudioSource) {
audioPlayer.pause()
}
}
/**
* stop
*/
fun stopAudio() {
if (audioPlayer.getCurrentSource() == currentAudioSource) {
audioPlayer.stop()
}
audioPlayer.removeListener(currentAudioSource, this)
}
/**
* stop automatically while view detach
*/
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
stopAudio()
lifecycleObserver?.let { observer ->
(context as? LifecycleOwner)?.lifecycle?.removeObserver(observer)
}
}
/**
* release
*/
fun releaseAudio() {
stopAudio()
currentAudioSource = ""
}
// Specified source state changed callback
override fun onStateChanged(state: AudioPlayState, source: String) {
if (source != currentAudioSource) return
when (state) {
is AudioPlayState.Playing -> onAudioStarted()
is AudioPlayState.Paused -> onAudioPaused()
is AudioPlayState.Completed -> onAudioCompleted()
is AudioPlayState.Error -> onAudioError(state.errorMessage)
else -> {}
}
}
override fun onProgressChanged(position: Int, duration: Int, source: String) {
if (source == currentAudioSource) {
onAudioProgressChanged(position, duration)
}
}
override fun onBufferingUpdate(percent: Int, source: String) {
if (source == currentAudioSource) {
onAudioBufferingUpdate(percent)
}
}
// Sub class could override methods
open fun onPlayError(message: String) {}
open fun onAudioStarted() {}
open fun onAudioPaused() {}
open fun onAudioCompleted() {}
open fun onAudioError(errorMessage: String) {}
open fun onAudioProgressChanged(position: Int, duration: Int) {}
open fun onAudioBufferingUpdate(percent: Int) {}
}

View File

@ -13,8 +13,6 @@ import android.util.Log
import com.remax.visualnovel.configs.NovelApplication
import java.io.File
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
// Player State
sealed class AudioPlayState {
@ -23,7 +21,7 @@ sealed class AudioPlayState {
object Playing : AudioPlayState()
object Paused : AudioPlayState()
object Stopped : AudioPlayState()
data class Error(val errorCode: Int, val errorMessage: String, val source: String) : AudioPlayState()
data class Error(val errorCode: Int, val errorMessage: String) : AudioPlayState()
object Completed : AudioPlayState()
}
@ -56,9 +54,9 @@ data class AudioPlayerConfig(
// Player state listener
interface AudioPlayListener {
fun onStateChanged(state: AudioPlayState, source: String)
fun onProgressChanged(position: Int, duration: Int, source: String)
fun onBufferingUpdate(percent: Int, source: String)
fun onStateChanged(state: AudioPlayState)
fun onProgressChanged(position: Int, duration: Int)
fun onBufferingUpdate(percent: Int)
}
@ -93,23 +91,24 @@ class GlobalAudioPlayerManager private constructor(
}
private var mediaPlayer: MediaPlayer? = null
private var mMediaPlayer: MediaPlayer? = null
private var currentState: AudioPlayState = AudioPlayState.Idle
private val mainHandler = Handler(Looper.getMainLooper())
private var currentSource: String = ""
private var previousSource: String = ""
private var isAudioFocusGranted = false
private var audioFocusRequest: AudioFocusRequest? = null
private var wasPlayingBeforeFocusLoss = false
// Listeners, just notify events about specified file(Map, key)
private val sourceListeners = ConcurrentHashMap<String, CopyOnWriteArrayList<AudioPlayListener>>()
// key: hash code of
@Volatile
private var mCurListener: AudioPlayListener? = null
// progress runnable
private var progressUpdateRunnable: Runnable? = null
@Volatile
private var isReleased = false
init {
@ -120,7 +119,7 @@ class GlobalAudioPlayerManager private constructor(
if (isReleased) return
try {
mediaPlayer = MediaPlayer().apply {
mMediaPlayer = MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setUsage(config.usage)
@ -133,9 +132,9 @@ class GlobalAudioPlayerManager private constructor(
setOnBufferingUpdateListener(::onBufferingUpdate)
isLooping = config.loop
}
setState(AudioPlayState.Idle, currentSource)
setState(AudioPlayState.Idle)
} catch (e: Exception) {
handleError(AudioErrorCode.UNKNOWN_ERROR, "MediaPlayer init failed: ${e.message}", currentSource)
handleError(AudioErrorCode.UNKNOWN_ERROR, "MediaPlayer init failed: ${e.message}")
}
}
@ -144,7 +143,7 @@ class GlobalAudioPlayerManager private constructor(
/**
* play source, also pause previous playing source.
*/
fun play(source: Any, listener: AudioPlayListener? = null, tag: String = ""): Boolean {
fun play(source: Any, listener: AudioPlayListener): Boolean {
if (isReleased) {
Log.w("AudioPlayer", "Player released, play failed")
return false
@ -152,103 +151,64 @@ class GlobalAudioPlayerManager private constructor(
val sourceString = convertSourceToString(source)
if (sourceString.isBlank()) {
handleError(AudioErrorCode.INVALID_DATA_SOURCE, "Invalid source", sourceString)
handleError(AudioErrorCode.INVALID_DATA_SOURCE, "Invalid source")
return false
}
// 停止上一个播放如果是不同的source
if (currentSource.isNotEmpty() && currentSource != sourceString) {
// Stop playing one
if (isPlaying()) {
stopCurrentPlayback()
}
// 注册监听器
listener?.let { addListener(sourceString, it, tag) }
currentSource = sourceString
mCurListener = listener
return startPlayback(source)
}
/**
* 为特定source添加监听器
*/
fun addListener(source: Any, listener: AudioPlayListener, tag: String = "") {
val sourceString = convertSourceToString(source)
if (sourceString.isBlank()) return
sourceListeners.getOrPut(sourceString) { CopyOnWriteArrayList() }.add(listener)
Log.d("AudioPlayer", "为source[$sourceString]添加监听器, 当前监听器数: ${sourceListeners[sourceString]?.size}")
}
/**
* 移除特定source的监听器
*/
fun removeListener(source: Any, listener: AudioPlayListener) {
val sourceString = convertSourceToString(source)
sourceListeners[sourceString]?.remove(listener)
}
/**
* 移除特定tag的所有监听器
*/
fun removeListenersByTag(tag: String, source: Any? = null) {
if (source != null) {
val sourceString = convertSourceToString(source)
sourceListeners[sourceString]?.removeAll { listener ->
// 如果监听器有tag属性可以根据需要实现
true // 这里需要根据实际tag实现过滤逻辑
}
} else {
sourceListeners.values.forEach { listeners ->
listeners.removeAll { true } // 简化实现实际需要根据tag过滤
}
}
}
/**
* 暂停播放
* Pause
*/
fun pause(): Boolean {
if (isReleased || currentState != AudioPlayState.Playing) return false
return try {
mediaPlayer?.pause()
setState(AudioPlayState.Paused, currentSource)
mMediaPlayer?.pause()
setState(AudioPlayState.Paused)
stopProgressUpdates()
true
} catch (e: Exception) {
handleError(AudioErrorCode.UNKNOWN_ERROR, "暂停失败: ${e.message}", currentSource)
handleError(AudioErrorCode.UNKNOWN_ERROR, "Pause failed: ${e.message}")
false
}
}
/**
* 恢复播放
* resume play
*/
fun resume(): Boolean {
if (isReleased || currentState != AudioPlayState.Paused) return false
if (!requestAudioFocus()) {
handleError(AudioErrorCode.AUDIO_FOCUS_REQUEST_FAILED, "无法获取音频焦点", currentSource)
handleError(AudioErrorCode.AUDIO_FOCUS_REQUEST_FAILED, "Can't get audio focus")
return false
}
return try {
mediaPlayer?.start()
setState(AudioPlayState.Playing, currentSource)
mMediaPlayer?.start()
setState(AudioPlayState.Playing)
startProgressUpdates()
true
} catch (e: Exception) {
handleError(AudioErrorCode.UNKNOWN_ERROR, "恢复播放失败: ${e.message}", currentSource)
handleError(AudioErrorCode.UNKNOWN_ERROR, "恢复播放失败: ${e.message}")
false
}
}
/**
* 停止播放
* stop play
*/
fun stop(): Boolean {
if (isReleased) return false
stopCurrentPlayback()
return true
}
@ -260,7 +220,7 @@ class GlobalAudioPlayerManager private constructor(
if (isReleased) return false
return try {
mediaPlayer?.takeIf { it.duration > 0 }?.let { player ->
mMediaPlayer?.takeIf { it.duration > 0 }?.let { player ->
val safePosition = position.coerceIn(0, player.duration)
player.seekTo(safePosition)
true
@ -269,12 +229,7 @@ class GlobalAudioPlayerManager private constructor(
false
}
}
/**
* 获取当前播放的source
*/
fun getCurrentSource(): String = currentSource
/**
* 是否正在播放
*/
@ -284,23 +239,22 @@ class GlobalAudioPlayerManager private constructor(
* 获取当前状态
*/
fun getCurrentState(): AudioPlayState = currentState
// endregion
// region 播放控制内部实现
private fun startPlayback(source: Any): Boolean {
if (!requestAudioFocus()) {
handleError(AudioErrorCode.AUDIO_FOCUS_REQUEST_FAILED, "无法获取音频焦点", currentSource)
handleError(AudioErrorCode.AUDIO_FOCUS_REQUEST_FAILED, "Can not get audio focus")
return false
}
return try {
setState(AudioPlayState.Preparing, currentSource)
mediaPlayer?.let { player ->
setState(AudioPlayState.Preparing)
mMediaPlayer?.let { player ->
player.reset()
when (source) {
is String -> setupDataSourceFromString(source)
is Uri -> player.setDataSource(mAppContext, source)
@ -312,10 +266,11 @@ class GlobalAudioPlayerManager private constructor(
}
else -> throw IllegalArgumentException("Not support media type")
}
player.prepareAsync()
true
} ?: false
} catch (e: Exception) {
handlePlayException(e, "Play prepare failed")
false
@ -323,7 +278,7 @@ class GlobalAudioPlayerManager private constructor(
}
private fun setupDataSourceFromString(source: String) {
mediaPlayer?.let { player ->
mMediaPlayer?.let { player ->
when {
source.startsWith("http://") || source.startsWith("https://") -> {
player.setDataSource(source)
@ -349,22 +304,15 @@ class GlobalAudioPlayerManager private constructor(
private fun stopCurrentPlayback() {
try {
mediaPlayer?.let { player ->
if (player.isPlaying) {
player.stop()
}
mMediaPlayer.let { player ->
player?.stop()
}
setState(AudioPlayState.Stopped, currentSource)
setState(AudioPlayState.Stopped)
stopProgressUpdates()
abandonAudioFocus()
// 通知上一个source的监听器停止
if (previousSource.isNotEmpty()) {
notifyStateChanged(AudioPlayState.Stopped, previousSource)
}
previousSource = currentSource
currentSource = ""
mCurListener = null
} catch (e: Exception) {
Log.e("AudioPlayer", "Stop playing failed: ${e.message}")
}
@ -466,7 +414,7 @@ class GlobalAudioPlayerManager private constructor(
private fun onAudioFocusDuck() {
if (config.duckOnFocusLoss) {
mediaPlayer?.setVolume(0.2f, 0.2f)
mMediaPlayer?.setVolume(0.2f, 0.2f)
} else {
wasPlayingBeforeFocusLoss = currentState == AudioPlayState.Playing
if (wasPlayingBeforeFocusLoss) {
@ -484,22 +432,23 @@ class GlobalAudioPlayerManager private constructor(
try {
if (config.autoPlay) {
mp.start()
setState(AudioPlayState.Playing, currentSource)
setState(AudioPlayState.Playing)
startProgressUpdates()
} else {
setState(AudioPlayState.Paused, currentSource)
setState(AudioPlayState.Paused)
}
} catch (e: Exception) {
handleError(AudioErrorCode.PREPARE_FAILED, "Player prepare failed: ${e.message}", currentSource)
handleError(AudioErrorCode.PREPARE_FAILED, "Player prepare failed: ${e.message}")
}
}
}
private fun onCompletion(mp: MediaPlayer) {
mainHandler.post {
setState(AudioPlayState.Completed, currentSource)
setState(AudioPlayState.Completed)
stopProgressUpdates()
abandonAudioFocus()
mCurListener = null
}
}
@ -511,14 +460,14 @@ class GlobalAudioPlayerManager private constructor(
MediaPlayer.MEDIA_ERROR_IO -> "I/O ERROR"
else -> "Media Error: what=$what, extra=$extra"
}
handleError(AudioErrorCode.PREPARE_FAILED, errorMessage, currentSource)
handleError(AudioErrorCode.PREPARE_FAILED, errorMessage)
}
return true
}
private fun onBufferingUpdate(mp: MediaPlayer, percent: Int) {
mainHandler.post {
notifyBufferingUpdate(percent, currentSource)
notifyBufferingUpdate(percent)
}
}
// endregion
@ -532,9 +481,9 @@ class GlobalAudioPlayerManager private constructor(
override fun run() {
if (currentState == AudioPlayState.Playing && !isReleased) {
try {
val position = mediaPlayer?.currentPosition ?: 0
val duration = mediaPlayer?.duration ?: 0
notifyProgressChanged(position, duration, currentSource)
val position = mMediaPlayer?.currentPosition ?: 0
val duration = mMediaPlayer?.duration ?: 0
notifyProgressChanged(position, duration)
} catch (e: Exception) {
}
}
@ -553,54 +502,42 @@ class GlobalAudioPlayerManager private constructor(
// endregion
// region Various state notifications
private fun setState(state: AudioPlayState, source: String) {
private fun setState(state: AudioPlayState) {
if (currentState != state) {
currentState = state
notifyStateChanged(state, source)
notifyStateChanged(state)
}
}
private fun notifyStateChanged(state: AudioPlayState, source: String) {
mainHandler.post {
sourceListeners[source]?.forEach { listener ->
try {
listener.onStateChanged(state, source)
} catch (e: Exception) {
Log.e("AudioPlayer", "onStateChanged exception: ${e.message}")
}
private fun notifyStateChanged(state: AudioPlayState) {
val listener = mCurListener
if (listener != null) {
mainHandler.post {
listener.onStateChanged(state)
}
}
}
private fun notifyProgressChanged(position: Int, duration: Int, source: String) {
private fun notifyProgressChanged(position: Int, duration: Int) {
mainHandler.post {
sourceListeners[source]?.forEach { listener ->
try {
listener.onProgressChanged(position, duration, source)
} catch (e: Exception) {
Log.e("AudioPlayer", "onProgressChanged Exception: ${e.message}")
}
if (mCurListener != null) {
mCurListener?.onProgressChanged(position, duration)
}
}
}
private fun notifyBufferingUpdate(percent: Int, source: String) {
private fun notifyBufferingUpdate(percent: Int) {
mainHandler.post {
sourceListeners[source]?.forEach { listener ->
try {
listener.onBufferingUpdate(percent, source)
} catch (e: Exception) {
Log.e("AudioPlayer", "onBufferingUpdate exception: ${e.message}")
}
if (mCurListener != null) {
mCurListener?.onBufferingUpdate(percent)
}
}
}
// endregion
// region 工具方法
// region Tools
private fun convertSourceToString(source: Any): String {
return when (source) {
is String -> source
@ -617,16 +554,16 @@ class GlobalAudioPlayerManager private constructor(
is IllegalArgumentException -> AudioErrorCode.FORMAT_NOT_SUPPORTED
else -> AudioErrorCode.UNKNOWN_ERROR
}
handleError(errorCode, "$message: ${e.message}", currentSource)
handleError(errorCode, "$message: ${e.message}")
}
private fun handleError(errorCode: Int, errorMessage: String, source: String) {
Log.e("AudioPlayer", "Player Error[$errorCode]: $errorMessage, source: $source")
setState(AudioPlayState.Error(errorCode, errorMessage, source), source)
private fun handleError(errorCode: Int, errorMessage: String) {
setState(AudioPlayState.Error(errorCode, errorMessage))
stopProgressUpdates()
abandonAudioFocus()
mCurListener = null
Log.e("AudioPlayer", "Player Error[$errorCode]: $errorMessage")
}
// endregion
// region release related
@ -636,12 +573,11 @@ class GlobalAudioPlayerManager private constructor(
isReleased = true
stopCurrentPlayback()
stopProgressUpdates()
sourceListeners.clear()
abandonAudioFocus()
try {
mediaPlayer?.release()
mediaPlayer = null
mMediaPlayer?.release()
mMediaPlayer = null
} catch (e: Exception) {
Log.e("AudioPlayer", "Release player failed: ${e.message}")
}