From 23e584707344ea9b12c06f9511fcbde11eb5e5bc Mon Sep 17 00:00:00 2001
From: birdbird <6892457-tzugen@users.noreply.gitlab.com>
Date: Wed, 3 Aug 2022 16:02:44 +0000
Subject: [PATCH] Upgrade to Media3 Beta2

---
 .../api/subsonic/SubsonicAPIClient.kt         |   7 +-
 gradle/libs.versions.toml                     |   2 +-
 .../ultrasonic/playback/APIDataSource.kt      | 336 ------------------
 .../ultrasonic/playback/CachedDataSource.kt   |   1 +
 .../ultrasonic/playback/PlaybackService.kt    |  36 +-
 .../ultrasonic/service/CachedMusicService.kt  |   4 +-
 .../moire/ultrasonic/service/MusicService.kt  |   2 +-
 .../ultrasonic/service/OfflineMusicService.kt |   4 +-
 .../ultrasonic/service/RESTMusicService.kt    |  36 +-
 .../moire/ultrasonic/subsonic/VideoPlayer.kt  |   6 +-
 10 files changed, 62 insertions(+), 372 deletions(-)
 delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt

diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt
index a6d5b60e..75bb756b 100644
--- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt
+++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt
@@ -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(
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ec516055..d6912a5c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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"
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt
deleted file mode 100644
index 4c04f9df..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/APIDataSource.kt
+++ /dev/null
@@ -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
-    }
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt
index 54878505..8bbebc3c 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CachedDataSource.kt
@@ -80,6 +80,7 @@ class CachedDataSource(
         }
 
         // else forward the call to upstream
+        Timber.d("No cache hit, forwarding call")
         return upstreamDataSource.open(dataSpec)
     }
 
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt
index 9eff1722..0a200060 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt
@@ -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())
         }
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt
index 9386df37..34af142b 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt
@@ -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)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt
index 2099527c..ea9ed883 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt
@@ -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
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt
index 5d651054..5c438218 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt
@@ -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)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
index b2a53e75..5223c641 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
@@ -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)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt
index efd77833..6ad3fe0e 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt
@@ -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/*"