sound 播放逻辑

This commit is contained in:
renhaoting 2025-11-06 10:55:46 +08:00
parent 8f171d3214
commit 1eb8b02ef0
8 changed files with 922 additions and 22 deletions

View File

@ -6,14 +6,17 @@ import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class ChatSound( data class ChatSound(
val ttsId: Long, val ttsId: Long = 0,
val language: Int, val language: Int = 0,
val desLanguage: String, val desLanguage: String = "",
val gender: Int, val gender: Int = 1,
val rules: Int, val rules: Int = 0,
val nameLanguage: String, val nameLanguage: String = "",
val headPortrait: String, val headPortrait: String = "",
var isSelected: Boolean = false var isSelected: Boolean = false,
// Other needed
var sampleUrl: String = ""
) : Parcelable { ) : Parcelable {
} }

View File

@ -172,6 +172,33 @@ class ChatSettingView @JvmOverloads constructor(
fun initSoundSelectorView() { fun initSoundSelectorView() {
val items = listOf(
ChatSound(
ttsId = 1,
gender = 2,
nameLanguage = "名称-1"
),
ChatSound(
ttsId = 1,
gender = 2,
nameLanguage = "名称-2"
),
ChatSound(
ttsId = 1,
gender = 1,
nameLanguage = "名称-3"
),
ChatSound(
ttsId = 1,
gender = 1,
nameLanguage = "名称-4"
),
ChatSound(
ttsId = 1,
gender = 2,
nameLanguage = "名称-5"
),
)
/*val items = listOf( /*val items = listOf(
ChatSound( ChatSound(
id = 1L, id = 1L,
@ -214,7 +241,7 @@ class ChatSettingView @JvmOverloads constructor(
) )
*/ */
with(mBinding.soundActorSelector) { with(mBinding.soundActorSelector) {
//setItems(items) setItems(items)
setEventListener( setEventListener(
object : ExpandSoundSelectView.IEventListener { object : ExpandSoundSelectView.IEventListener {
override fun onItemSelected( override fun onItemSelected(

View File

@ -5,21 +5,27 @@ 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.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 java.io.File
class ExpandSoundSelectView @JvmOverloads constructor( class ExpandSoundSelectView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : android.widget.LinearLayout(context, attrs, defStyleAttr) { ) : AudioPlayableView(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
@ -56,6 +62,10 @@ class ExpandSoundSelectView @JvmOverloads constructor(
override fun onFilterChanged(sexValue: Int) { override fun onFilterChanged(sexValue: Int) {
mEventListener.onFiltersChanged(sexValue) mEventListener.onFiltersChanged(sexValue)
} }
override fun onPlaySample(soundItem: ChatSound) {
playSoundSample(soundItem)
}
}) })
} }
@ -150,5 +160,88 @@ class ExpandSoundSelectView @JvmOverloads constructor(
} }
private fun playSoundSample(chatSound: ChatSound) {
if (audioPlayer.isPlaying()) {
pauseAudio()
}
if (!chatSound.sampleUrl.isEmpty()) {
playAudio( chatSound.sampleUrl)
} else {
playAudio(NovelApplication.appContext().getExternalFilesDir(null)?.path + "/ringtone.mp3")
}
}
override fun onAudioStarted() {
post {
// 更新UI为暂停状态
}
}
override fun onAudioPaused() {
post {
// 更新UI为播放状态
}
}
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()
}
}
*/

View File

@ -36,6 +36,7 @@ class ExpandSoundSubView @JvmOverloads constructor(
interface IEventListener { interface IEventListener {
fun onSoundSelected(sound: ChatSound) fun onSoundSelected(sound: ChatSound)
fun onFilterChanged(setValue: Int) fun onFilterChanged(setValue: Int)
fun onPlaySample(soundItem: ChatSound)
} }
@ -68,10 +69,6 @@ class ExpandSoundSubView @JvmOverloads constructor(
} }
} }
fun playActorSound(bubble : ChatSound) {
}
fun setEventListener(eventListener: IEventListener) { fun setEventListener(eventListener: IEventListener) {
mEventListener = eventListener mEventListener = eventListener
@ -86,8 +83,8 @@ class ExpandSoundSubView @JvmOverloads constructor(
addType<ChatSound>(R.layout.layout_item_setting_sound) addType<ChatSound>(R.layout.layout_item_setting_sound)
onClick(R.id.left_container) { onClick(R.id.left_container) {
val bubble = getModel<ChatSound>() val sound = getModel<ChatSound>()
playActorSound(bubble) mEventListener.onPlaySample(sound)
} }
onClick(R.id.item_root) { onClick(R.id.item_root) {

View File

@ -0,0 +1,128 @@
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

@ -0,0 +1,651 @@
package com.remax.visualnovel.utils.media
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.MediaPlayer
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
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 {
object Idle : AudioPlayState()
object Preparing : AudioPlayState()
object Playing : AudioPlayState()
object Paused : AudioPlayState()
object Stopped : AudioPlayState()
data class Error(val errorCode: Int, val errorMessage: String, val source: String) : AudioPlayState()
object Completed : AudioPlayState()
}
// error code
object AudioErrorCode {
const val UNKNOWN_ERROR = 1000
const val NETWORK_ERROR = 1001
const val FILE_NOT_FOUND = 1002
const val FORMAT_NOT_SUPPORTED = 1003
const val PERMISSION_DENIED = 1004
const val AUDIO_FOCUS_LOST = 1005
const val PREPARE_FAILED = 1006
const val AUDIO_FOCUS_REQUEST_FAILED = 1007
const val INVALID_DATA_SOURCE = 1008
}
// Player config
data class AudioPlayerConfig(
val streamType: Int = AudioManager.STREAM_MUSIC,
val usage: Int = AudioAttributes.USAGE_MEDIA,
val contentType: Int = AudioAttributes.CONTENT_TYPE_MUSIC,
val focusGain: Int = AudioManager.AUDIOFOCUS_GAIN,
val autoPlay: Boolean = true,
val loop: Boolean = false,
val timeoutMs: Long = 30000,
val retryCount: Int = 2,
val duckOnFocusLoss: Boolean = true
)
// 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)
}
// Single instance player manager
class GlobalAudioPlayerManager private constructor(
private val config: AudioPlayerConfig = AudioPlayerConfig()
) {
companion object {
@Volatile
private var instance: GlobalAudioPlayerManager? = null
private val mAppContext: Context = NovelApplication.appContext()
fun getInstance(config: AudioPlayerConfig = AudioPlayerConfig()): GlobalAudioPlayerManager {
return instance ?: synchronized(this) {
instance ?: GlobalAudioPlayerManager(config).also { instance = it }
}
}
fun releaseInstance() {
instance?.release()
instance = null
}
}
// Global audio focus manager
private val audioManager: AudioManager by lazy {
mAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
}
private var mediaPlayer: 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>>()
// progress runnable
private var progressUpdateRunnable: Runnable? = null
private var isReleased = false
init {
initializeMediaPlayer()
}
private fun initializeMediaPlayer() {
if (isReleased) return
try {
mediaPlayer = MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setUsage(config.usage)
.setContentType(config.contentType)
.build()
)
setOnPreparedListener(::onPrepared)
setOnCompletionListener(::onCompletion)
setOnErrorListener(::onError)
setOnBufferingUpdateListener(::onBufferingUpdate)
isLooping = config.loop
}
setState(AudioPlayState.Idle, currentSource)
} catch (e: Exception) {
handleError(AudioErrorCode.UNKNOWN_ERROR, "MediaPlayer init failed: ${e.message}", currentSource)
}
}
/**
* play source, also pause previous playing source.
*/
fun play(source: Any, listener: AudioPlayListener? = null, tag: String = ""): Boolean {
if (isReleased) {
Log.w("AudioPlayer", "Player released, play failed")
return false
}
val sourceString = convertSourceToString(source)
if (sourceString.isBlank()) {
handleError(AudioErrorCode.INVALID_DATA_SOURCE, "Invalid source", sourceString)
return false
}
// 停止上一个播放如果是不同的source
if (currentSource.isNotEmpty() && currentSource != sourceString) {
stopCurrentPlayback()
}
// 注册监听器
listener?.let { addListener(sourceString, it, tag) }
currentSource = sourceString
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过滤
}
}
}
/**
* 暂停播放
*/
fun pause(): Boolean {
if (isReleased || currentState != AudioPlayState.Playing) return false
return try {
mediaPlayer?.pause()
setState(AudioPlayState.Paused, currentSource)
stopProgressUpdates()
true
} catch (e: Exception) {
handleError(AudioErrorCode.UNKNOWN_ERROR, "暂停失败: ${e.message}", currentSource)
false
}
}
/**
* 恢复播放
*/
fun resume(): Boolean {
if (isReleased || currentState != AudioPlayState.Paused) return false
if (!requestAudioFocus()) {
handleError(AudioErrorCode.AUDIO_FOCUS_REQUEST_FAILED, "无法获取音频焦点", currentSource)
return false
}
return try {
mediaPlayer?.start()
setState(AudioPlayState.Playing, currentSource)
startProgressUpdates()
true
} catch (e: Exception) {
handleError(AudioErrorCode.UNKNOWN_ERROR, "恢复播放失败: ${e.message}", currentSource)
false
}
}
/**
* 停止播放
*/
fun stop(): Boolean {
if (isReleased) return false
stopCurrentPlayback()
return true
}
/**
* 跳转到指定位置
*/
fun seekTo(position: Int): Boolean {
if (isReleased) return false
return try {
mediaPlayer?.takeIf { it.duration > 0 }?.let { player ->
val safePosition = position.coerceIn(0, player.duration)
player.seekTo(safePosition)
true
} ?: false
} catch (e: Exception) {
false
}
}
/**
* 获取当前播放的source
*/
fun getCurrentSource(): String = currentSource
/**
* 是否正在播放
*/
fun isPlaying(): Boolean = currentState == AudioPlayState.Playing && !isReleased
/**
* 获取当前状态
*/
fun getCurrentState(): AudioPlayState = currentState
// endregion
// region 播放控制内部实现
private fun startPlayback(source: Any): Boolean {
if (!requestAudioFocus()) {
handleError(AudioErrorCode.AUDIO_FOCUS_REQUEST_FAILED, "无法获取音频焦点", currentSource)
return false
}
return try {
setState(AudioPlayState.Preparing, currentSource)
mediaPlayer?.let { player ->
player.reset()
when (source) {
is String -> setupDataSourceFromString(source)
is Uri -> player.setDataSource(mAppContext, source)
is File -> {
if (!source.exists()) {
throw IOException("File not exist: ${source.absolutePath}")
}
player.setDataSource(source.absolutePath)
}
else -> throw IllegalArgumentException("Not support media type")
}
player.prepareAsync()
true
} ?: false
} catch (e: Exception) {
handlePlayException(e, "Play prepare failed")
false
}
}
private fun setupDataSourceFromString(source: String) {
mediaPlayer?.let { player ->
when {
source.startsWith("http://") || source.startsWith("https://") -> {
player.setDataSource(source)
}
source.startsWith("file://") -> {
player.setDataSource(source)
}
source.startsWith("content://") -> {
player.setDataSource(mAppContext, Uri.parse(source))
}
else -> {
// 尝试作为文件路径处理
val file = File(source)
if (file.exists()) {
player.setDataSource(source)
} else {
throw IOException("File not exist: $source")
}
}
}
}
}
private fun stopCurrentPlayback() {
try {
mediaPlayer?.let { player ->
if (player.isPlaying) {
player.stop()
}
}
setState(AudioPlayState.Stopped, currentSource)
stopProgressUpdates()
abandonAudioFocus()
// 通知上一个source的监听器停止
if (previousSource.isNotEmpty()) {
notifyStateChanged(AudioPlayState.Stopped, previousSource)
}
previousSource = currentSource
currentSource = ""
} catch (e: Exception) {
Log.e("AudioPlayer", "Stop playing failed: ${e.message}")
}
}
// endregion
// region 音频焦点管理
private fun requestAudioFocus(): Boolean {
if (isAudioFocusGranted) return true
return try {
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audioFocusRequest = AudioFocusRequest.Builder(config.focusGain).apply {
setAudioAttributes(
AudioAttributes.Builder()
.setUsage(config.usage)
.setContentType(config.contentType)
.build()
)
setOnAudioFocusChangeListener(::onAudioFocusChange)
setWillPauseWhenDucked(config.duckOnFocusLoss)
}.build()
audioManager.requestAudioFocus(audioFocusRequest!!)
} else {
@Suppress("DEPRECATION")
audioManager.requestAudioFocus(
::onAudioFocusChange,
config.streamType,
config.focusGain
)
}
isAudioFocusGranted = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
isAudioFocusGranted
} catch (e: Exception) {
Log.e("AudioPlayer", "Request audio focus failed: ${e.message}")
false
}
}
private fun abandonAudioFocus() {
try {
if (isAudioFocusGranted) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audioFocusRequest?.let { audioManager.abandonAudioFocusRequest(it) }
} else {
@Suppress("DEPRECATION")
audioManager.abandonAudioFocus(::onAudioFocusChange)
}
isAudioFocusGranted = false
}
} catch (e: Exception) {
Log.e("AudioPlayer", "Release audio focus failed: ${e.message}")
}
}
private fun onAudioFocusChange(focusChange: Int) {
mainHandler.post {
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> {
onAudioFocusGained()
}
AudioManager.AUDIOFOCUS_LOSS -> {
onAudioFocusLost()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
onAudioFocusLostTransient()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
onAudioFocusDuck()
}
}
}
}
private fun onAudioFocusGained() {
isAudioFocusGranted = true
if (wasPlayingBeforeFocusLoss && currentState == AudioPlayState.Paused) {
resume()
}
wasPlayingBeforeFocusLoss = false
}
private fun onAudioFocusLost() {
wasPlayingBeforeFocusLoss = currentState == AudioPlayState.Playing
stop()
}
private fun onAudioFocusLostTransient() {
wasPlayingBeforeFocusLoss = currentState == AudioPlayState.Playing
if (wasPlayingBeforeFocusLoss) {
pause()
}
}
private fun onAudioFocusDuck() {
if (config.duckOnFocusLoss) {
mediaPlayer?.setVolume(0.2f, 0.2f)
} else {
wasPlayingBeforeFocusLoss = currentState == AudioPlayState.Playing
if (wasPlayingBeforeFocusLoss) {
pause()
}
}
}
// endregion
// region MediaPlayer callback
private fun onPrepared(mp: MediaPlayer) {
mainHandler.post {
try {
if (config.autoPlay) {
mp.start()
setState(AudioPlayState.Playing, currentSource)
startProgressUpdates()
} else {
setState(AudioPlayState.Paused, currentSource)
}
} catch (e: Exception) {
handleError(AudioErrorCode.PREPARE_FAILED, "Player prepare failed: ${e.message}", currentSource)
}
}
}
private fun onCompletion(mp: MediaPlayer) {
mainHandler.post {
setState(AudioPlayState.Completed, currentSource)
stopProgressUpdates()
abandonAudioFocus()
}
}
private fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
mainHandler.post {
val errorMessage = when (what) {
MediaPlayer.MEDIA_ERROR_UNKNOWN -> "Unknown error"
MediaPlayer.MEDIA_ERROR_SERVER_DIED -> "Media server error"
MediaPlayer.MEDIA_ERROR_IO -> "I/O ERROR"
else -> "Media Error: what=$what, extra=$extra"
}
handleError(AudioErrorCode.PREPARE_FAILED, errorMessage, currentSource)
}
return true
}
private fun onBufferingUpdate(mp: MediaPlayer, percent: Int) {
mainHandler.post {
notifyBufferingUpdate(percent, currentSource)
}
}
// endregion
// region progress
private fun startProgressUpdates() {
stopProgressUpdates()
progressUpdateRunnable = object : Runnable {
override fun run() {
if (currentState == AudioPlayState.Playing && !isReleased) {
try {
val position = mediaPlayer?.currentPosition ?: 0
val duration = mediaPlayer?.duration ?: 0
notifyProgressChanged(position, duration, currentSource)
} catch (e: Exception) {
}
}
mainHandler.postDelayed(this, 1000)
}
}
mainHandler.post(progressUpdateRunnable!!)
}
private fun stopProgressUpdates() {
progressUpdateRunnable?.let {
mainHandler.removeCallbacks(it)
progressUpdateRunnable = null
}
}
// endregion
// region Various state notifications
private fun setState(state: AudioPlayState, source: String) {
if (currentState != state) {
currentState = state
notifyStateChanged(state, source)
}
}
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 notifyProgressChanged(position: Int, duration: Int, source: String) {
mainHandler.post {
sourceListeners[source]?.forEach { listener ->
try {
listener.onProgressChanged(position, duration, source)
} catch (e: Exception) {
Log.e("AudioPlayer", "onProgressChanged Exception: ${e.message}")
}
}
}
}
private fun notifyBufferingUpdate(percent: Int, source: String) {
mainHandler.post {
sourceListeners[source]?.forEach { listener ->
try {
listener.onBufferingUpdate(percent, source)
} catch (e: Exception) {
Log.e("AudioPlayer", "onBufferingUpdate exception: ${e.message}")
}
}
}
}
// endregion
// region 工具方法
private fun convertSourceToString(source: Any): String {
return when (source) {
is String -> source
is Uri -> source.toString()
is File -> source.absolutePath
else -> ""
}
}
private fun handlePlayException(e: Exception, message: String) {
val errorCode = when (e) {
is IOException -> AudioErrorCode.NETWORK_ERROR
is SecurityException -> AudioErrorCode.PERMISSION_DENIED
is IllegalArgumentException -> AudioErrorCode.FORMAT_NOT_SUPPORTED
else -> AudioErrorCode.UNKNOWN_ERROR
}
handleError(errorCode, "$message: ${e.message}", currentSource)
}
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)
stopProgressUpdates()
abandonAudioFocus()
}
// endregion
// region release related
fun release() {
if (isReleased) return
isReleased = true
stopCurrentPlayback()
stopProgressUpdates()
sourceListeners.clear()
abandonAudioFocus()
try {
mediaPlayer?.release()
mediaPlayer = null
} catch (e: Exception) {
Log.e("AudioPlayer", "Release player failed: ${e.message}")
}
}
// endregion
}

View File

@ -64,17 +64,17 @@ open class IconFontTextView @JvmOverloads constructor(context: Context, attrs: A
var endDrawable: IconFontDrawable? = null var endDrawable: IconFontDrawable? = null
var topDrawable: IconFontDrawable? = null var topDrawable: IconFontDrawable? = null
var bottomDrawable: IconFontDrawable? = null var bottomDrawable: IconFontDrawable? = null
if (!startIconFont.isNullOrEmpty()) { if (!startIconFont.isNullOrBlank()) {
startDrawable = IconFontDrawable(context, startIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp)) startDrawable = IconFontDrawable(context, startIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp))
} }
if (!endIconFont.isNullOrEmpty()) { if (!endIconFont.isNullOrBlank()) {
endDrawable = endDrawable =
IconFontDrawable(context, endIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp)) IconFontDrawable(context, endIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp))
} }
if (!topIconFont.isNullOrEmpty()) { if (!topIconFont.isNullOrBlank()) {
topDrawable = IconFontDrawable(context, topIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp)) topDrawable = IconFontDrawable(context, topIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp))
} }
if (!bottomIconFont.isNullOrEmpty()) { if (!bottomIconFont.isNullOrBlank()) {
bottomDrawable = IconFontDrawable(context, bottomIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp)) bottomDrawable = IconFontDrawable(context, bottomIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp))
} }
if (startIconFont == null && topIconFont == null && endIconFont == null && bottomIconFont == null) { if (startIconFont == null && topIconFont == null && endIconFont == null && bottomIconFont == null) {

View File

@ -25,6 +25,7 @@
app:advRadius="@dimen/dp_28" /> app:advRadius="@dimen/dp_28" />
<com.remax.visualnovel.widget.uitoken.view.UITokenImageView <com.remax.visualnovel.widget.uitoken.view.UITokenImageView
android:id="@+id/iv_play_sample"
android:layout_width="@dimen/dp_20" android:layout_width="@dimen/dp_20"
android:layout_height="@dimen/dp_20" android:layout_height="@dimen/dp_20"
android:layout_gravity="end|top" android:layout_gravity="end|top"