mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-04-06 02:37:18 +00:00
Add MediaBrowserPlaybackPreparer
This class will receive the media URLs generated by [MediaBrowserImpl] and will start playback of the corresponding streams or playlists. Co-authored-by: Haggai Eran <haggai.eran@gmail.com> Co-authored-by: Profpatsch <mail@profpatsch.de>
This commit is contained in:
parent
6cedd117fe
commit
3fcac10e7f
@ -0,0 +1,258 @@
|
||||
package org.schabi.newpipe.player.mediabrowser
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.ResultReceiver
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import android.util.Log
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.PlaybackPreparer
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.InfoItem.InfoType
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager
|
||||
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue
|
||||
import org.schabi.newpipe.util.ChannelTabHelper
|
||||
import org.schabi.newpipe.util.ExtractorHelper
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import java.util.function.BiConsumer
|
||||
|
||||
/**
|
||||
* This class is used to cleanly separate the Service implementation (in
|
||||
* [org.schabi.newpipe.player.PlayerService]) and the playback preparer implementation (in this
|
||||
* file). We currently use the playback preparer only in conjunction with the media browser: the
|
||||
* playback preparer will receive the media URLs generated by [MediaBrowserImpl] and will start
|
||||
* playback of the corresponding streams or playlists.
|
||||
*
|
||||
* @param setMediaSessionError takes an error String and an error code from [PlaybackStateCompat],
|
||||
* calls `sessionConnector.setCustomErrorMessage(errorString, errorCode)`
|
||||
* @param clearMediaSessionError calls `sessionConnector.setCustomErrorMessage(null)`
|
||||
*/
|
||||
class MediaBrowserPlaybackPreparer(
|
||||
private val context: Context,
|
||||
private val setMediaSessionError: BiConsumer<String, Int>, // error string, error code
|
||||
private val clearMediaSessionError: Runnable,
|
||||
) : PlaybackPreparer {
|
||||
private val database = NewPipeDatabase.getInstance(context)
|
||||
private var disposable: Disposable? = null
|
||||
|
||||
fun dispose() {
|
||||
disposable?.dispose()
|
||||
}
|
||||
|
||||
//region Overrides
|
||||
override fun getSupportedPrepareActions(): Long {
|
||||
return PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
|
||||
}
|
||||
|
||||
override fun onPrepare(playWhenReady: Boolean) {
|
||||
// TODO handle onPrepare
|
||||
}
|
||||
|
||||
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "onPrepareFromMediaId($mediaId, $playWhenReady, $extras)")
|
||||
}
|
||||
|
||||
disposable?.dispose()
|
||||
disposable = extractPlayQueueFromMediaId(mediaId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ playQueue ->
|
||||
clearMediaSessionError.run()
|
||||
NavigationHelper.playOnBackgroundPlayer(context, playQueue, playWhenReady)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.e(TAG, "Failed to start playback of media ID [$mediaId]", throwable)
|
||||
onPrepareError()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||
onUnsupportedError()
|
||||
}
|
||||
|
||||
override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) {
|
||||
onUnsupportedError()
|
||||
}
|
||||
|
||||
override fun onCommand(
|
||||
player: Player,
|
||||
command: String,
|
||||
extras: Bundle?,
|
||||
cb: ResultReceiver?
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Errors
|
||||
private fun onUnsupportedError() {
|
||||
setMediaSessionError.accept(
|
||||
context.getString(R.string.content_not_supported),
|
||||
PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED
|
||||
)
|
||||
}
|
||||
|
||||
private fun onPrepareError() {
|
||||
setMediaSessionError.accept(
|
||||
context.getString(R.string.error_snackbar_message),
|
||||
PlaybackStateCompat.ERROR_CODE_APP_ERROR
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Building play queues from playlists and history
|
||||
private fun extractLocalPlayQueue(playlistId: Long, index: Int): Single<PlayQueue> {
|
||||
return LocalPlaylistManager(database).getPlaylistStreams(playlistId).firstOrError()
|
||||
.map { items -> SinglePlayQueue(items.map { it.toStreamInfoItem() }, index) }
|
||||
}
|
||||
|
||||
private fun extractRemotePlayQueue(playlistId: Long, index: Int): Single<PlayQueue> {
|
||||
return RemotePlaylistManager(database).getPlaylist(playlistId).firstOrError()
|
||||
.flatMap { ExtractorHelper.getPlaylistInfo(it.serviceId, it.url, false) }
|
||||
.flatMap { info ->
|
||||
info.errors.firstOrNull { it !is ContentNotSupportedException }?.let {
|
||||
return@flatMap Single.error(it)
|
||||
}
|
||||
Single.just(PlaylistPlayQueue(info, index))
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractPlayQueueFromMediaId(mediaId: String): Single<PlayQueue> {
|
||||
try {
|
||||
val mediaIdUri = Uri.parse(mediaId)
|
||||
val path = ArrayList(mediaIdUri.pathSegments)
|
||||
if (path.isEmpty()) {
|
||||
throw parseError(mediaId)
|
||||
}
|
||||
|
||||
return when (/*val uriType = */path.removeAt(0)) {
|
||||
ID_BOOKMARKS -> extractPlayQueueFromPlaylistMediaId(
|
||||
mediaId,
|
||||
path,
|
||||
mediaIdUri.getQueryParameter(ID_URL) ?: throw parseError(mediaId)
|
||||
)
|
||||
|
||||
ID_HISTORY -> extractPlayQueueFromHistoryMediaId(mediaId, path)
|
||||
|
||||
ID_INFO_ITEM -> extractPlayQueueFromInfoItemMediaId(
|
||||
mediaId,
|
||||
path,
|
||||
mediaIdUri.getQueryParameter(ID_URL) ?: throw parseError(mediaId)
|
||||
)
|
||||
|
||||
else -> throw parseError(mediaId)
|
||||
}
|
||||
} catch (e: ContentNotAvailableException) {
|
||||
return Single.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ContentNotAvailableException::class)
|
||||
private fun extractPlayQueueFromPlaylistMediaId(
|
||||
mediaId: String,
|
||||
path: MutableList<String>,
|
||||
url: String,
|
||||
): Single<PlayQueue> {
|
||||
if (path.isEmpty()) {
|
||||
throw parseError(mediaId)
|
||||
}
|
||||
|
||||
when (val playlistType = path.removeAt(0)) {
|
||||
ID_LOCAL, ID_REMOTE -> {
|
||||
if (path.size != 2) {
|
||||
throw parseError(mediaId)
|
||||
}
|
||||
val playlistId = path[0].toLong()
|
||||
val index = path[1].toInt()
|
||||
return if (playlistType == ID_LOCAL)
|
||||
extractLocalPlayQueue(playlistId, index)
|
||||
else
|
||||
extractRemotePlayQueue(playlistId, index)
|
||||
}
|
||||
|
||||
ID_URL -> {
|
||||
if (path.size != 1) {
|
||||
throw parseError(mediaId)
|
||||
}
|
||||
|
||||
val serviceId = path[0].toInt()
|
||||
return ExtractorHelper.getPlaylistInfo(serviceId, url, false)
|
||||
.map { PlaylistPlayQueue(it) }
|
||||
}
|
||||
|
||||
else -> throw parseError(mediaId)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ContentNotAvailableException::class)
|
||||
private fun extractPlayQueueFromHistoryMediaId(
|
||||
mediaId: String,
|
||||
path: List<String>,
|
||||
): Single<PlayQueue> {
|
||||
if (path.size != 1) {
|
||||
throw parseError(mediaId)
|
||||
}
|
||||
|
||||
val streamId = path[0].toLong()
|
||||
return database.streamHistoryDAO().getHistory()
|
||||
.firstOrError()
|
||||
.map { items ->
|
||||
val infoItems = items
|
||||
.filter { it.streamId == streamId }
|
||||
.map { it.toStreamInfoItem() }
|
||||
SinglePlayQueue(infoItems, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ContentNotAvailableException::class)
|
||||
private fun extractPlayQueueFromInfoItemMediaId(
|
||||
mediaId: String,
|
||||
path: List<String>,
|
||||
url: String,
|
||||
): Single<PlayQueue> {
|
||||
if (path.size != 2) {
|
||||
throw parseError(mediaId)
|
||||
}
|
||||
|
||||
val serviceId = path[1].toInt()
|
||||
return when (/*val infoItemType = */infoItemTypeFromString(path[0])) {
|
||||
InfoType.STREAM -> ExtractorHelper.getStreamInfo(serviceId, url, false)
|
||||
.map { SinglePlayQueue(it) }
|
||||
|
||||
InfoType.PLAYLIST -> ExtractorHelper.getPlaylistInfo(serviceId, url, false)
|
||||
.map { PlaylistPlayQueue(it) }
|
||||
|
||||
InfoType.CHANNEL -> ExtractorHelper.getChannelInfo(serviceId, url, false)
|
||||
.map { info ->
|
||||
val playableTab = info.tabs
|
||||
.firstOrNull { ChannelTabHelper.isStreamsTab(it) }
|
||||
?: throw ContentNotAvailableException("No streams tab found")
|
||||
return@map ChannelTabPlayQueue(serviceId, ListLinkHandler(playableTab))
|
||||
}
|
||||
|
||||
else -> throw parseError(mediaId)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
companion object {
|
||||
private val TAG = MediaBrowserPlaybackPreparer::class.simpleName
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user