Merge branch 'beta2' into 'develop'

Upgrade to Media3 Beta2

See merge request ultrasonic/ultrasonic!799
This commit is contained in:
Nite 2022-08-03 16:02:44 +00:00
commit f7b50d072d
10 changed files with 62 additions and 372 deletions

View File

@ -10,7 +10,6 @@ import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.ResponseBody
import okhttp3.logging.HttpLoggingInterceptor
import org.moire.ultrasonic.api.subsonic.interceptors.PasswordHexInterceptor
@ -66,8 +65,6 @@ class SubsonicAPIClient(
val okHttpClient: OkHttpClient = baseOkClient.newBuilder()
// Disable HTTP2 because OkHttp with Exoplayer causes a bug. See https://github.com/square/okhttp/issues/6749
// TODO Check if the bug is fixed and try to re-enable HTTP2
.protocols(listOf(Protocol.HTTP_1_1))
.readTimeout(READ_TIMEOUT, MILLISECONDS)
.apply { if (config.allowSelfSignedCertificate) allowSelfSignedCertificates() }
.addInterceptor { chain ->
@ -98,10 +95,12 @@ class SubsonicAPIClient(
.apply { if (config.debug) addLogging() }
.build()
val baseUrl = "${config.baseUrl}/rest/"
// Create the Retrofit instance, and register a special converter factory
// It will update our protocol version to the correct version, once we made a successful call
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("${config.baseUrl}/rest/")
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(
VersionAwareJacksonConverterFactory.create(

View File

@ -10,7 +10,7 @@ ktlintGradle = "10.2.0"
detekt = "1.19.0"
preferences = "1.1.1"
media = "1.3.1"
media3 = "1.0.0-beta01"
media3 = "1.0.0-beta02"
androidSupport = "1.4.0"
androidLegacySupport = "1.0.0"

View File

@ -1,336 +0,0 @@
/*
* APIDataSource.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.playback
import android.annotation.SuppressLint
import android.net.Uri
import androidx.core.net.toUri
import androidx.media3.common.C
import androidx.media3.common.PlaybackException
import androidx.media3.common.util.Assertions
import androidx.media3.common.util.Util
import androidx.media3.datasource.BaseDataSource
import androidx.media3.datasource.DataSourceException
import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.HttpDataSource.HttpDataSourceException
import androidx.media3.datasource.HttpDataSource.InvalidResponseCodeException
import androidx.media3.datasource.HttpDataSource.RequestProperties
import androidx.media3.datasource.HttpUtil
import androidx.media3.datasource.TransferListener
import com.google.common.net.HttpHeaders
import java.io.IOException
import java.io.InputStream
import java.io.InterruptedIOException
import okhttp3.Call
import okhttp3.ResponseBody
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
import org.moire.ultrasonic.api.subsonic.throwOnFailure
import org.moire.ultrasonic.api.subsonic.toStreamResponse
import timber.log.Timber
/**
* An [HttpDataSource] that delegates to Square's [Call.Factory].
*
*
* Note: HTTP request headers will be set using all parameters passed via (in order of decreasing
* priority) the `dataSpec`, [.setRequestProperty] and the default parameters used to
* construct the instance.
*/
@SuppressLint("UnsafeOptInUsageError")
@Suppress("MagicNumber")
open class APIDataSource private constructor(
subsonicAPIClient: SubsonicAPIClient
) : BaseDataSource(true),
HttpDataSource {
/** [DataSource.Factory] for [APIDataSource] instances. */
class Factory(private var subsonicAPIClient: SubsonicAPIClient) : HttpDataSource.Factory {
private val defaultRequestProperties: RequestProperties = RequestProperties()
private var transferListener: TransferListener? = null
override fun setDefaultRequestProperties(
defaultRequestProperties: Map<String, String>
): Factory {
this.defaultRequestProperties.clearAndSet(defaultRequestProperties)
return this
}
/**
* Sets the [TransferListener] that will be used.
*
* See [DataSource.addTransferListener].
*
* @param transferListener The listener that will be used.
* @return This factory.
*/
fun setTransferListener(transferListener: TransferListener?): Factory {
this.transferListener = transferListener
return this
}
fun setAPIClient(newClient: SubsonicAPIClient) {
this.subsonicAPIClient = newClient
}
override fun createDataSource(): APIDataSource {
val dataSource = APIDataSource(
subsonicAPIClient
)
if (transferListener != null) {
dataSource.addTransferListener(transferListener!!)
}
return dataSource
}
}
private val subsonicAPIClient: SubsonicAPIClient = Assertions.checkNotNull(subsonicAPIClient)
private val requestProperties: RequestProperties = RequestProperties()
private var dataSpec: DataSpec? = null
private var response: retrofit2.Response<ResponseBody>? = null
private var responseByteStream: InputStream? = null
private var openedNetwork = false
private var bytesToRead: Long = 0
private var bytesRead: Long = 0
override fun getUri(): Uri? {
return when (response) {
null -> null
else -> response!!.raw().request.url.toString().toUri()
}
}
override fun getResponseCode(): Int {
return if (response == null) -1 else response!!.code()
}
override fun getResponseHeaders(): Map<String, List<String>> {
return if (response == null) emptyMap() else response!!.headers().toMultimap()
}
override fun setRequestProperty(name: String, value: String) {
Assertions.checkNotNull(name)
Assertions.checkNotNull(value)
requestProperties[name] = value
}
override fun clearRequestProperty(name: String) {
Assertions.checkNotNull(name)
requestProperties.remove(name)
}
override fun clearAllRequestProperties() {
requestProperties.clear()
}
@Suppress("LongMethod", "NestedBlockDepth")
@Throws(HttpDataSourceException::class)
override fun open(dataSpec: DataSpec): Long {
Timber.i(
"APIDatasource: Open: %s %s %s",
dataSpec.uri,
dataSpec.position,
dataSpec.toString()
)
this.dataSpec = dataSpec
bytesRead = 0
bytesToRead = 0
transferInitializing(dataSpec)
val components = dataSpec.uri.toString().split('|')
val id = components[0]
val bitrate = components[1].toInt()
val request = subsonicAPIClient.api.stream(id, bitrate, offset = dataSpec.position)
val response: retrofit2.Response<ResponseBody>?
val streamResponse: StreamResponse
try {
this.response = request.execute()
response = this.response
streamResponse = response!!.toStreamResponse()
responseByteStream = streamResponse.stream
} catch (e: IOException) {
throw HttpDataSourceException.createForIOException(
e, dataSpec, HttpDataSourceException.TYPE_OPEN
)
}
streamResponse.throwOnFailure()
val responseCode = response.code()
// Check for a valid response code.
if (!response.isSuccessful) {
if (responseCode == 416) {
val documentSize =
HttpUtil.getDocumentSize(response.headers()[HttpHeaders.CONTENT_RANGE])
if (dataSpec.position == documentSize) {
openedNetwork = true
transferStarted(dataSpec)
return if (dataSpec.length != C.LENGTH_UNSET.toLong()) dataSpec.length else 0
}
}
val errorResponseBody: ByteArray = try {
Util.toByteArray(Assertions.checkNotNull(responseByteStream))
} catch (ignore: IOException) {
Util.EMPTY_BYTE_ARRAY
}
val headers = response.headers().toMultimap()
closeConnectionQuietly()
val cause: IOException? =
if (responseCode == 416) DataSourceException(
PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE
) else null
throw InvalidResponseCodeException(
responseCode, response.message(), cause, headers, dataSpec, errorResponseBody
)
}
// If we requested a range starting from a non-zero position and received a 200 rather than a
// 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position.
val bytesToSkip =
if (responseCode == 200 && dataSpec.position != 0L) dataSpec.position else 0
// Determine the length of the data to be read, after skipping.
bytesToRead = if (dataSpec.length != C.LENGTH_UNSET.toLong()) {
dataSpec.length
} else {
val contentLength = response.body()!!.contentLength()
if (contentLength != -1L) contentLength - bytesToSkip else C.LENGTH_UNSET.toLong()
}
openedNetwork = true
transferStarted(dataSpec)
try {
skipFully(bytesToSkip, dataSpec)
} catch (e: HttpDataSourceException) {
closeConnectionQuietly()
throw e
}
return bytesToRead
}
@Throws(HttpDataSourceException::class)
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
// Timber.d("APIDatasource: Read: %s %s", offset, length)
return try {
readInternal(buffer, offset, length)
} catch (e: IOException) {
throw HttpDataSourceException.createForIOException(
e, Util.castNonNull(dataSpec), HttpDataSourceException.TYPE_READ
)
}
}
override fun close() {
Timber.i("APIDatasource: Close")
if (openedNetwork) {
openedNetwork = false
transferEnded()
closeConnectionQuietly()
}
}
/**
* 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
)
}
}
}
/**
* Reads up to `length` bytes of data and stores them into `buffer`, starting at index
* `offset`.
*
*
* This method blocks until at least one byte of data can be read, the end of the opened range
* is detected, or an exception is thrown.
*
* @param buffer The buffer into which the read data should be stored.
* @param offset The start offset into `buffer` at which data should be written.
* @param readLength The maximum number of bytes to read.
* @return The number of bytes read, or [C.RESULT_END_OF_INPUT] if the end of the opened
* range is reached.
* @throws IOException If an error occurs reading from the source.
*/
@Throws(IOException::class)
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) {
return C.RESULT_END_OF_INPUT
}
bytesRead += read.toLong()
bytesTransferred(read)
return read
}
/** Closes the current connection quietly, if there is one. */
private fun closeConnectionQuietly() {
if (response != null) {
Assertions.checkNotNull(response!!.body()).close()
response = null
}
responseByteStream = null
}
}

View File

@ -80,6 +80,7 @@ class CachedDataSource(
}
// else forward the call to upstream
Timber.d("No cache hit, forwarding call")
return upstreamDataSource.open(dataSpec)
}

View File

@ -13,18 +13,20 @@ import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.C.USAGE_MEDIA
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.ResolvingDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import io.reactivex.rxjava3.disposables.CompositeDisposable
import okhttp3.OkHttpClient
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.activity.NavigationActivity
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign
import org.moire.ultrasonic.util.Constants
@ -34,7 +36,6 @@ import timber.log.Timber
class PlaybackService : MediaLibraryService(), KoinComponent {
private lateinit var player: ExoPlayer
private lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var apiDataSource: APIDataSource.Factory
private lateinit var librarySessionCallback: MediaLibrarySession.Callback
@ -82,17 +83,33 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
UApp.instance!!.shutdownKoin()
}
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
private val resolver: ResolvingDataSource.Resolver = ResolvingDataSource.Resolver {
val components = it.uri.toString().split('|')
val id = components[0]
val bitrate = components[1].toInt()
val uri = getMusicService().getStreamUrl(id, bitrate, null)!!
// AirSonic doesn't seem to stream correctly with the default
// icy-metadata headers set by media3, so remove them.
it.buildUpon().setUri(uri).setHttpRequestHeaders(emptyMap()).build()
}
private fun initializeSessionAndPlayer() {
if (isStarted) return
setMediaNotificationProvider(MediaNotificationProvider(UApp.applicationContext()))
val subsonicAPIClient: SubsonicAPIClient by inject()
// Create a new plain OkHttpClient
val builder = OkHttpClient.Builder()
val client = builder.build()
// Create a MediaSource which passes calls through our OkHttp Stack
apiDataSource = APIDataSource.Factory(subsonicAPIClient)
val cacheDataSourceFactory: DataSource.Factory = CachedDataSource.Factory(apiDataSource)
// Create the wrapped data sources:
// CachedDataSource is the first. If it cannot find a file,
// it will forward to ResolvingDataSource, which will create a URL through the resolver
// and pass it onto the OkHttpDataSource.
val okHttpDataSource = OkHttpDataSource.Factory(client)
val resolvingDataSource = ResolvingDataSource.Factory(okHttpDataSource, resolver)
val cacheDataSourceFactory: DataSource.Factory =
CachedDataSource.Factory(resolvingDataSource)
// Create a renderer with HW rendering support
val renderer = DefaultRenderersFactory(this)
@ -125,9 +142,6 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
// Set a listener to update the API client when the active server has changed
rxBusSubscription += RxBus.activeServerChangeObservable.subscribe {
val newClient: SubsonicAPIClient by inject()
apiDataSource.setAPIClient(newClient)
// Set the player wake mode
player.setWakeMode(getWakeModeFlag())
}

View File

@ -312,8 +312,8 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
}
@Throws(Exception::class)
override fun getVideoUrl(id: String): String? {
return musicService.getVideoUrl(id)
override fun getStreamUrl(id: String, maxBitRate: Int?, format: String?): String? {
return musicService.getStreamUrl(id, maxBitRate, format)
}
@Throws(Exception::class)

View File

@ -136,7 +136,7 @@ interface MusicService {
): Pair<InputStream, Boolean>
@Throws(Exception::class)
fun getVideoUrl(id: String): String?
fun getStreamUrl(id: String, maxBitRate: Int?, format: String?): String?
@Throws(Exception::class)
fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus

View File

@ -425,8 +425,8 @@ class OfflineMusicService : MusicService, KoinComponent {
}
@Throws(OfflineException::class)
override fun getVideoUrl(id: String): String? {
throw OfflineException("getVideoUrl isn't available in offline mode")
override fun getStreamUrl(id: String, maxBitRate: Int?, format: String?): String? {
throw OfflineException("getStreamUrl isn't available in offline mode")
}
@Throws(OfflineException::class)

View File

@ -467,9 +467,28 @@ open class RESTMusicService(
* call because that could take a long time.
*/
@Throws(Exception::class)
override fun getVideoUrl(
id: String
override fun getStreamUrl(
id: String,
maxBitRate: Int?,
format: String?
): String {
Timber.i("Start")
// Get the request from Retrofit, but don't execute it!
val request = API.stream(id).request()
// Create a new call with the request, and execute ist on our custom client
val response = streamClient.newCall(request).execute()
// The complete url :)
val url = response.request.url
Timber.i("Done")
return url.toString()
}
val streamClient by lazy {
// Create a new modified okhttp client to intercept the URL
val builder = subsonicAPIClient.okHttpClient.newBuilder()
@ -485,18 +504,7 @@ open class RESTMusicService(
}
// Create a new Okhttp client
val client = builder.build()
// Get the request from Retrofit, but don't execute it!
val request = API.stream(id, format = "raw").request()
// Create a new call with the request, and execute ist on our custom client
val response = client.newCall(request).execute()
// The complete url :)
val url = response.request.url
return url.toString()
builder.build()
}
@Throws(Exception::class)

View File

@ -21,7 +21,11 @@ class VideoPlayer {
}
try {
val intent = Intent(Intent.ACTION_VIEW)
val url = MusicServiceFactory.getMusicService().getVideoUrl(track.id)
val url = MusicServiceFactory.getMusicService().getStreamUrl(
track.id,
maxBitRate = null,
format = "raw"
)
intent.setDataAndType(
Uri.parse(url),
"video/*"