我需要解密 AES/ECB/PCSK5Padding 加密的远程音频文件并使用 ExoPlayer 即时播放它。我实现了一个自定义
DataSource
来处理 read()
和 open()
方法。当音频文件从开头(位置 = 0)开始时它工作正常,但当它从其他位置开始或当我寻找音频时,密码会崩溃并裁剪文件,导致 ExoPlayer 错误。
通过搜索,我意识到问题涉及 CipherInputStream 的 skip()
方法,该方法应该在加密算法方面被覆盖。
有了以上条件,我该如何实现skip()
方法呢?
这里是
HttpCipherEncryptedDataSource
作为自定义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
}
}
还有
CipherHttpInputStream
处理skip
方法:
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()
}
}
经过几天的搜索和调试,最终我发现了问题所在。由于此 DataSource 负责播放远程加密音频文件,因此每次调用
open()
时,都会实例化一个新的 InputStream
来包装文件流,从 ExoPlayer 应该启动的确切位置开始。所以这种场景下就没有必要使用skip()
方法了。但是,该错误仍然引用文件的起始位置。至于CipherInputStream逐块读取上游(cipher.blockSize
),如果起始位置不能被cipher.blockSize
整除,则会导致ERROR_CODE_PARSING_CONTAINER_MALFORMED
ExoPlayer错误。
为了解决这个障碍,我编写了以下函数来计算有关密码块大小的适当起始位置并将其存储在新的 dataSpec
中。
这是函数:
// 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
)
}
应该用在
open
的开头,代替原来的dataSpec
。这是整个工作习惯DataSource
来源:
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,
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(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
}
}
companion object {
private const val MAX_BYTES_TO_DRAIN: Long = 2048
}
}