647 lines
24 KiB
Kotlin
647 lines
24 KiB
Kotlin
package io.legado.app.service
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.app.PendingIntent
|
|
import android.net.Uri
|
|
import androidx.core.net.toUri
|
|
import androidx.lifecycle.lifecycleScope
|
|
import androidx.media3.common.C
|
|
import androidx.media3.common.MediaItem
|
|
import androidx.media3.common.PlaybackException
|
|
import androidx.media3.common.Player
|
|
import androidx.media3.common.Timeline
|
|
import androidx.media3.database.StandaloneDatabaseProvider
|
|
import androidx.media3.datasource.DataSource
|
|
import androidx.media3.datasource.cache.CacheDataSink
|
|
import androidx.media3.datasource.cache.CacheDataSource
|
|
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
|
import androidx.media3.datasource.cache.SimpleCache
|
|
import androidx.media3.exoplayer.ExoPlayer
|
|
import androidx.media3.exoplayer.offline.DefaultDownloaderFactory
|
|
import androidx.media3.exoplayer.offline.DownloadRequest
|
|
import androidx.media3.exoplayer.offline.Downloader
|
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
|
import androidx.media3.exoplayer.source.MediaSource
|
|
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy
|
|
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
|
|
import com.script.ScriptException
|
|
import io.legado.app.R
|
|
import io.legado.app.constant.AppLog
|
|
import io.legado.app.constant.AppPattern
|
|
import io.legado.app.data.appDb
|
|
import io.legado.app.data.entities.HttpTTS
|
|
import io.legado.app.exception.NoStackTraceException
|
|
import io.legado.app.help.BookHelp
|
|
import io.legado.app.help.config.AppConfig
|
|
import io.legado.app.help.coroutine.Coroutine
|
|
import io.legado.app.help.exoplayer.InputStreamDataSource
|
|
import io.legado.app.help.http.okHttpClient
|
|
import io.legado.app.model.ReadAloud
|
|
import io.legado.app.model.ReadBook
|
|
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
|
import io.legado.app.ui.book.read.page.entities.TextChapter
|
|
import io.legado.app.utils.FileUtils
|
|
import io.legado.app.utils.MD5Utils
|
|
import io.legado.app.utils.printOnDebug
|
|
import io.legado.app.utils.servicePendingIntent
|
|
import io.legado.app.utils.toastOnUi
|
|
import kotlinx.coroutines.CancellationException
|
|
import kotlinx.coroutines.Dispatchers.Main
|
|
import kotlinx.coroutines.Job
|
|
import kotlinx.coroutines.channels.Channel
|
|
import kotlinx.coroutines.currentCoroutineContext
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.ensureActive
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.runBlocking
|
|
import kotlinx.coroutines.sync.Mutex
|
|
import kotlinx.coroutines.sync.withLock
|
|
import okhttp3.Response
|
|
import org.mozilla.javascript.WrappedException
|
|
import splitties.init.appCtx
|
|
import java.io.File
|
|
import java.io.InputStream
|
|
import java.net.ConnectException
|
|
import java.net.SocketTimeoutException
|
|
import kotlin.coroutines.coroutineContext
|
|
|
|
/**
|
|
* 在线朗读
|
|
* (已修改:支持跨章节预缓存 + 自定义清理时间)
|
|
*/
|
|
@SuppressLint("UnsafeOptInUsageError")
|
|
class HttpReadAloudService : BaseReadAloudService(),
|
|
Player.Listener {
|
|
private val exoPlayer: ExoPlayer by lazy {
|
|
ExoPlayer.Builder(this).build()
|
|
}
|
|
private val ttsFolderPath: String by lazy {
|
|
cacheDir.absolutePath + File.separator + "httpTTS" + File.separator
|
|
}
|
|
private val cache by lazy {
|
|
SimpleCache(
|
|
File(cacheDir, "httpTTS_cache"),
|
|
LeastRecentlyUsedCacheEvictor(128 * 1024 * 1024),
|
|
StandaloneDatabaseProvider(appCtx)
|
|
)
|
|
}
|
|
private val cacheDataSinkFactory by lazy {
|
|
CacheDataSink.Factory()
|
|
.setCache(cache)
|
|
}
|
|
private val loadErrorHandlingPolicy by lazy {
|
|
CustomLoadErrorHandlingPolicy()
|
|
}
|
|
private var speechRate: Int = AppConfig.speechRatePlay + 5
|
|
private var downloadTask: Coroutine<*>? = null
|
|
private var playIndexJob: Job? = null
|
|
private var downloadErrorNo: Int = 0
|
|
private var playErrorNo = 0
|
|
private val downloadTaskActiveLock = Mutex()
|
|
|
|
override fun onCreate() {
|
|
super.onCreate()
|
|
exoPlayer.addListener(this)
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
super.onDestroy()
|
|
downloadTask?.cancel()
|
|
exoPlayer.release()
|
|
cache.release()
|
|
Coroutine.async {
|
|
removeCacheFile()
|
|
}
|
|
}
|
|
|
|
override fun play() {
|
|
pageChanged = false
|
|
exoPlayer.stop()
|
|
if (!requestFocus()) return
|
|
if (contentList.isEmpty()) {
|
|
AppLog.putDebug("朗读列表为空")
|
|
ReadBook.readAloud()
|
|
} else {
|
|
super.play()
|
|
if (AppConfig.streamReadAloudAudio) {
|
|
downloadAndPlayAudiosStream()
|
|
} else {
|
|
downloadAndPlayAudios()
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun playStop() {
|
|
exoPlayer.stop()
|
|
playIndexJob?.cancel()
|
|
}
|
|
|
|
private fun updateNextPos() {
|
|
readAloudNumber += contentList[nowSpeak].length + 1 - paragraphStartPos
|
|
paragraphStartPos = 0
|
|
if (nowSpeak < contentList.lastIndex) {
|
|
nowSpeak++
|
|
} else {
|
|
nextChapter()
|
|
}
|
|
}
|
|
|
|
private fun downloadAndPlayAudios() {
|
|
exoPlayer.clearMediaItems()
|
|
downloadTask?.cancel()
|
|
downloadTask = execute {
|
|
downloadTaskActiveLock.withLock {
|
|
ensureActive()
|
|
val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null")
|
|
|
|
// 1. 处理当前正在播放的章节
|
|
contentList.forEachIndexed { index, content ->
|
|
ensureActive()
|
|
if (index < nowSpeak) return@forEachIndexed
|
|
var text = content
|
|
if (paragraphStartPos > 0 && index == nowSpeak) {
|
|
text = text.substring(paragraphStartPos)
|
|
}
|
|
val fileName = md5SpeakFileName(text)
|
|
val speakText = text.replace(AppPattern.notReadAloudRegex, "")
|
|
if (speakText.isEmpty()) {
|
|
AppLog.put("阅读段落内容为空,使用无声音频代替。\n朗读文本:$text")
|
|
createSilentSound(fileName)
|
|
} else if (!hasSpeakFile(fileName)) {
|
|
runCatching {
|
|
val inputStream = getSpeakStream(httpTts, speakText)
|
|
if (inputStream != null) {
|
|
createSpeakFile(fileName, inputStream)
|
|
} else {
|
|
createSilentSound(fileName)
|
|
}
|
|
}.onFailure {
|
|
when (it) {
|
|
is CancellationException -> Unit
|
|
else -> pauseReadAloud()
|
|
}
|
|
return@execute
|
|
}
|
|
}
|
|
val file = getSpeakFileAsMd5(fileName)
|
|
val mediaItem = MediaItem.fromUri(Uri.fromFile(file))
|
|
launch(Main) {
|
|
exoPlayer.addMediaItem(mediaItem)
|
|
}
|
|
}
|
|
// 2. 触发预下载 (新逻辑)
|
|
preDownloadAudios(httpTts)
|
|
}
|
|
}.onError {
|
|
AppLog.put("朗读下载出错\n${it.localizedMessage}", it, true)
|
|
}
|
|
}
|
|
|
|
// ================================================================
|
|
// 修改点:强力预缓存逻辑 (循环查库,跨章节)
|
|
// ================================================================
|
|
private suspend fun preDownloadAudios(httpTts: HttpTTS) {
|
|
val book = ReadBook.book ?: return
|
|
// 获取当前章节索引
|
|
val currentIdx = ReadBook.textChapter?.chapterIndex ?: return
|
|
// 获取用户设置的预加载数量 (例如 50)
|
|
val limit = AppConfig.audioPreDownloadNum
|
|
|
|
// 循环:从下一章开始,往后找 limit 个章节
|
|
for (i in 1..limit) {
|
|
currentCoroutineContext().ensureActive()
|
|
|
|
// 计算目标章节索引
|
|
val targetIndex = currentIdx + i
|
|
|
|
// 查数据库获取章节信息 (如果超出了总章数会返回空,就停止)
|
|
val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, targetIndex) ?: break
|
|
|
|
// 获取章节内容 (这里假设已缓存文本,或者尝试读取)
|
|
val contentString = BookHelp.getContent(book, chapter)
|
|
|
|
// 如果内容为空 (还没下载正文),就跳过,不强行下载正文以免阻塞
|
|
if (contentString.isNullOrEmpty()) continue
|
|
|
|
// 模拟 TextChapter 的分割逻辑,转成段落列表
|
|
val contentList = contentString.split("\n").filter { it.isNotEmpty() }
|
|
|
|
// 逐段检查并下载音频
|
|
contentList.forEach { content ->
|
|
currentCoroutineContext().ensureActive()
|
|
// 注意:跨章节时 MD5 需要基于 targetIndex 对应的 title (虽然 MD5Utils.md5Encode16(chapter.title) 可能和 TextChapter 不完全一致,但通常足够)
|
|
// 为了保险,我们手动拼装 MD5 文件名逻辑
|
|
val titleMd5 = MD5Utils.md5Encode16(chapter.title)
|
|
val contentMd5 = MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content")
|
|
val fileName = "${titleMd5}_${contentMd5}"
|
|
|
|
val speakText = content.replace(AppPattern.notReadAloudRegex, "")
|
|
if (speakText.isEmpty()) {
|
|
createSilentSound(fileName)
|
|
} else if (!hasSpeakFile(fileName)) {
|
|
runCatching {
|
|
val inputStream = getSpeakStream(httpTts, speakText)
|
|
if (inputStream != null) {
|
|
createSpeakFile(fileName, inputStream)
|
|
} else {
|
|
createSilentSound(fileName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun downloadAndPlayAudiosStream() {
|
|
exoPlayer.clearMediaItems()
|
|
downloadTask?.cancel()
|
|
downloadTask = execute {
|
|
downloadTaskActiveLock.withLock {
|
|
ensureActive()
|
|
val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null")
|
|
val downloaderChannel = Channel<Downloader>()
|
|
launch {
|
|
for (downloader in downloaderChannel) {
|
|
downloader.download(null)
|
|
}
|
|
}
|
|
contentList.forEachIndexed { index, content ->
|
|
ensureActive()
|
|
if (index < nowSpeak) return@forEachIndexed
|
|
var text = content
|
|
if (paragraphStartPos > 0 && index == nowSpeak) {
|
|
text = text.substring(paragraphStartPos)
|
|
}
|
|
val speakText = text.replace(AppPattern.notReadAloudRegex, "")
|
|
if (speakText.isEmpty()) {
|
|
AppLog.put("阅读段落内容为空,使用无声音频代替。\n朗读文本:$speakText")
|
|
}
|
|
val fileName = md5SpeakFileName(text)
|
|
val dataSourceFactory = createDataSourceFactory(httpTts, speakText)
|
|
val downloader = createDownloader(dataSourceFactory, fileName)
|
|
downloaderChannel.send(downloader)
|
|
val mediaSource = createMediaSource(dataSourceFactory, fileName)
|
|
launch(Main) {
|
|
exoPlayer.addMediaSource(mediaSource)
|
|
}
|
|
}
|
|
preDownloadAudiosStream(httpTts, downloaderChannel)
|
|
}
|
|
}.onError {
|
|
AppLog.put("朗读下载出错\n${it.localizedMessage}", it, true)
|
|
}
|
|
}
|
|
|
|
// ================================================================
|
|
// 修改点:流式预缓存逻辑 (循环查库,跨章节)
|
|
// ================================================================
|
|
private suspend fun preDownloadAudiosStream(
|
|
httpTts: HttpTTS,
|
|
downloaderChannel: Channel<Downloader>
|
|
) {
|
|
val book = ReadBook.book ?: return
|
|
val currentIdx = ReadBook.textChapter?.chapterIndex ?: return
|
|
val limit = AppConfig.audioPreDownloadNum
|
|
|
|
for (i in 1..limit) {
|
|
currentCoroutineContext().ensureActive()
|
|
val targetIndex = currentIdx + i
|
|
val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, targetIndex) ?: break
|
|
val contentString = BookHelp.getContent(book, chapter)
|
|
if (contentString.isNullOrEmpty()) continue
|
|
|
|
val contentList = contentString.split("\n").filter { it.isNotEmpty() }
|
|
|
|
contentList.forEach { content ->
|
|
currentCoroutineContext().ensureActive()
|
|
val titleMd5 = MD5Utils.md5Encode16(chapter.title)
|
|
val contentMd5 = MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content")
|
|
val fileName = "${titleMd5}_${contentMd5}"
|
|
|
|
val speakText = content.replace(AppPattern.notReadAloudRegex, "")
|
|
val dataSourceFactory = createDataSourceFactory(httpTts, speakText)
|
|
val downloader = createDownloader(dataSourceFactory, fileName)
|
|
downloaderChannel.send(downloader)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun createDataSourceFactory(
|
|
httpTts: HttpTTS,
|
|
speakText: String
|
|
): CacheDataSource.Factory {
|
|
val upstreamFactory = DataSource.Factory {
|
|
InputStreamDataSource {
|
|
if (speakText.isEmpty()) {
|
|
null
|
|
} else {
|
|
kotlin.runCatching {
|
|
runBlocking(lifecycleScope.coroutineContext[Job]!!) {
|
|
getSpeakStream(httpTts, speakText)
|
|
}
|
|
}.onFailure {
|
|
when (it) {
|
|
is InterruptedException,
|
|
is CancellationException -> Unit
|
|
|
|
else -> pauseReadAloud()
|
|
}
|
|
}.getOrThrow()
|
|
} ?: resources.openRawResource(R.raw.silent_sound)
|
|
}
|
|
}
|
|
val factory = CacheDataSource.Factory()
|
|
.setCache(cache)
|
|
.setUpstreamDataSourceFactory(upstreamFactory)
|
|
.setCacheWriteDataSinkFactory(cacheDataSinkFactory)
|
|
return factory
|
|
}
|
|
|
|
private fun createDownloader(factory: CacheDataSource.Factory, fileName: String): Downloader {
|
|
val uri = fileName.toUri()
|
|
val request = DownloadRequest.Builder(fileName, uri).build()
|
|
return DefaultDownloaderFactory(factory, okHttpClient.dispatcher.executorService)
|
|
.createDownloader(request)
|
|
}
|
|
|
|
private fun createMediaSource(factory: DataSource.Factory, fileName: String): MediaSource {
|
|
return DefaultMediaSourceFactory(this)
|
|
.setDataSourceFactory(factory)
|
|
.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
|
|
.createMediaSource(MediaItem.fromUri(fileName))
|
|
}
|
|
|
|
private suspend fun getSpeakStream(
|
|
httpTts: HttpTTS,
|
|
speakText: String
|
|
): InputStream? {
|
|
while (true) {
|
|
try {
|
|
val analyzeUrl = AnalyzeUrl(
|
|
httpTts.url,
|
|
speakText = speakText,
|
|
speakSpeed = speechRate,
|
|
source = httpTts,
|
|
readTimeout = 300 * 1000L,
|
|
coroutineContext = currentCoroutineContext()
|
|
)
|
|
var response = analyzeUrl.getResponseAwait()
|
|
currentCoroutineContext().ensureActive()
|
|
val checkJs = httpTts.loginCheckJs
|
|
if (checkJs?.isNotBlank() == true) {
|
|
response = analyzeUrl.evalJS(checkJs, response) as Response
|
|
}
|
|
response.headers["Content-Type"]?.let { contentType ->
|
|
val contentType = contentType.substringBefore(";")
|
|
val ct = httpTts.contentType
|
|
if (contentType == "application/json" || contentType.startsWith("text/")) {
|
|
throw NoStackTraceException(response.body.string())
|
|
} else if (ct?.isNotBlank() == true) {
|
|
if (!contentType.matches(ct.toRegex())) {
|
|
throw NoStackTraceException(
|
|
"TTS服务器返回错误:" + response.body.string()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
currentCoroutineContext().ensureActive()
|
|
response.body.byteStream().let { stream ->
|
|
downloadErrorNo = 0
|
|
return stream
|
|
}
|
|
} catch (e: Exception) {
|
|
when (e) {
|
|
is CancellationException -> throw e
|
|
is ScriptException, is WrappedException -> {
|
|
AppLog.put("js错误\n${e.localizedMessage}", e, true)
|
|
e.printOnDebug()
|
|
throw e
|
|
}
|
|
|
|
is SocketTimeoutException, is ConnectException -> {
|
|
downloadErrorNo++
|
|
if (downloadErrorNo > 5) {
|
|
val msg = "tts超时或连接错误超过5次\n${e.localizedMessage}"
|
|
AppLog.put(msg, e, true)
|
|
throw e
|
|
}
|
|
}
|
|
|
|
else -> {
|
|
downloadErrorNo++
|
|
val msg = "tts下载错误\n${e.localizedMessage}"
|
|
AppLog.put(msg, e)
|
|
e.printOnDebug()
|
|
if (downloadErrorNo > 5) {
|
|
val msg1 = "TTS服务器连续5次错误,已暂停阅读。"
|
|
AppLog.put(msg1, e, true)
|
|
throw e
|
|
} else {
|
|
AppLog.put("TTS下载音频出错,使用无声音频代替。\n朗读文本:$speakText")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
private fun md5SpeakFileName(content: String, textChapter: TextChapter? = this.textChapter): String {
|
|
return MD5Utils.md5Encode16(textChapter?.title ?: "") + "_" +
|
|
MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content")
|
|
}
|
|
|
|
private fun createSilentSound(fileName: String) {
|
|
val file = createSpeakFile(fileName)
|
|
file.writeBytes(resources.openRawResource(R.raw.silent_sound).readBytes())
|
|
}
|
|
|
|
private fun hasSpeakFile(name: String): Boolean {
|
|
return FileUtils.exist("${ttsFolderPath}$name.mp3")
|
|
}
|
|
|
|
private fun getSpeakFileAsMd5(name: String): File {
|
|
return File("${ttsFolderPath}$name.mp3")
|
|
}
|
|
|
|
private fun createSpeakFile(name: String): File {
|
|
return FileUtils.createFileIfNotExist("${ttsFolderPath}$name.mp3")
|
|
}
|
|
|
|
private fun createSpeakFile(name: String, inputStream: InputStream) {
|
|
FileUtils.createFileIfNotExist("${ttsFolderPath}$name.mp3").outputStream().use { out ->
|
|
inputStream.use {
|
|
it.copyTo(out)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 移除缓存文件 (支持自定义清理时间)
|
|
*/
|
|
private fun removeCacheFile() {
|
|
val titleMd5 = MD5Utils.md5Encode16(textChapter?.title ?: "")
|
|
FileUtils.listDirsAndFiles(ttsFolderPath)?.forEach {
|
|
val isSilentSound = it.length() == 2160L
|
|
if ((!it.name.startsWith(titleMd5)
|
|
&& System.currentTimeMillis() - it.lastModified() > AppConfig.audioCacheCleanTime)
|
|
|| isSilentSound
|
|
) {
|
|
FileUtils.delete(it.absolutePath)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
override fun pauseReadAloud(abandonFocus: Boolean) {
|
|
super.pauseReadAloud(abandonFocus)
|
|
kotlin.runCatching {
|
|
playIndexJob?.cancel()
|
|
exoPlayer.pause()
|
|
}
|
|
}
|
|
|
|
override fun resumeReadAloud() {
|
|
super.resumeReadAloud()
|
|
kotlin.runCatching {
|
|
if (pageChanged) {
|
|
play()
|
|
} else {
|
|
exoPlayer.play()
|
|
upPlayPos()
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun upPlayPos() {
|
|
playIndexJob?.cancel()
|
|
val textChapter = textChapter ?: return
|
|
playIndexJob = lifecycleScope.launch {
|
|
upTtsProgress(readAloudNumber + 1)
|
|
if (exoPlayer.duration <= 0) {
|
|
return@launch
|
|
}
|
|
val speakTextLength = contentList[nowSpeak].length
|
|
if (speakTextLength <= 0) {
|
|
return@launch
|
|
}
|
|
val sleep = exoPlayer.duration / speakTextLength
|
|
val start = speakTextLength * exoPlayer.currentPosition / exoPlayer.duration
|
|
for (i in start..contentList[nowSpeak].length) {
|
|
if (pageIndex + 1 < textChapter.pageSize
|
|
&& readAloudNumber + i > textChapter.getReadLength(pageIndex + 1)
|
|
) {
|
|
pageIndex++
|
|
ReadBook.moveToNextPage()
|
|
upTtsProgress(readAloudNumber + i.toInt())
|
|
}
|
|
delay(sleep)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 更新朗读速度
|
|
*/
|
|
override fun upSpeechRate(reset: Boolean) {
|
|
downloadTask?.cancel()
|
|
exoPlayer.stop()
|
|
speechRate = AppConfig.speechRatePlay + 5
|
|
if (AppConfig.streamReadAloudAudio) {
|
|
downloadAndPlayAudiosStream()
|
|
} else {
|
|
downloadAndPlayAudios()
|
|
}
|
|
}
|
|
|
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
|
super.onPlaybackStateChanged(playbackState)
|
|
when (playbackState) {
|
|
Player.STATE_IDLE -> {
|
|
// 空闲
|
|
}
|
|
|
|
Player.STATE_BUFFERING -> {
|
|
// 缓冲中
|
|
}
|
|
|
|
Player.STATE_READY -> {
|
|
// 准备好
|
|
if (pause) return
|
|
exoPlayer.play()
|
|
upPlayPos()
|
|
}
|
|
|
|
Player.STATE_ENDED -> {
|
|
// 结束
|
|
playErrorNo = 0
|
|
updateNextPos()
|
|
exoPlayer.stop()
|
|
exoPlayer.clearMediaItems()
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
|
when (reason) {
|
|
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED -> {
|
|
if (!timeline.isEmpty && exoPlayer.playbackState == Player.STATE_IDLE) {
|
|
exoPlayer.prepare()
|
|
}
|
|
}
|
|
|
|
else -> {}
|
|
}
|
|
}
|
|
|
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) return
|
|
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
|
playErrorNo = 0
|
|
}
|
|
updateNextPos()
|
|
upPlayPos()
|
|
}
|
|
|
|
override fun onPlayerError(error: PlaybackException) {
|
|
super.onPlayerError(error)
|
|
AppLog.put("朗读错误\n${contentList[nowSpeak]}", error)
|
|
deleteCurrentSpeakFile()
|
|
playErrorNo++
|
|
if (playErrorNo >= 5) {
|
|
toastOnUi("朗读连续5次错误, 最后一次错误代码(${error.localizedMessage})")
|
|
AppLog.put("朗读连续5次错误, 最后一次错误代码(${error.localizedMessage})", error)
|
|
pauseReadAloud()
|
|
} else {
|
|
if (exoPlayer.hasNextMediaItem()) {
|
|
exoPlayer.seekToNextMediaItem()
|
|
exoPlayer.prepare()
|
|
} else {
|
|
exoPlayer.clearMediaItems()
|
|
updateNextPos()
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun deleteCurrentSpeakFile() {
|
|
if (AppConfig.streamReadAloudAudio) {
|
|
return
|
|
}
|
|
val mediaItem = exoPlayer.currentMediaItem ?: return
|
|
val filePath = mediaItem.localConfiguration!!.uri.path!!
|
|
File(filePath).delete()
|
|
}
|
|
|
|
override fun aloudServicePendingIntent(actionStr: String): PendingIntent? {
|
|
return servicePendingIntent<HttpReadAloudService>(actionStr)
|
|
}
|
|
|
|
class CustomLoadErrorHandlingPolicy : DefaultLoadErrorHandlingPolicy(0) {
|
|
override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorHandlingPolicy.LoadErrorInfo): Long {
|
|
return C.TIME_UNSET
|
|
}
|
|
}
|
|
|
|
}
|