221 lines
7.0 KiB
Kotlin
221 lines
7.0 KiB
Kotlin
/*
|
|
* CachedDataSource.kt
|
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
|
*
|
|
* Distributed under terms of the GNU GPLv3 license.
|
|
*/
|
|
|
|
package org.moire.ultrasonic.playback
|
|
|
|
import android.net.Uri
|
|
import androidx.core.net.toUri
|
|
import androidx.media3.common.C
|
|
import androidx.media3.common.PlaybackException
|
|
import androidx.media3.common.util.Util
|
|
import androidx.media3.datasource.BaseDataSource
|
|
import androidx.media3.datasource.DataSource
|
|
import androidx.media3.datasource.DataSpec
|
|
import androidx.media3.datasource.HttpDataSource.HttpDataSourceException
|
|
import java.io.IOException
|
|
import java.io.InputStream
|
|
import java.io.InterruptedIOException
|
|
import org.moire.ultrasonic.util.AbstractFile
|
|
import org.moire.ultrasonic.util.FileUtil
|
|
import org.moire.ultrasonic.util.Storage
|
|
import timber.log.Timber
|
|
|
|
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
|
class CachedDataSource(
|
|
private var upstreamDataSource: DataSource
|
|
) : BaseDataSource(true) {
|
|
|
|
class Factory(
|
|
private var upstreamDataSourceFactory: DataSource.Factory
|
|
) : DataSource.Factory {
|
|
|
|
override fun createDataSource(): CachedDataSource {
|
|
return createDataSourceInternal(
|
|
upstreamDataSourceFactory.createDataSource()
|
|
)
|
|
}
|
|
|
|
private fun createDataSourceInternal(
|
|
upstreamDataSource: DataSource
|
|
): CachedDataSource {
|
|
return CachedDataSource(
|
|
upstreamDataSource
|
|
)
|
|
}
|
|
}
|
|
|
|
private var bytesToRead: Long = 0
|
|
private var bytesRead: Long = 0
|
|
private var dataSpec: DataSpec? = null
|
|
private var responseByteStream: InputStream? = null
|
|
private var openedFile = false
|
|
private var cachePath: String? = null
|
|
private var cacheFile: AbstractFile? = null
|
|
|
|
override fun open(dataSpec: DataSpec): Long {
|
|
Timber.i(
|
|
"CachedDatasource: Open: %s",
|
|
dataSpec.toString()
|
|
)
|
|
|
|
this.dataSpec = dataSpec
|
|
bytesRead = 0
|
|
bytesToRead = 0
|
|
|
|
val components = dataSpec.uri.toString().split('|')
|
|
val path = components[2]
|
|
val cacheLength = checkCache(path)
|
|
|
|
// We have found an item in the cache, return early
|
|
if (cacheLength > 0) {
|
|
transferInitializing(dataSpec)
|
|
bytesToRead = cacheLength
|
|
transferStarted(dataSpec)
|
|
skipFully(dataSpec.position, dataSpec)
|
|
return bytesToRead
|
|
}
|
|
|
|
// else forward the call to upstream
|
|
return upstreamDataSource.open(dataSpec)
|
|
}
|
|
|
|
@Suppress("MagicNumber")
|
|
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
|
|
// if (offset > 0 || length > 4)
|
|
// Timber.d("CachedDatasource: Read: %s %s", offset, length)
|
|
return if (cachePath != null) {
|
|
try {
|
|
readInternal(buffer, offset, length)
|
|
} catch (e: IOException) {
|
|
throw HttpDataSourceException.createForIOException(
|
|
e, Util.castNonNull(dataSpec), HttpDataSourceException.TYPE_READ
|
|
)
|
|
}
|
|
} else {
|
|
upstreamDataSource.read(buffer, offset, length)
|
|
}
|
|
}
|
|
|
|
private fun readInternal(buffer: ByteArray, offset: Int, readLength: Int): Int {
|
|
var readLengthCpy = readLength
|
|
if (readLengthCpy == 0) {
|
|
return 0
|
|
}
|
|
if (bytesToRead != C.LENGTH_UNSET.toLong()) {
|
|
val bytesRemaining = bytesToRead - bytesRead
|
|
if (bytesRemaining == 0L) {
|
|
return C.RESULT_END_OF_INPUT
|
|
}
|
|
readLengthCpy = readLengthCpy.toLong().coerceAtMost(bytesRemaining).toInt()
|
|
}
|
|
val read = Util.castNonNull(responseByteStream).read(buffer, offset, readLengthCpy)
|
|
if (read == -1) {
|
|
Timber.i("CachedDatasource: EndOfInput")
|
|
return C.RESULT_END_OF_INPUT
|
|
}
|
|
bytesRead += read.toLong()
|
|
bytesTransferred(read)
|
|
return read
|
|
}
|
|
|
|
/**
|
|
* Attempts to skip the specified number of bytes in full.
|
|
*
|
|
* @param bytesToSkip The number of bytes to skip.
|
|
* @param dataSpec The [DataSpec].
|
|
* @throws HttpDataSourceException If the thread is interrupted during the operation, or an error
|
|
* occurs while reading from the source, or if the data ended before skipping the specified
|
|
* number of bytes.
|
|
*/
|
|
@Suppress("ThrowsCount")
|
|
@Throws(HttpDataSourceException::class)
|
|
private fun skipFully(bytesToSkip: Long, dataSpec: DataSpec) {
|
|
var bytesToSkipCpy = bytesToSkip
|
|
if (bytesToSkipCpy == 0L) {
|
|
return
|
|
}
|
|
val skipBuffer = ByteArray(4096)
|
|
try {
|
|
while (bytesToSkipCpy > 0) {
|
|
val readLength =
|
|
bytesToSkipCpy.coerceAtMost(skipBuffer.size.toLong()).toInt()
|
|
val read = Util.castNonNull(responseByteStream).read(skipBuffer, 0, readLength)
|
|
if (Thread.currentThread().isInterrupted) {
|
|
throw InterruptedIOException()
|
|
}
|
|
if (read == -1) {
|
|
throw HttpDataSourceException(
|
|
dataSpec,
|
|
PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
|
|
HttpDataSourceException.TYPE_OPEN
|
|
)
|
|
}
|
|
bytesToSkipCpy -= read.toLong()
|
|
bytesTransferred(read)
|
|
}
|
|
return
|
|
} catch (e: IOException) {
|
|
if (e is HttpDataSourceException) {
|
|
throw e
|
|
} else {
|
|
throw HttpDataSourceException(
|
|
dataSpec,
|
|
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
|
|
HttpDataSourceException.TYPE_OPEN
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This method is called by StatsDataSource to verify that the loading succeeded,
|
|
* so its important that we return the correct value here..
|
|
*/
|
|
override fun getUri(): Uri? {
|
|
return cachePath?.toUri() ?: upstreamDataSource.uri
|
|
}
|
|
|
|
override fun close() {
|
|
Timber.i("CachedDatasource: close %s", openedFile)
|
|
if (openedFile) {
|
|
openedFile = false
|
|
transferEnded()
|
|
responseByteStream?.close()
|
|
responseByteStream = null
|
|
} else {
|
|
upstreamDataSource.close()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks our cache for a matching media file
|
|
*/
|
|
private fun checkCache(path: String): Long {
|
|
var filePath: String = path
|
|
var found = Storage.isPathExists(path)
|
|
|
|
if (!found) {
|
|
filePath = FileUtil.getCompleteFile(path)
|
|
found = Storage.isPathExists(filePath)
|
|
}
|
|
|
|
if (!found) return -1
|
|
|
|
cachePath = filePath
|
|
openedFile = true
|
|
|
|
cacheFile = Storage.getFromPath(filePath)!!
|
|
responseByteStream = cacheFile!!.getFileInputStream()
|
|
|
|
val descriptor = cacheFile!!.getDocumentFileDescriptor("r")
|
|
val length = descriptor!!.length
|
|
descriptor.close()
|
|
|
|
return length
|
|
}
|
|
}
|