mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-16 09:20:37 +03:00
Merge branch 'beta2' into 'develop'
Upgrade to Media3 Beta2 See merge request ultrasonic/ultrasonic!799
This commit is contained in:
commit
f7b50d072d
@ -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(
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -80,6 +80,7 @@ class CachedDataSource(
|
||||
}
|
||||
|
||||
// else forward the call to upstream
|
||||
Timber.d("No cache hit, forwarding call")
|
||||
return upstreamDataSource.open(dataSpec)
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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/*"
|
||||
|
Loading…
x
Reference in New Issue
Block a user