sound 播放逻辑+1
This commit is contained in:
parent
1eb8b02ef0
commit
2d967c52a0
|
|
@ -14,6 +14,7 @@ data class ChatSound(
|
||||||
val nameLanguage: String = "",
|
val nameLanguage: String = "",
|
||||||
val headPortrait: String = "",
|
val headPortrait: String = "",
|
||||||
var isSelected: Boolean = false,
|
var isSelected: Boolean = false,
|
||||||
|
var isPlaying: Boolean = false,
|
||||||
|
|
||||||
// Other needed
|
// Other needed
|
||||||
var sampleUrl: String = ""
|
var sampleUrl: String = ""
|
||||||
|
|
|
||||||
|
|
@ -5,31 +5,32 @@ import android.animation.Animator
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Environment
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.Toast
|
|
||||||
import com.remax.visualnovel.R
|
import com.remax.visualnovel.R
|
||||||
import com.remax.visualnovel.configs.NovelApplication
|
import com.remax.visualnovel.configs.NovelApplication
|
||||||
import com.remax.visualnovel.databinding.LayoutExpandSelectViewBinding
|
import com.remax.visualnovel.databinding.LayoutExpandSelectViewBinding
|
||||||
import com.remax.visualnovel.entity.response.ChatSound
|
import com.remax.visualnovel.entity.response.ChatSound
|
||||||
import com.remax.visualnovel.utils.media.AudioPlayableView
|
import com.remax.visualnovel.utils.media.AudioPlayListener
|
||||||
import java.io.File
|
import com.remax.visualnovel.utils.media.AudioPlayState
|
||||||
|
import com.remax.visualnovel.utils.media.GlobalAudioPlayerManager
|
||||||
|
|
||||||
|
|
||||||
class ExpandSoundSelectView @JvmOverloads constructor(
|
class ExpandSoundSelectView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = 0
|
defStyleAttr: Int = 0
|
||||||
) : AudioPlayableView(context, attrs, defStyleAttr) {
|
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
private lateinit var mBinding: LayoutExpandSelectViewBinding
|
private lateinit var mBinding: LayoutExpandSelectViewBinding
|
||||||
private lateinit var mExpandView : ExpandSoundSubView
|
private lateinit var mExpandView : ExpandSoundSubView
|
||||||
private var isExpanded = false
|
private var isExpanded = false
|
||||||
private var animationDuration = 300
|
private var animationDuration = 300
|
||||||
|
private val globalAudioPlayer = GlobalAudioPlayerManager.getInstance()
|
||||||
|
private var mSampleIsPlaying = false
|
||||||
|
|
||||||
|
|
||||||
private lateinit var mEventListener: IEventListener
|
private lateinit var mEventListener: IEventListener
|
||||||
interface IEventListener {
|
interface IEventListener {
|
||||||
|
|
@ -161,87 +162,37 @@ class ExpandSoundSelectView @JvmOverloads constructor(
|
||||||
|
|
||||||
|
|
||||||
private fun playSoundSample(chatSound: ChatSound) {
|
private fun playSoundSample(chatSound: ChatSound) {
|
||||||
if (audioPlayer.isPlaying()) {
|
if (chatSound.sampleUrl.isEmpty()) {
|
||||||
pauseAudio()
|
// return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chatSound.sampleUrl.isEmpty()) {
|
with (globalAudioPlayer) {
|
||||||
playAudio( chatSound.sampleUrl)
|
globalAudioPlayer.play(NovelApplication.appContext().getExternalFilesDir(null)?.path + "/ringtone.mp3",
|
||||||
} else {
|
|
||||||
playAudio(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() {
|
override fun onDetachedFromWindow() {
|
||||||
post {
|
super.onDetachedFromWindow()
|
||||||
// 更新UI为播放状态
|
if (mSampleIsPlaying) {
|
||||||
}
|
globalAudioPlayer.stop()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAudioError(errorMessage: String) {
|
|
||||||
post {
|
|
||||||
Toast.makeText(context, "播放失败: $errorMessage", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
// 在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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.remax.visualnovel.ui.chat.setting.customui.expandableSelector
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.drake.brv.annotaion.DividerOrientation
|
import com.drake.brv.annotaion.DividerOrientation
|
||||||
import com.drake.brv.utils.bindingAdapter
|
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.utils.ResUtil
|
||||||
import com.remax.visualnovel.widget.uitoken.setBgColorDirectly
|
import com.remax.visualnovel.widget.uitoken.setBgColorDirectly
|
||||||
|
|
||||||
|
|
||||||
private const val FILTER_ALL:Int = 0
|
private const val FILTER_ALL:Int = 0
|
||||||
private const val FILTER_MALE:Int = 1
|
private const val FILTER_MALE:Int = 1
|
||||||
private const val FILTER_FEMALE:Int = 2
|
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.setTextColor(ResUtil.getColor(if (item.gender == 1) R.color.male_text_color else R.color.female_text_color))
|
||||||
tvSoundDescrible.text = item.desLanguage
|
tvSoundDescrible.text = item.desLanguage
|
||||||
ivSelect.setImageResource(if (item.isSelected) R.drawable.sound_item_selected else R.drawable.sound_item_unselected)
|
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
|
mBinding.itemsRv.models = items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onItemPlayStateChanged(chatSound: ChatSound, isPlaying: Boolean) {
|
||||||
|
chatSound.isPlaying = isPlaying
|
||||||
|
mBinding.itemsRv.bindingAdapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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) {}
|
|
||||||
}
|
|
||||||
|
|
@ -13,8 +13,6 @@ import android.util.Log
|
||||||
import com.remax.visualnovel.configs.NovelApplication
|
import com.remax.visualnovel.configs.NovelApplication
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
|
||||||
|
|
||||||
// Player State
|
// Player State
|
||||||
sealed class AudioPlayState {
|
sealed class AudioPlayState {
|
||||||
|
|
@ -23,7 +21,7 @@ sealed class AudioPlayState {
|
||||||
object Playing : AudioPlayState()
|
object Playing : AudioPlayState()
|
||||||
object Paused : AudioPlayState()
|
object Paused : AudioPlayState()
|
||||||
object Stopped : 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()
|
object Completed : AudioPlayState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,9 +54,9 @@ data class AudioPlayerConfig(
|
||||||
|
|
||||||
// Player state listener
|
// Player state listener
|
||||||
interface AudioPlayListener {
|
interface AudioPlayListener {
|
||||||
fun onStateChanged(state: AudioPlayState, source: String)
|
fun onStateChanged(state: AudioPlayState)
|
||||||
fun onProgressChanged(position: Int, duration: Int, source: String)
|
fun onProgressChanged(position: Int, duration: Int)
|
||||||
fun onBufferingUpdate(percent: Int, source: String)
|
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 var currentState: AudioPlayState = AudioPlayState.Idle
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
|
||||||
private var currentSource: String = ""
|
|
||||||
private var previousSource: String = ""
|
|
||||||
private var isAudioFocusGranted = false
|
private var isAudioFocusGranted = false
|
||||||
private var audioFocusRequest: AudioFocusRequest? = null
|
private var audioFocusRequest: AudioFocusRequest? = null
|
||||||
private var wasPlayingBeforeFocusLoss = false
|
private var wasPlayingBeforeFocusLoss = false
|
||||||
|
|
||||||
|
|
||||||
// Listeners, just notify events about specified file(Map, key)
|
// key: hash code of
|
||||||
private val sourceListeners = ConcurrentHashMap<String, CopyOnWriteArrayList<AudioPlayListener>>()
|
@Volatile
|
||||||
|
private var mCurListener: AudioPlayListener? = null
|
||||||
|
|
||||||
|
|
||||||
// progress runnable
|
// progress runnable
|
||||||
private var progressUpdateRunnable: Runnable? = null
|
private var progressUpdateRunnable: Runnable? = null
|
||||||
|
@Volatile
|
||||||
private var isReleased = false
|
private var isReleased = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -120,7 +119,7 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
if (isReleased) return
|
if (isReleased) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mediaPlayer = MediaPlayer().apply {
|
mMediaPlayer = MediaPlayer().apply {
|
||||||
setAudioAttributes(
|
setAudioAttributes(
|
||||||
AudioAttributes.Builder()
|
AudioAttributes.Builder()
|
||||||
.setUsage(config.usage)
|
.setUsage(config.usage)
|
||||||
|
|
@ -133,9 +132,9 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
setOnBufferingUpdateListener(::onBufferingUpdate)
|
setOnBufferingUpdateListener(::onBufferingUpdate)
|
||||||
isLooping = config.loop
|
isLooping = config.loop
|
||||||
}
|
}
|
||||||
setState(AudioPlayState.Idle, currentSource)
|
setState(AudioPlayState.Idle)
|
||||||
} catch (e: Exception) {
|
} 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.
|
* 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) {
|
if (isReleased) {
|
||||||
Log.w("AudioPlayer", "Player released, play failed")
|
Log.w("AudioPlayer", "Player released, play failed")
|
||||||
return false
|
return false
|
||||||
|
|
@ -152,103 +151,64 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
|
|
||||||
val sourceString = convertSourceToString(source)
|
val sourceString = convertSourceToString(source)
|
||||||
if (sourceString.isBlank()) {
|
if (sourceString.isBlank()) {
|
||||||
handleError(AudioErrorCode.INVALID_DATA_SOURCE, "Invalid source", sourceString)
|
handleError(AudioErrorCode.INVALID_DATA_SOURCE, "Invalid source")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止上一个播放(如果是不同的source)
|
// Stop playing one
|
||||||
if (currentSource.isNotEmpty() && currentSource != sourceString) {
|
if (isPlaying()) {
|
||||||
stopCurrentPlayback()
|
stopCurrentPlayback()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册监听器
|
mCurListener = listener
|
||||||
listener?.let { addListener(sourceString, it, tag) }
|
|
||||||
|
|
||||||
currentSource = sourceString
|
|
||||||
return startPlayback(source)
|
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的监听器
|
* Pause
|
||||||
*/
|
|
||||||
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过滤
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 暂停播放
|
|
||||||
*/
|
*/
|
||||||
fun pause(): Boolean {
|
fun pause(): Boolean {
|
||||||
if (isReleased || currentState != AudioPlayState.Playing) return false
|
if (isReleased || currentState != AudioPlayState.Playing) return false
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
mediaPlayer?.pause()
|
mMediaPlayer?.pause()
|
||||||
setState(AudioPlayState.Paused, currentSource)
|
setState(AudioPlayState.Paused)
|
||||||
stopProgressUpdates()
|
stopProgressUpdates()
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
handleError(AudioErrorCode.UNKNOWN_ERROR, "暂停失败: ${e.message}", currentSource)
|
handleError(AudioErrorCode.UNKNOWN_ERROR, "Pause failed: ${e.message}")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 恢复播放
|
* resume play
|
||||||
*/
|
*/
|
||||||
fun resume(): Boolean {
|
fun resume(): Boolean {
|
||||||
if (isReleased || currentState != AudioPlayState.Paused) return false
|
if (isReleased || currentState != AudioPlayState.Paused) return false
|
||||||
|
|
||||||
if (!requestAudioFocus()) {
|
if (!requestAudioFocus()) {
|
||||||
handleError(AudioErrorCode.AUDIO_FOCUS_REQUEST_FAILED, "无法获取音频焦点", currentSource)
|
handleError(AudioErrorCode.AUDIO_FOCUS_REQUEST_FAILED, "Can't get audio focus")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
mediaPlayer?.start()
|
mMediaPlayer?.start()
|
||||||
setState(AudioPlayState.Playing, currentSource)
|
setState(AudioPlayState.Playing)
|
||||||
startProgressUpdates()
|
startProgressUpdates()
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
handleError(AudioErrorCode.UNKNOWN_ERROR, "恢复播放失败: ${e.message}", currentSource)
|
handleError(AudioErrorCode.UNKNOWN_ERROR, "恢复播放失败: ${e.message}")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停止播放
|
* stop play
|
||||||
*/
|
*/
|
||||||
fun stop(): Boolean {
|
fun stop(): Boolean {
|
||||||
if (isReleased) return false
|
if (isReleased) return false
|
||||||
|
|
||||||
stopCurrentPlayback()
|
stopCurrentPlayback()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -260,7 +220,7 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
if (isReleased) return false
|
if (isReleased) return false
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
mediaPlayer?.takeIf { it.duration > 0 }?.let { player ->
|
mMediaPlayer?.takeIf { it.duration > 0 }?.let { player ->
|
||||||
val safePosition = position.coerceIn(0, player.duration)
|
val safePosition = position.coerceIn(0, player.duration)
|
||||||
player.seekTo(safePosition)
|
player.seekTo(safePosition)
|
||||||
true
|
true
|
||||||
|
|
@ -270,11 +230,6 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前播放的source
|
|
||||||
*/
|
|
||||||
fun getCurrentSource(): String = currentSource
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否正在播放
|
* 是否正在播放
|
||||||
*/
|
*/
|
||||||
|
|
@ -284,21 +239,20 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
* 获取当前状态
|
* 获取当前状态
|
||||||
*/
|
*/
|
||||||
fun getCurrentState(): AudioPlayState = currentState
|
fun getCurrentState(): AudioPlayState = currentState
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region 播放控制内部实现
|
|
||||||
|
|
||||||
|
// region 播放控制内部实现
|
||||||
private fun startPlayback(source: Any): Boolean {
|
private fun startPlayback(source: Any): Boolean {
|
||||||
if (!requestAudioFocus()) {
|
if (!requestAudioFocus()) {
|
||||||
handleError(AudioErrorCode.AUDIO_FOCUS_REQUEST_FAILED, "无法获取音频焦点", currentSource)
|
handleError(AudioErrorCode.AUDIO_FOCUS_REQUEST_FAILED, "Can not get audio focus")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
setState(AudioPlayState.Preparing, currentSource)
|
setState(AudioPlayState.Preparing)
|
||||||
|
|
||||||
mediaPlayer?.let { player ->
|
mMediaPlayer?.let { player ->
|
||||||
player.reset()
|
player.reset()
|
||||||
|
|
||||||
when (source) {
|
when (source) {
|
||||||
|
|
@ -316,6 +270,7 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
player.prepareAsync()
|
player.prepareAsync()
|
||||||
true
|
true
|
||||||
} ?: false
|
} ?: false
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
handlePlayException(e, "Play prepare failed")
|
handlePlayException(e, "Play prepare failed")
|
||||||
false
|
false
|
||||||
|
|
@ -323,7 +278,7 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupDataSourceFromString(source: String) {
|
private fun setupDataSourceFromString(source: String) {
|
||||||
mediaPlayer?.let { player ->
|
mMediaPlayer?.let { player ->
|
||||||
when {
|
when {
|
||||||
source.startsWith("http://") || source.startsWith("https://") -> {
|
source.startsWith("http://") || source.startsWith("https://") -> {
|
||||||
player.setDataSource(source)
|
player.setDataSource(source)
|
||||||
|
|
@ -349,22 +304,15 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
|
|
||||||
private fun stopCurrentPlayback() {
|
private fun stopCurrentPlayback() {
|
||||||
try {
|
try {
|
||||||
mediaPlayer?.let { player ->
|
mMediaPlayer.let { player ->
|
||||||
if (player.isPlaying) {
|
player?.stop()
|
||||||
player.stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setState(AudioPlayState.Stopped, currentSource)
|
|
||||||
|
setState(AudioPlayState.Stopped)
|
||||||
stopProgressUpdates()
|
stopProgressUpdates()
|
||||||
abandonAudioFocus()
|
abandonAudioFocus()
|
||||||
|
|
||||||
// 通知上一个source的监听器停止
|
mCurListener = null
|
||||||
if (previousSource.isNotEmpty()) {
|
|
||||||
notifyStateChanged(AudioPlayState.Stopped, previousSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
previousSource = currentSource
|
|
||||||
currentSource = ""
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("AudioPlayer", "Stop playing failed: ${e.message}")
|
Log.e("AudioPlayer", "Stop playing failed: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
@ -466,7 +414,7 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
|
|
||||||
private fun onAudioFocusDuck() {
|
private fun onAudioFocusDuck() {
|
||||||
if (config.duckOnFocusLoss) {
|
if (config.duckOnFocusLoss) {
|
||||||
mediaPlayer?.setVolume(0.2f, 0.2f)
|
mMediaPlayer?.setVolume(0.2f, 0.2f)
|
||||||
} else {
|
} else {
|
||||||
wasPlayingBeforeFocusLoss = currentState == AudioPlayState.Playing
|
wasPlayingBeforeFocusLoss = currentState == AudioPlayState.Playing
|
||||||
if (wasPlayingBeforeFocusLoss) {
|
if (wasPlayingBeforeFocusLoss) {
|
||||||
|
|
@ -484,22 +432,23 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
try {
|
try {
|
||||||
if (config.autoPlay) {
|
if (config.autoPlay) {
|
||||||
mp.start()
|
mp.start()
|
||||||
setState(AudioPlayState.Playing, currentSource)
|
setState(AudioPlayState.Playing)
|
||||||
startProgressUpdates()
|
startProgressUpdates()
|
||||||
} else {
|
} else {
|
||||||
setState(AudioPlayState.Paused, currentSource)
|
setState(AudioPlayState.Paused)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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) {
|
private fun onCompletion(mp: MediaPlayer) {
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
setState(AudioPlayState.Completed, currentSource)
|
setState(AudioPlayState.Completed)
|
||||||
stopProgressUpdates()
|
stopProgressUpdates()
|
||||||
abandonAudioFocus()
|
abandonAudioFocus()
|
||||||
|
mCurListener = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -511,14 +460,14 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
MediaPlayer.MEDIA_ERROR_IO -> "I/O ERROR"
|
MediaPlayer.MEDIA_ERROR_IO -> "I/O ERROR"
|
||||||
else -> "Media Error: what=$what, extra=$extra"
|
else -> "Media Error: what=$what, extra=$extra"
|
||||||
}
|
}
|
||||||
handleError(AudioErrorCode.PREPARE_FAILED, errorMessage, currentSource)
|
handleError(AudioErrorCode.PREPARE_FAILED, errorMessage)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onBufferingUpdate(mp: MediaPlayer, percent: Int) {
|
private fun onBufferingUpdate(mp: MediaPlayer, percent: Int) {
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
notifyBufferingUpdate(percent, currentSource)
|
notifyBufferingUpdate(percent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
@ -532,9 +481,9 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
override fun run() {
|
override fun run() {
|
||||||
if (currentState == AudioPlayState.Playing && !isReleased) {
|
if (currentState == AudioPlayState.Playing && !isReleased) {
|
||||||
try {
|
try {
|
||||||
val position = mediaPlayer?.currentPosition ?: 0
|
val position = mMediaPlayer?.currentPosition ?: 0
|
||||||
val duration = mediaPlayer?.duration ?: 0
|
val duration = mMediaPlayer?.duration ?: 0
|
||||||
notifyProgressChanged(position, duration, currentSource)
|
notifyProgressChanged(position, duration)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -553,54 +502,42 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Various state notifications
|
// region Various state notifications
|
||||||
private fun setState(state: AudioPlayState, source: String) {
|
private fun setState(state: AudioPlayState) {
|
||||||
if (currentState != state) {
|
if (currentState != state) {
|
||||||
currentState = state
|
currentState = state
|
||||||
notifyStateChanged(state, source)
|
notifyStateChanged(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyStateChanged(state: AudioPlayState, source: String) {
|
private fun notifyStateChanged(state: AudioPlayState) {
|
||||||
mainHandler.post {
|
val listener = mCurListener
|
||||||
sourceListeners[source]?.forEach { listener ->
|
if (listener != null) {
|
||||||
try {
|
mainHandler.post {
|
||||||
listener.onStateChanged(state, source)
|
listener.onStateChanged(state)
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("AudioPlayer", "onStateChanged exception: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyProgressChanged(position: Int, duration: Int, source: String) {
|
private fun notifyProgressChanged(position: Int, duration: Int) {
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
sourceListeners[source]?.forEach { listener ->
|
if (mCurListener != null) {
|
||||||
try {
|
mCurListener?.onProgressChanged(position, duration)
|
||||||
listener.onProgressChanged(position, duration, source)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("AudioPlayer", "onProgressChanged Exception: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyBufferingUpdate(percent: Int, source: String) {
|
private fun notifyBufferingUpdate(percent: Int) {
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
sourceListeners[source]?.forEach { listener ->
|
if (mCurListener != null) {
|
||||||
try {
|
mCurListener?.onBufferingUpdate(percent)
|
||||||
listener.onBufferingUpdate(percent, source)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("AudioPlayer", "onBufferingUpdate exception: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// region 工具方法
|
// region Tools
|
||||||
private fun convertSourceToString(source: Any): String {
|
private fun convertSourceToString(source: Any): String {
|
||||||
return when (source) {
|
return when (source) {
|
||||||
is String -> source
|
is String -> source
|
||||||
|
|
@ -617,16 +554,16 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
is IllegalArgumentException -> AudioErrorCode.FORMAT_NOT_SUPPORTED
|
is IllegalArgumentException -> AudioErrorCode.FORMAT_NOT_SUPPORTED
|
||||||
else -> AudioErrorCode.UNKNOWN_ERROR
|
else -> AudioErrorCode.UNKNOWN_ERROR
|
||||||
}
|
}
|
||||||
handleError(errorCode, "$message: ${e.message}", currentSource)
|
handleError(errorCode, "$message: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleError(errorCode: Int, errorMessage: String, source: String) {
|
private fun handleError(errorCode: Int, errorMessage: String) {
|
||||||
Log.e("AudioPlayer", "Player Error[$errorCode]: $errorMessage, source: $source")
|
setState(AudioPlayState.Error(errorCode, errorMessage))
|
||||||
setState(AudioPlayState.Error(errorCode, errorMessage, source), source)
|
|
||||||
stopProgressUpdates()
|
stopProgressUpdates()
|
||||||
abandonAudioFocus()
|
abandonAudioFocus()
|
||||||
|
mCurListener = null
|
||||||
|
Log.e("AudioPlayer", "Player Error[$errorCode]: $errorMessage")
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region release related
|
// region release related
|
||||||
|
|
@ -636,12 +573,11 @@ class GlobalAudioPlayerManager private constructor(
|
||||||
isReleased = true
|
isReleased = true
|
||||||
stopCurrentPlayback()
|
stopCurrentPlayback()
|
||||||
stopProgressUpdates()
|
stopProgressUpdates()
|
||||||
sourceListeners.clear()
|
|
||||||
abandonAudioFocus()
|
abandonAudioFocus()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mediaPlayer?.release()
|
mMediaPlayer?.release()
|
||||||
mediaPlayer = null
|
mMediaPlayer = null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("AudioPlayer", "Release player failed: ${e.message}")
|
Log.e("AudioPlayer", "Release player failed: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue