Files
QieKan-3.0/app/src/main/java/io/legato/kazusa/service/AudioPlayService.kt
T
2025-09-05 03:38:27 +08:00

658 lines
22 KiB
Kotlin

package io.legato.kazusa.service
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.AudioManager
import android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
import androidx.lifecycle.lifecycleScope
import androidx.media.AudioFocusRequestCompat
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import io.legato.kazusa.R
import io.legato.kazusa.base.BaseService
import io.legato.kazusa.constant.AppConst
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.constant.EventBus
import io.legato.kazusa.constant.IntentAction
import io.legato.kazusa.constant.NotificationId
import io.legato.kazusa.constant.Status
import io.legato.kazusa.help.MediaHelp
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.coroutine.Coroutine
import io.legato.kazusa.help.exoplayer.ExoPlayerHelper
import io.legato.kazusa.help.glide.ImageLoader
import io.legato.kazusa.model.AudioPlay
import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
import io.legato.kazusa.model.analyzeRule.AnalyzeUrl.Companion.getMediaItem
import io.legato.kazusa.receiver.MediaButtonReceiver
import io.legato.kazusa.ui.book.audio.AudioPlayActivity
import io.legato.kazusa.utils.activityPendingIntent
import io.legato.kazusa.utils.broadcastPendingIntent
import io.legato.kazusa.utils.postEvent
import io.legato.kazusa.utils.printOnDebug
import io.legato.kazusa.utils.servicePendingIntent
import io.legato.kazusa.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import splitties.init.appCtx
import splitties.systemservices.audioManager
import splitties.systemservices.notificationManager
import splitties.systemservices.powerManager
import splitties.systemservices.wifiManager
/**
* 音频播放服务
*/
class AudioPlayService : BaseService(),
AudioManager.OnAudioFocusChangeListener,
Player.Listener {
companion object {
@JvmStatic
var isRun = false
private set
@JvmStatic
var pause = true
private set
@JvmStatic
var timeMinute: Int = 0
var url: String = ""
private set
private const val MEDIA_SESSION_ACTIONS = (PlaybackStateCompat.ACTION_PLAY
or PlaybackStateCompat.ACTION_PAUSE
or PlaybackStateCompat.ACTION_PLAY_PAUSE
or PlaybackStateCompat.ACTION_SEEK_TO)
private const val APP_ACTION_STOP = "Stop"
private const val APP_ACTION_TIMER = "Timer"
}
private val useWakeLock = AppConfig.audioPlayUseWakeLock
private val wakeLock by lazy {
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:AudioPlayService")
.apply {
this.setReferenceCounted(false)
}
}
private val wifiLock by lazy {
@Suppress("DEPRECATION")
wifiManager?.createWifiLock(WIFI_MODE_FULL_HIGH_PERF, "legado:AudioPlayService")?.apply {
setReferenceCounted(false)
}
}
private val mFocusRequest: AudioFocusRequestCompat by lazy {
MediaHelp.buildAudioFocusRequestCompat(this)
}
private val exoPlayer: ExoPlayer by lazy {
ExoPlayerHelper.createHttpExoPlayer(this)
}
private var mediaSessionCompat: MediaSessionCompat? = null
private var broadcastReceiver: BroadcastReceiver? = null
private var needResumeOnAudioFocusGain = false
private var position = AudioPlay.book?.durChapterPos ?: 0
private var dsJob: Job? = null
private var upNotificationJob: Coroutine<*>? = null
private var upPlayProgressJob: Job? = null
private var playSpeed: Float = 1f
private var cover: Bitmap =
BitmapFactory.decodeResource(appCtx.resources, R.drawable.icon_read_book)
override fun onCreate() {
super.onCreate()
isRun = true
exoPlayer.addListener(this)
AudioPlay.registerService(this)
initMediaSession()
initBroadcastReceiver()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
doDs()
execute {
ImageLoader
.loadBitmap(this@AudioPlayService, AudioPlay.book?.getDisplayCover())
.submit()
.get()
}.onSuccess {
if (it.width > 16 && it.height > 16) {
cover = it
upMediaMetadata()
upAudioPlayNotification()
}
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.action?.let { action ->
when (action) {
IntentAction.play -> {
exoPlayer.stop()
upPlayProgressJob?.cancel()
pause = false
position = AudioPlay.book?.durChapterPos ?: 0
url = AudioPlay.durPlayUrl
play()
}
IntentAction.playNew -> {
exoPlayer.stop()
upPlayProgressJob?.cancel()
pause = false
position = 0
url = AudioPlay.durPlayUrl
play()
}
IntentAction.stopPlay -> {
exoPlayer.stop()
upPlayProgressJob?.cancel()
AudioPlay.status = Status.STOP
postEvent(EventBus.AUDIO_STATE, Status.STOP)
}
IntentAction.pause -> pause()
IntentAction.resume -> resume()
IntentAction.prev -> AudioPlay.prev()
IntentAction.next -> AudioPlay.next()
IntentAction.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f))
IntentAction.addTimer -> addTimer()
IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0))
IntentAction.adjustProgress -> {
adjustProgress(intent.getIntExtra("position", position))
}
IntentAction.stop -> stopSelf()
}
}
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
if (useWakeLock) {
wakeLock.release()
wifiLock?.release()
}
isRun = false
abandonFocus()
exoPlayer.release()
mediaSessionCompat?.release()
unregisterReceiver(broadcastReceiver)
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED)
AudioPlay.status = Status.STOP
postEvent(EventBus.AUDIO_STATE, Status.STOP)
AudioPlay.unregisterService()
upNotificationJob?.invokeOnCompletion {
notificationManager.cancel(NotificationId.AudioPlayService)
}
}
/**
* 播放音频
*/
@SuppressLint("WakelockTimeout")
private fun play() {
if (useWakeLock) {
wakeLock.acquire()
wifiLock?.acquire()
}
upAudioPlayNotification()
if (!requestFocus()) {
return
}
execute(context = Main) {
AudioPlay.status = Status.STOP
postEvent(EventBus.AUDIO_STATE, Status.STOP)
upPlayProgressJob?.cancel()
val analyzeUrl = AnalyzeUrl(
url,
source = AudioPlay.bookSource,
ruleData = AudioPlay.book,
chapter = AudioPlay.durChapter,
coroutineContext = coroutineContext
)
exoPlayer.setMediaItem(analyzeUrl.getMediaItem())
exoPlayer.playWhenReady = true
exoPlayer.seekTo(position.toLong())
exoPlayer.prepare()
}.onError {
AppLog.put("播放出错\n${it.localizedMessage}", it)
toastOnUi("$url ${it.localizedMessage}")
stopSelf()
}
}
/**
* 暂停播放
*/
private fun pause(abandonFocus: Boolean = true) {
if (useWakeLock) {
wakeLock.release()
wifiLock?.release()
}
try {
pause = true
if (abandonFocus) {
abandonFocus()
}
upPlayProgressJob?.cancel()
position = exoPlayer.currentPosition.toInt()
if (exoPlayer.isPlaying) exoPlayer.pause()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED)
AudioPlay.status = Status.PAUSE
postEvent(EventBus.AUDIO_STATE, Status.PAUSE)
upAudioPlayNotification()
} catch (e: Exception) {
e.printOnDebug()
}
}
/**
* 恢复播放
*/
@SuppressLint("WakelockTimeout")
private fun resume() {
if (useWakeLock) {
wakeLock.acquire()
wifiLock?.acquire()
}
try {
pause = false
if (url.isEmpty()) {
AudioPlay.loadOrUpPlayUrl()
return
}
if (!exoPlayer.isPlaying) {
exoPlayer.play()
}
upPlayProgress()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
AudioPlay.status = Status.PLAY
postEvent(EventBus.AUDIO_STATE, Status.PLAY)
upAudioPlayNotification()
} catch (e: Exception) {
e.printOnDebug()
stopSelf()
}
}
/**
* 调节进度
*/
private fun adjustProgress(position: Int) {
this.position = position
exoPlayer.seekTo(position.toLong())
}
/**
* 调节速度
*/
@SuppressLint(value = ["ObsoleteSdkInt"])
private fun upSpeed(speed: Float) {
kotlin.runCatching {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
playSpeed = speed
exoPlayer.setPlaybackSpeed(playSpeed)
postEvent(EventBus.AUDIO_SPEED, playSpeed)
}
}
}
/**
* 播放状态监控
*/
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
when (playbackState) {
Player.STATE_IDLE -> {
// 空闲
}
Player.STATE_BUFFERING -> {
// 缓冲中
}
Player.STATE_READY -> {
// 准备好
AudioPlay.upLoading(false)
if (exoPlayer.playWhenReady) {
AudioPlay.status = Status.PLAY
postEvent(EventBus.AUDIO_STATE, Status.PLAY)
} else {
AudioPlay.status = Status.PAUSE
postEvent(EventBus.AUDIO_STATE, Status.PAUSE)
}
postEvent(EventBus.AUDIO_SIZE, exoPlayer.duration.toInt())
upMediaMetadata()
upPlayProgress()
AudioPlay.saveDurChapter(exoPlayer.duration)
}
Player.STATE_ENDED -> {
// 结束
upPlayProgressJob?.cancel()
AudioPlay.playPositionChanged(exoPlayer.duration.toInt())
AudioPlay.next()
}
}
upAudioPlayNotification()
}
private fun upMediaMetadata() {
val metadata = MediaMetadataCompat.Builder()
.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, cover)
.putText(MediaMetadataCompat.METADATA_KEY_TITLE, AudioPlay.durChapter?.title ?: "null")
.putText(MediaMetadataCompat.METADATA_KEY_ARTIST, AudioPlay.book?.name ?: "null")
.putText(MediaMetadataCompat.METADATA_KEY_ALBUM, AudioPlay.book?.author ?: "null")
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, exoPlayer.duration)
.build()
mediaSessionCompat?.setMetadata(metadata)
}
/**
* 播放错误事件
*/
override fun onPlayerError(error: PlaybackException) {
super.onPlayerError(error)
AudioPlay.status = Status.STOP
postEvent(EventBus.AUDIO_STATE, Status.STOP)
AudioPlay.upLoading(false)
val errorMsg = "音频播放出错\n${error.errorCodeName} ${error.errorCode}"
AppLog.put(errorMsg, error)
toastOnUi(errorMsg)
}
private fun setTimer(minute: Int) {
timeMinute = minute
doDs()
}
private fun addTimer() {
if (timeMinute == 180) {
timeMinute = 0
} else {
timeMinute += 10
if (timeMinute > 180) timeMinute = 180
}
doDs()
}
/**
* 定时
*/
private fun doDs() {
postEvent(EventBus.AUDIO_DS, timeMinute)
upAudioPlayNotification()
dsJob?.cancel()
dsJob = lifecycleScope.launch {
while (isActive) {
delay(60000)
if (!pause) {
if (timeMinute >= 0) {
timeMinute--
}
if (timeMinute == 0) {
AudioPlay.stop()
postEvent(EventBus.AUDIO_DS, timeMinute)
break
}
}
postEvent(EventBus.AUDIO_DS, timeMinute)
upAudioPlayNotification()
}
}
}
/**
* 每隔1秒发送播放进度
*/
private fun upPlayProgress() {
upPlayProgressJob?.cancel()
upPlayProgressJob = lifecycleScope.launch {
while (isActive) {
//更新buffer位置
AudioPlay.playPositionChanged(exoPlayer.currentPosition.toInt())
postEvent(EventBus.AUDIO_BUFFER_PROGRESS, exoPlayer.bufferedPosition.toInt())
postEvent(EventBus.AUDIO_PROGRESS, AudioPlay.durChapterPos)
postEvent(EventBus.AUDIO_SIZE, exoPlayer.duration.toInt())
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
delay(1000)
}
}
}
/**
* 更新媒体状态
*/
private fun upMediaSessionPlaybackState(state: Int) {
mediaSessionCompat?.setPlaybackState(
PlaybackStateCompat.Builder()
.setActions(MEDIA_SESSION_ACTIONS)
.setState(state, exoPlayer.currentPosition, 1f)
.setBufferedPosition(exoPlayer.bufferedPosition)
.addCustomAction(
APP_ACTION_STOP,
getString(R.string.stop),
R.drawable.ic_stop_black_24dp
)
.addCustomAction(
APP_ACTION_TIMER,
getString(R.string.set_timer),
R.drawable.ic_time_add_24dp
)
.build()
)
}
/**
* 初始化MediaSession, 注册多媒体按钮
*/
@SuppressLint("UnspecifiedImmutableFlag")
private fun initMediaSession() {
mediaSessionCompat = MediaSessionCompat(this, "readAloud")
mediaSessionCompat?.setCallback(object : MediaSessionCompat.Callback() {
override fun onSeekTo(pos: Long) {
position = pos.toInt()
exoPlayer.seekTo(pos)
}
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
return MediaButtonReceiver.handleIntent(this@AudioPlayService, mediaButtonEvent)
}
override fun onPlay() = resume()
override fun onPause() = pause()
override fun onCustomAction(action: String?, extras: Bundle?) {
action ?: return
when (action) {
APP_ACTION_STOP -> stopSelf()
APP_ACTION_TIMER -> addTimer()
}
}
})
mediaSessionCompat?.setMediaButtonReceiver(
broadcastPendingIntent<MediaButtonReceiver>(Intent.ACTION_MEDIA_BUTTON)
)
mediaSessionCompat?.isActive = true
}
/**
* 断开耳机监听
*/
private fun initBroadcastReceiver() {
broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY == intent.action) {
pause()
}
}
}
val intentFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
registerReceiver(broadcastReceiver, intentFilter)
}
/**
* 音频焦点变化
*/
override fun onAudioFocusChange(focusChange: Int) {
if (AppConfig.ignoreAudioFocus) {
AppLog.put("忽略音频焦点处理(有声)")
return
}
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> {
if (needResumeOnAudioFocusGain) {
AppLog.put("音频焦点获得,继续播放")
resume()
} else {
AppLog.put("音频焦点获得")
}
}
AudioManager.AUDIOFOCUS_LOSS -> {
AppLog.put("音频焦点丢失,暂停播放")
pause()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
AppLog.put("音频焦点暂时丢失并会很快再次获得,暂停播放")
if (!pause) {
needResumeOnAudioFocusGain = true
pause(false)
}
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
// 短暂丢失焦点,这种情况是被其他应用申请了短暂的焦点希望其他声音能压低音量(或者关闭声音)凸显这个声音(比如短信提示音),
AppLog.put("音频焦点短暂丢失,不做处理")
}
}
}
private fun createNotification(): NotificationCompat.Builder {
var nTitle: String = when {
pause -> getString(R.string.audio_pause)
timeMinute in 1..60 -> getString(
R.string.playing_timer,
timeMinute
)
else -> getString(R.string.audio_play_t)
}
nTitle += ": ${AudioPlay.book?.name}"
var nSubtitle = AudioPlay.durChapter?.title
if (nSubtitle.isNullOrEmpty()) {
nSubtitle = getString(R.string.audio_play_s)
}
val builder = NotificationCompat
.Builder(this@AudioPlayService, AppConst.channelIdReadAloud)
.setSmallIcon(R.drawable.ic_volume_up)
.setSubText(getString(R.string.audio))
.setOngoing(true)
.setOnlyAlertOnce(true)
.setContentTitle(nTitle)
.setContentText(nSubtitle)
.setContentIntent(
activityPendingIntent<AudioPlayActivity>("activity")
)
builder.setLargeIcon(cover)
if (pause) {
builder.addAction(
R.drawable.ic_play,
getString(R.string.resume),
servicePendingIntent<AudioPlayService>(IntentAction.resume)
)
} else {
builder.addAction(
R.drawable.ic_pause,
getString(R.string.pause),
servicePendingIntent<AudioPlayService>(IntentAction.pause)
)
}
builder.addAction(
R.drawable.ic_stop_black_24dp,
getString(R.string.stop),
servicePendingIntent<AudioPlayService>(IntentAction.stop)
)
builder.addAction(
R.drawable.ic_time_add_24dp,
getString(R.string.set_timer),
servicePendingIntent<AudioPlayService>(IntentAction.addTimer)
)
builder.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setShowActionsInCompactView(0, 1, 2)
.setMediaSession(mediaSessionCompat?.sessionToken)
)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
return builder
}
private fun upAudioPlayNotification() {
upNotificationJob = execute {
try {
val notification = createNotification()
notificationManager.notify(NotificationId.AudioPlayService, notification.build())
} catch (e: Exception) {
AppLog.put("创建音频播放通知出错,${e.localizedMessage}", e, true)
}
}
}
/**
* 更新通知
*/
override fun startForegroundNotification() {
execute {
try {
val notification = createNotification()
startForeground(NotificationId.AudioPlayService, notification.build())
} catch (e: Exception) {
AppLog.put("创建音频播放通知出错,${e.localizedMessage}", e, true)
//创建通知出错不结束服务就会崩溃,服务必须绑定通知
stopSelf()
}
}
}
/**
* 请求音频焦点
* @return 音频焦点
*/
private fun requestFocus(): Boolean {
if (AppConfig.ignoreAudioFocus) {
return true
}
return MediaHelp.requestFocus(mFocusRequest)
}
/**
* 放弃音频焦点
*/
private fun abandonFocus() {
@Suppress("DEPRECATION")
audioManager.abandonAudioFocus(this)
}
}