I need to play an encrypted remote audio file on the fly using ExoPlayer. I've implemented a custom DataSource
to handle read()
and open()
methods. It works fine when the audio file starts from the beginning (position = 0) but when it starts from some other position or when I seek the audio, the cipher breaks down and crops the file resulting in ExoPlayer error.
Through the search, I realized that the problem refers to the skip()
method of CipherInputStream which should get overridden regarding the encryption algorithm.
Having the above conditions, how should I implement the skip()
method?
Here is HttpCipherEncryptedDataSource
as the custom DataSource
:
class HttpCipherEncryptedDataSource(
private val key: ByteArray,
) : DataSource {
private val connectionMaker = HttpConnectionMaker()
private var connection: HttpURLConnection? = null
private var cipherInputStream: CipherHttpInputStream? = null
private var dataSpec: DataSpec? = null
private var uri: Uri? = null
private var bytesToRead: Long = 0
private var bytesRead: Long = 0
private var isOpen = false
override fun open(dataSpec: DataSpec): Long {
this.uri = dataSpec.uri
this.dataSpec = dataSpec
// make server connection
connection = connectionMaker.make(dataSpec)
val responseCode = connection!!.responseCode
val responseMessage = connection!!.responseMessage
// Check for a valid response code.
if (responseCode < 200 || responseCode > 299) {
val headers = connection!!.headerFields
if (responseCode == 416) {
val documentSize =
HttpUtil.getDocumentSize(connection!!.getHeaderField(HttpHeaders.CONTENT_RANGE))
if (dataSpec.position == documentSize) {
isOpen = true
return if (dataSpec.length != C.LENGTH_UNSET.toLong()) dataSpec.length else 0
}
}
val errorStream = connection!!.errorStream
val errorResponseBody = try {
if (errorStream != null) Util.toByteArray(errorStream) else Util.EMPTY_BYTE_ARRAY
} catch (e: IOException) {
Util.EMPTY_BYTE_ARRAY
}
connectionMaker.closeConnection()
val cause: IOException? =
if (responseCode == 416) DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE) else null
throw InvalidResponseCodeException(
responseCode, responseMessage, cause, headers, dataSpec, errorResponseBody
)
}
// calculate current position
val bytesToSkip =
if (responseCode == 200 && dataSpec.position != 0L) dataSpec.position else 0
// Determine the length of the data to be read, after skipping.
val isCompressed = isCompressed(connection!!)
if (!isCompressed) {
bytesToRead = if (dataSpec.length != C.LENGTH_UNSET.toLong()) {
dataSpec.length
} else {
val contentLength = HttpUtil.getContentLength(
connection!!.getHeaderField(HttpHeaders.CONTENT_LENGTH),
connection!!.getHeaderField(HttpHeaders.CONTENT_RANGE)
)
if (contentLength != C.LENGTH_UNSET.toLong()) contentLength - bytesToSkip else C.LENGTH_UNSET.toLong()
}
} else {
// Gzip is enabled. If the server opts to use gzip then the content length in the response
// will be that of the compressed data, which isn't what we want. Always use the dataSpec
// length in this case.
bytesToRead = dataSpec.length
}
var encryptedStream: InputStream?
try {
encryptedStream = connection!!.inputStream
if (isCompressed) {
encryptedStream = GZIPInputStream(encryptedStream)
}
setupCipherInputStream(encryptedStream!!)
cipherInputStream?.forceSkip(dataSpec.position)
} catch (e: IOException) {
connectionMaker.closeConnection()
throw HttpDataSourceException(
e,
dataSpec,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_OPEN
)
}
isOpen = true
return bytesToRead
}
private fun setupCipherInputStream(encryptedFileStream: InputStream) {
val keySpec = SecretKeySpec(
key,
"AES"
)
val cipher = Cipher.getInstance(
"AES/ECB/PCSK5Padding"
)
cipherInputStream = CipherHttpInputStream(
encryptedFileStream,
cipher,
keySpec
)
}
private fun isCompressed(connection: HttpURLConnection): Boolean {
val contentEncoding = connection.getHeaderField("Content-Encoding")
return "gzip".equals(contentEncoding, ignoreCase = true)
}
@Throws(HttpDataSourceException::class)
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
try {
var readLength = length
if (readLength == 0) {
return 0
}
if (bytesToRead != C.LENGTH_UNSET.toLong()) {
val bytesRemaining: Long = bytesToRead - bytesRead
if (bytesRemaining == 0L) {
return C.RESULT_END_OF_INPUT
}
readLength = Math.min(readLength.toLong(), bytesRemaining).toInt()
}
val read = Util.castNonNull<InputStream>(cipherInputStream).read(buffer, offset, readLength)
if (read == -1) {
return C.RESULT_END_OF_INPUT
}
bytesRead += read.toLong()
return read
} catch (e: IOException) {
throw HttpDataSourceException.createForIOException(
e, Util.castNonNull(dataSpec), HttpDataSourceException.TYPE_READ
)
}
}
override fun addTransferListener(transferListener: TransferListener) {}
override fun getUri() = uri
@Throws(HttpDataSourceException::class)
override fun close() {
try {
val inputStream: InputStream? = this.cipherInputStream
if (inputStream != null) {
val bytesRemaining =
if (bytesToRead == C.LENGTH_UNSET.toLong()) C.LENGTH_UNSET.toLong() else bytesToRead - bytesRead
maybeTerminateInputStream(connection, bytesRemaining)
try {
inputStream.close()
} catch (e: IOException) {
throw HttpDataSourceException(
e,
Util.castNonNull(dataSpec),
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_CLOSE
)
}
}
} finally {
cipherInputStream = null
connectionMaker.closeConnection()
if (isOpen) {
isOpen = false
}
}
}
private fun maybeTerminateInputStream(connection: HttpURLConnection?, bytesRemaining: Long) {
if (connection == null || Util.SDK_INT < 19 || Util.SDK_INT > 20) {
return
}
try {
val inputStream = connection.inputStream
if (bytesRemaining == C.LENGTH_UNSET.toLong()) {
// If the input stream has already ended, do nothing. The socket may be re-used.
if (inputStream.read() == -1) {
return
}
} else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {
// There isn't much data left. Prefer to allow it to drain, which may allow the socket to be
// re-used.
return
}
val className = inputStream.javaClass.name
if ("com.android.okhttp.internal.http.HttpTransport\$ChunkedInputStream" == className
|| ("com.android.okhttp.internal.http.HttpTransport\$FixedLengthInputStream"
== className)
) {
val superclass: Class<in InputStream>? = inputStream.javaClass.superclass
val unexpectedEndOfInput =
Assertions.checkNotNull(superclass).getDeclaredMethod("unexpectedEndOfInput")
unexpectedEndOfInput.isAccessible = true
unexpectedEndOfInput.invoke(inputStream)
}
} catch (e: Exception) {
// If an IOException then the connection didn't ever have an input stream, or it was closed
// already. If another type of exception then something went wrong, most likely the device
// isn't using okhttp.
e.printStackTrace()
}
}
companion object {
private const val MAX_BYTES_TO_DRAIN: Long = 2048
}
}
and also CipherHttpInputStream
to handle skip
method:
class CipherHttpInputStream(
private val upstream: InputStream,
private val cipher: Cipher,
private val secretKeySpec: SecretKeySpec,
) : CipherInputStream(upstream, cipher) {
private val MAX_SKIP_BUFFER_SIZE = 2048
fun forceSkip(bytesToSkip: Long) {
var remaining: Long = bytesToSkip
var nr: Int
if (bytesToSkip <= 0) {
return
}
val size = Math.min(MAX_SKIP_BUFFER_SIZE.toLong(), remaining).toInt()
val skipBuffer = ByteArray(size)
initCipher()
while (remaining > 0) {
nr = upstream.read(skipBuffer, 0, Math.min(size.toLong(), remaining).toInt())
if (nr < 0) {
break
}
remaining -= nr.toLong()
}
}
private fun initCipher() {
cipher.init(
Cipher.DECRYPT_MODE,
secretKeySpec,
)
}
override fun available(): Int {
return upstream.available()
}
}
After days of searching and debugging, eventually I came up with the problem. Since this DataSource is responsible for playing a remote encrypted Audio file, every time that open()
is called (particularly when seeking), a new InputStream
is instantiated wrapping the file streams starting at the exact position in which ExoPlayer is supposed to start. So there is no need to use the skip()
method in this scenario. However, the bug still refers back to the starting position of the file. As far as CipherInputStream reads the upstream block by block(cipher.blockSize
) if the starting position is not divisible by cipher.blockSize
, it will result in ERROR_CODE_PARSING_CONTAINER_MALFORMED
ExoPlayer error.
To tackle this obstacle, I wrote the following function to calculate the appropriate starting position regarding the cipher block size and store it inside a new dataSpec
.
Here is the function:
// check if the new position divided by cipher.blockSize
// results in zero. If not truncate the remaining.
private fun modifyBytesBlocks(dataSpec: DataSpec): DataSpec {
val bytesSinceStartOfCurrentBlock = dataSpec.position % cipher.blockSize
var bytesUntilPreviousBlockStart =
dataSpec.position - bytesSinceStartOfCurrentBlock - cipher.blockSize
if (bytesUntilPreviousBlockStart < 0) bytesUntilPreviousBlockStart = 0
return DataSpec(
dataSpec.uri,
dataSpec.httpMethod,
dataSpec.httpBody,
bytesUntilPreviousBlockStart,
bytesUntilPreviousBlockStart,
dataSpec.length,
dataSpec.key,
dataSpec.flags,
dataSpec.httpRequestHeaders
)
}
which should be used at the beginning of open
, replacing the original dataSpec
. Here is the whole working custom DataSource
source:
class HttpCipherEncryptedDataSource(key: ByteArray) : DataSource {
private val connectionMaker = HttpConnectionMaker()
private val keySpec = SecretKeySpec(
key,
"AES"
)
private val cipher = Cipher.getInstance(
"AES/ECB/PKCS5Padding"
)
private var connection: HttpURLConnection? = null
private var cipherInputStream: CipherHttpInputStream? = null
private var updatedDataSpec: DataSpec? = null
private var uri: Uri? = null
private var bytesToRead: Long = 0
private var bytesRead: Long = 0
private var isOpen = false
override fun open(dataSpec: DataSpec): Long {
bytesRead = 0
bytesToRead = 0
this.uri = dataSpec.uri
this.updatedDataSpec = modifyBytesBlocks(dataSpec)
val responseCode: Int
val responseMessage: String
try {
// make server connection
connection = connectionMaker.make(updatedDataSpec!!)
responseCode = connection!!.responseCode
responseMessage = connection!!.responseMessage
} catch (e: IOException) {
connectionMaker.closeConnection()
throw HttpDataSourceException.createForIOException(
e, dataSpec, HttpDataSourceException.TYPE_OPEN
)
}
// Check for a valid response code.
if (responseCode < 200 || responseCode > 299) {
val headers = connection!!.headerFields
if (responseCode == 416) {
val documentSize =
HttpUtil.getDocumentSize(connection!!.getHeaderField(HttpHeaders.CONTENT_RANGE))
if (updatedDataSpec!!.position == documentSize) {
isOpen = true
return if (updatedDataSpec!!.length != C.LENGTH_UNSET.toLong()) updatedDataSpec!!.length else 0
}
}
val errorStream = connection!!.errorStream
val errorResponseBody = try {
if (errorStream != null) Util.toByteArray(errorStream) else Util.EMPTY_BYTE_ARRAY
} catch (e: IOException) {
Util.EMPTY_BYTE_ARRAY
}
connectionMaker.closeConnection()
val cause: IOException? =
if (responseCode == 416) DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE) else null
throw InvalidResponseCodeException(
responseCode, responseMessage, cause, headers, updatedDataSpec!!, errorResponseBody
)
}
// calculate current position
val bytesToSkip =
if (responseCode == 200 && updatedDataSpec!!.position != 0L) updatedDataSpec!!.position else 0
// Determine the length of the data to be read, after skipping.
val isCompressed = isCompressed(connection!!)
if (!isCompressed) {
bytesToRead = if (updatedDataSpec!!.length != C.LENGTH_UNSET.toLong()) {
updatedDataSpec!!.length
} else {
val contentLength = HttpUtil.getContentLength(
connection!!.getHeaderField(HttpHeaders.CONTENT_LENGTH),
connection!!.getHeaderField(HttpHeaders.CONTENT_RANGE)
)
if (contentLength != C.LENGTH_UNSET.toLong()) contentLength - bytesToSkip else C.LENGTH_UNSET.toLong()
}
} else {
// Gzip is enabled. If the server opts to use gzip then the content length in the response
// will be that of the compressed data, which isn't what we want. Always use the dataSpec
// length in this case.
bytesToRead = updatedDataSpec!!.length
}
var encryptedStream: InputStream?
try {
encryptedStream = connection!!.inputStream
if (isCompressed) {
encryptedStream = GZIPInputStream(encryptedStream)
}
setupCipherInputStream(encryptedStream!!)
} catch (e: IOException) {
connectionMaker.closeConnection()
throw HttpDataSourceException(
e,
updatedDataSpec!!,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_OPEN
)
}
isOpen = true
return bytesToRead
}
// check if the new position divided by cipher.blockSize
// results in zero. If not truncate the remaining.
private fun modifyBytesBlocks(dataSpec: DataSpec): DataSpec {
val bytesSinceStartOfCurrentBlock = dataSpec.position % cipher.blockSize
var bytesUntilPreviousBlockStart =
dataSpec.position - bytesSinceStartOfCurrentBlock - cipher.blockSize
if (bytesUntilPreviousBlockStart < 0) bytesUntilPreviousBlockStart = 0
return DataSpec(
dataSpec.uri,
dataSpec.httpMethod,
dataSpec.httpBody,
bytesUntilPreviousBlockStart,
bytesUntilPreviousBlockStart,
dataSpec.length,
dataSpec.key,
dataSpec.flags,
dataSpec.httpRequestHeaders
)
}
private fun setupCipherInputStream(encryptedFileStream: InputStream) {
cipher.init(Cipher.DECRYPT_MODE, keySpec)
cipherInputStream = CipherHttpInputStream(
encryptedFileStream,
cipher
)
}
private fun isCompressed(connection: HttpURLConnection): Boolean {
val contentEncoding = connection.getHeaderField("Content-Encoding")
return "gzip".equals(contentEncoding, ignoreCase = true)
}
@Throws(HttpDataSourceException::class)
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
try {
var readLength = length
if (readLength == 0) {
return 0
}
if (bytesToRead != C.LENGTH_UNSET.toLong()) {
val bytesRemaining: Long = bytesToRead - bytesRead
if (bytesRemaining == 0L) {
return C.RESULT_END_OF_INPUT
}
readLength = Math.min(readLength.toLong(), bytesRemaining).toInt()
}
val read =
Util.castNonNull<InputStream>(cipherInputStream).read(buffer, offset, readLength)
if (read == -1) {
return C.RESULT_END_OF_INPUT
}
bytesRead += read.toLong()
return read
} catch (e: IOException) {
throw HttpDataSourceException.createForIOException(
e, Util.castNonNull(updatedDataSpec), HttpDataSourceException.TYPE_READ
)
}
}
override fun addTransferListener(transferListener: TransferListener) {}
override fun getUri() = uri
@Throws(HttpDataSourceException::class)
override fun close() {
try {
val inputStream: InputStream? = this.cipherInputStream
if (inputStream != null) {
val bytesRemaining =
if (bytesToRead == C.LENGTH_UNSET.toLong()) C.LENGTH_UNSET.toLong() else bytesToRead - bytesRead
maybeTerminateInputStream(connection, bytesRemaining)
try {
inputStream.close()
} catch (e: IOException) {
throw HttpDataSourceException(
e,
Util.castNonNull(updatedDataSpec),
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_CLOSE
)
}
}
} finally {
cipherInputStream = null
connectionMaker.closeConnection()
if (isOpen) {
isOpen = false
}
}
}
private fun maybeTerminateInputStream(connection: HttpURLConnection?, bytesRemaining: Long) {
if (connection == null || Util.SDK_INT < 19 || Util.SDK_INT > 20) {
return
}
try {
val inputStream = connection.inputStream
if (bytesRemaining == C.LENGTH_UNSET.toLong()) {
// If the input stream has already ended, do nothing. The socket may be re-used.
if (inputStream.read() == -1) {
return
}
} else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {
// There isn't much data left. Prefer to allow it to drain, which may allow the socket to be
// re-used.
return
}
val className = inputStream.javaClass.name
if ("com.android.okhttp.internal.http.HttpTransport\$ChunkedInputStream" == className
|| ("com.android.okhttp.internal.http.HttpTransport\$FixedLengthInputStream"
== className)
) {
val superclass: Class<in InputStream>? = inputStream.javaClass.superclass
val unexpectedEndOfInput =
Assertions.checkNotNull(superclass).getDeclaredMethod("unexpectedEndOfInput")
unexpectedEndOfInput.isAccessible = true
unexpectedEndOfInput.invoke(inputStream)
}
} catch (e: Exception) {
// If an IOException then the connection didn't ever have an input stream, or it was closed
// already. If another type of exception then something went wrong, most likely the device
// isn't using okhttp.
e.printStackTrace()
}
}
class HttpConnectionMaker {
private var defaultRequestProperties: HttpDataSource.RequestProperties? = null
private var requestProperties = HttpDataSource.RequestProperties()
private var readTimeoutMillis = DEFAULT_CONNECT_TIMEOUT_MILLIS
private var connectTimeoutMillis = DEFAULT_READ_TIMEOUT_MILLIS
private var allowCrossProtocolRedirects = false
private var keepPostFor302Redirects = false
private var connection: HttpURLConnection? = null
private var userAgent: String? = null
@Throws(IOException::class)
fun make(dataSpec: DataSpec): HttpURLConnection {
var url = URL(dataSpec.uri.toString())
var httpMethod: @DataSpec.HttpMethod Int = dataSpec.httpMethod
var httpBody = dataSpec.httpBody
val position = dataSpec.position
val length = dataSpec.length
val allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)
if (!allowCrossProtocolRedirects && !keepPostFor302Redirects) {
// HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection
// automatically. This is the behavior we want, so use it.
return make(
url,
httpMethod,
httpBody,
position,
length,
allowGzip, /* followRedirects= */
true,
dataSpec.httpRequestHeaders
)
}
// We need to handle redirects ourselves to allow cross-protocol redirects or to keep the POST
// request method for 302.
var redirectCount = 0
while (redirectCount++ <= MAX_REDIRECTS) {
connection = make(
url,
httpMethod,
httpBody,
position,
length,
allowGzip, /* followRedirects= */
false,
dataSpec.httpRequestHeaders
)
val responseCode = connection?.responseCode
val location = connection?.getHeaderField("Location")
if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD)
&& (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
|| responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP
|| responseCode == HttpURLConnection.HTTP_SEE_OTHER
|| responseCode == HTTP_STATUS_TEMPORARY_REDIRECT
|| responseCode == HTTP_STATUS_PERMANENT_REDIRECT)
) {
connection?.disconnect()
url = handleRedirect(url, location, dataSpec)
} else if (httpMethod == DataSpec.HTTP_METHOD_POST
&& (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
|| responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP
|| responseCode == HttpURLConnection.HTTP_SEE_OTHER)
) {
connection?.disconnect()
val shouldKeepPost =
keepPostFor302Redirects && responseCode == HttpURLConnection.HTTP_MOVED_TEMP
if (!shouldKeepPost) {
// POST request follows the redirect and is transformed into a GET request.
httpMethod = DataSpec.HTTP_METHOD_GET
httpBody = null
}
url = handleRedirect(url, location, dataSpec)
} else {
return connection!!
}
}
throw HttpDataSourceException(
NoRouteToHostException("Too many redirects: $redirectCount"),
dataSpec,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN
)
}
@Throws(IOException::class)
private fun make(
url: URL,
httpMethod: @DataSpec.HttpMethod Int,
httpBody: ByteArray?,
position: Long,
length: Long,
allowGzip: Boolean,
followRedirects: Boolean,
requestParameters: Map<String, String>,
): HttpURLConnection {
val connection = openConnection(url)
connection.connectTimeout = connectTimeoutMillis
connection.readTimeout = readTimeoutMillis
val requestHeaders: MutableMap<String, String> = HashMap()
if (defaultRequestProperties != null) {
requestHeaders.putAll(defaultRequestProperties!!.snapshot)
}
requestHeaders.putAll(requestProperties.snapshot)
requestHeaders.putAll(requestParameters)
for ((key, value) in requestHeaders) {
connection.setRequestProperty(key, value)
}
//header range
val rangeHeader = buildRangeRequestHeader(position, length)
if (rangeHeader != null) {
connection.setRequestProperty(HttpHeaders.RANGE, rangeHeader)
}
if (userAgent != null) {
connection.setRequestProperty(HttpHeaders.USER_AGENT, userAgent)
}
connection.setRequestProperty(
HttpHeaders.ACCEPT_ENCODING,
if (allowGzip) "gzip" else "identity"
)
connection.instanceFollowRedirects = followRedirects
connection.doOutput = httpBody != null
connection.requestMethod = DataSpec.getStringForHttpMethod(httpMethod)
if (httpBody != null) {
connection.setFixedLengthStreamingMode(httpBody.size)
connection.connect()
val os = connection.outputStream
os.write(httpBody)
os.close()
} else {
connection.connect()
}
return connection
}
private fun buildRangeRequestHeader(position: Long, length: Long): String? {
if (position == 0L && length == C.LENGTH_UNSET.toLong()) {
return null
}
val rangeValue = StringBuilder()
rangeValue.append("bytes=")
rangeValue.append(position)
rangeValue.append("-")
if (length != C.LENGTH_UNSET.toLong()) {
rangeValue.append(position + length - 1)
}
return rangeValue.toString()
}
@VisibleForTesting
@Throws(IOException::class)
fun openConnection(url: URL): HttpURLConnection {
return url.openConnection() as HttpURLConnection
}
@Throws(HttpDataSourceException::class)
private fun handleRedirect(originalUrl: URL, location: String?, dataSpec: DataSpec): URL {
if (location == null) {
throw HttpDataSourceException(
"Null location redirect",
dataSpec,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN
)
}
// Form the new url.
val url: URL = try {
URL(originalUrl, location)
} catch (e: MalformedURLException) {
throw HttpDataSourceException(
e,
dataSpec,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN
)
}
// Check that the protocol of the new url is supported.
val protocol = url.protocol
if ("https" != protocol && "http" != protocol) {
throw HttpDataSourceException(
"Unsupported protocol redirect: $protocol",
dataSpec,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN
)
}
if (!allowCrossProtocolRedirects && protocol != originalUrl.protocol) {
throw HttpDataSourceException(
"Disallowed cross-protocol redirect ("
+ originalUrl.protocol
+ " to "
+ protocol
+ ")",
dataSpec,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN
)
}
return url
}
fun closeConnection() {
if (connection != null) {
try {
connection?.disconnect()
} catch (e: Exception) {
e.printStackTrace()
}
connection = null
}
}
companion object {
private const val DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000
/** The default read timeout, in milliseconds. */
private const val DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000
private const val MAX_REDIRECTS = 20 // Same limit as okhttp.
private const val HTTP_STATUS_TEMPORARY_REDIRECT = 307
private const val HTTP_STATUS_PERMANENT_REDIRECT = 308
}
}
class CipherHttpInputStream(
private val upstream: InputStream,
cipher: Cipher,
) : CipherInputStream(upstream, cipher) {
@Throws(IOException::class)
override fun available(): Int {
return upstream.available()
}
}
companion object {
private const val MAX_BYTES_TO_DRAIN: Long = 2048
}
}