Skip to content

Commit 3fcac10

Browse files
StypoxhaggaieProfpatsch
committed
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>
1 parent 6cedd11 commit 3fcac10

1 file changed

Lines changed: 258 additions & 0 deletions

File tree

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
package org.schabi.newpipe.player.mediabrowser
2+
3+
import android.content.Context
4+
import android.net.Uri
5+
import android.os.Bundle
6+
import android.os.ResultReceiver
7+
import android.support.v4.media.session.PlaybackStateCompat
8+
import android.util.Log
9+
import com.google.android.exoplayer2.Player
10+
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.PlaybackPreparer
11+
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
12+
import io.reactivex.rxjava3.core.Single
13+
import io.reactivex.rxjava3.disposables.Disposable
14+
import io.reactivex.rxjava3.schedulers.Schedulers
15+
import org.schabi.newpipe.MainActivity
16+
import org.schabi.newpipe.NewPipeDatabase
17+
import org.schabi.newpipe.R
18+
import org.schabi.newpipe.extractor.InfoItem.InfoType
19+
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
20+
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
21+
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler
22+
import org.schabi.newpipe.local.playlist.LocalPlaylistManager
23+
import org.schabi.newpipe.local.playlist.RemotePlaylistManager
24+
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue
25+
import org.schabi.newpipe.player.playqueue.PlayQueue
26+
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue
27+
import org.schabi.newpipe.player.playqueue.SinglePlayQueue
28+
import org.schabi.newpipe.util.ChannelTabHelper
29+
import org.schabi.newpipe.util.ExtractorHelper
30+
import org.schabi.newpipe.util.NavigationHelper
31+
import java.util.function.BiConsumer
32+
33+
/**
34+
* This class is used to cleanly separate the Service implementation (in
35+
* [org.schabi.newpipe.player.PlayerService]) and the playback preparer implementation (in this
36+
* file). We currently use the playback preparer only in conjunction with the media browser: the
37+
* playback preparer will receive the media URLs generated by [MediaBrowserImpl] and will start
38+
* playback of the corresponding streams or playlists.
39+
*
40+
* @param setMediaSessionError takes an error String and an error code from [PlaybackStateCompat],
41+
* calls `sessionConnector.setCustomErrorMessage(errorString, errorCode)`
42+
* @param clearMediaSessionError calls `sessionConnector.setCustomErrorMessage(null)`
43+
*/
44+
class MediaBrowserPlaybackPreparer(
45+
private val context: Context,
46+
private val setMediaSessionError: BiConsumer<String, Int>, // error string, error code
47+
private val clearMediaSessionError: Runnable,
48+
) : PlaybackPreparer {
49+
private val database = NewPipeDatabase.getInstance(context)
50+
private var disposable: Disposable? = null
51+
52+
fun dispose() {
53+
disposable?.dispose()
54+
}
55+
56+
//region Overrides
57+
override fun getSupportedPrepareActions(): Long {
58+
return PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
59+
}
60+
61+
override fun onPrepare(playWhenReady: Boolean) {
62+
// TODO handle onPrepare
63+
}
64+
65+
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
66+
if (MainActivity.DEBUG) {
67+
Log.d(TAG, "onPrepareFromMediaId($mediaId, $playWhenReady, $extras)")
68+
}
69+
70+
disposable?.dispose()
71+
disposable = extractPlayQueueFromMediaId(mediaId)
72+
.subscribeOn(Schedulers.io())
73+
.observeOn(AndroidSchedulers.mainThread())
74+
.subscribe(
75+
{ playQueue ->
76+
clearMediaSessionError.run()
77+
NavigationHelper.playOnBackgroundPlayer(context, playQueue, playWhenReady)
78+
},
79+
{ throwable ->
80+
Log.e(TAG, "Failed to start playback of media ID [$mediaId]", throwable)
81+
onPrepareError()
82+
}
83+
)
84+
}
85+
86+
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
87+
onUnsupportedError()
88+
}
89+
90+
override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) {
91+
onUnsupportedError()
92+
}
93+
94+
override fun onCommand(
95+
player: Player,
96+
command: String,
97+
extras: Bundle?,
98+
cb: ResultReceiver?
99+
): Boolean {
100+
return false
101+
}
102+
//endregion
103+
104+
//region Errors
105+
private fun onUnsupportedError() {
106+
setMediaSessionError.accept(
107+
context.getString(R.string.content_not_supported),
108+
PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED
109+
)
110+
}
111+
112+
private fun onPrepareError() {
113+
setMediaSessionError.accept(
114+
context.getString(R.string.error_snackbar_message),
115+
PlaybackStateCompat.ERROR_CODE_APP_ERROR
116+
)
117+
}
118+
//endregion
119+
120+
//region Building play queues from playlists and history
121+
private fun extractLocalPlayQueue(playlistId: Long, index: Int): Single<PlayQueue> {
122+
return LocalPlaylistManager(database).getPlaylistStreams(playlistId).firstOrError()
123+
.map { items -> SinglePlayQueue(items.map { it.toStreamInfoItem() }, index) }
124+
}
125+
126+
private fun extractRemotePlayQueue(playlistId: Long, index: Int): Single<PlayQueue> {
127+
return RemotePlaylistManager(database).getPlaylist(playlistId).firstOrError()
128+
.flatMap { ExtractorHelper.getPlaylistInfo(it.serviceId, it.url, false) }
129+
.flatMap { info ->
130+
info.errors.firstOrNull { it !is ContentNotSupportedException }?.let {
131+
return@flatMap Single.error(it)
132+
}
133+
Single.just(PlaylistPlayQueue(info, index))
134+
}
135+
}
136+
137+
private fun extractPlayQueueFromMediaId(mediaId: String): Single<PlayQueue> {
138+
try {
139+
val mediaIdUri = Uri.parse(mediaId)
140+
val path = ArrayList(mediaIdUri.pathSegments)
141+
if (path.isEmpty()) {
142+
throw parseError(mediaId)
143+
}
144+
145+
return when (/*val uriType = */path.removeAt(0)) {
146+
ID_BOOKMARKS -> extractPlayQueueFromPlaylistMediaId(
147+
mediaId,
148+
path,
149+
mediaIdUri.getQueryParameter(ID_URL) ?: throw parseError(mediaId)
150+
)
151+
152+
ID_HISTORY -> extractPlayQueueFromHistoryMediaId(mediaId, path)
153+
154+
ID_INFO_ITEM -> extractPlayQueueFromInfoItemMediaId(
155+
mediaId,
156+
path,
157+
mediaIdUri.getQueryParameter(ID_URL) ?: throw parseError(mediaId)
158+
)
159+
160+
else -> throw parseError(mediaId)
161+
}
162+
} catch (e: ContentNotAvailableException) {
163+
return Single.error(e)
164+
}
165+
}
166+
167+
@Throws(ContentNotAvailableException::class)
168+
private fun extractPlayQueueFromPlaylistMediaId(
169+
mediaId: String,
170+
path: MutableList<String>,
171+
url: String,
172+
): Single<PlayQueue> {
173+
if (path.isEmpty()) {
174+
throw parseError(mediaId)
175+
}
176+
177+
when (val playlistType = path.removeAt(0)) {
178+
ID_LOCAL, ID_REMOTE -> {
179+
if (path.size != 2) {
180+
throw parseError(mediaId)
181+
}
182+
val playlistId = path[0].toLong()
183+
val index = path[1].toInt()
184+
return if (playlistType == ID_LOCAL)
185+
extractLocalPlayQueue(playlistId, index)
186+
else
187+
extractRemotePlayQueue(playlistId, index)
188+
}
189+
190+
ID_URL -> {
191+
if (path.size != 1) {
192+
throw parseError(mediaId)
193+
}
194+
195+
val serviceId = path[0].toInt()
196+
return ExtractorHelper.getPlaylistInfo(serviceId, url, false)
197+
.map { PlaylistPlayQueue(it) }
198+
}
199+
200+
else -> throw parseError(mediaId)
201+
}
202+
}
203+
204+
@Throws(ContentNotAvailableException::class)
205+
private fun extractPlayQueueFromHistoryMediaId(
206+
mediaId: String,
207+
path: List<String>,
208+
): Single<PlayQueue> {
209+
if (path.size != 1) {
210+
throw parseError(mediaId)
211+
}
212+
213+
val streamId = path[0].toLong()
214+
return database.streamHistoryDAO().getHistory()
215+
.firstOrError()
216+
.map { items ->
217+
val infoItems = items
218+
.filter { it.streamId == streamId }
219+
.map { it.toStreamInfoItem() }
220+
SinglePlayQueue(infoItems, 0)
221+
}
222+
}
223+
224+
@Throws(ContentNotAvailableException::class)
225+
private fun extractPlayQueueFromInfoItemMediaId(
226+
mediaId: String,
227+
path: List<String>,
228+
url: String,
229+
): Single<PlayQueue> {
230+
if (path.size != 2) {
231+
throw parseError(mediaId)
232+
}
233+
234+
val serviceId = path[1].toInt()
235+
return when (/*val infoItemType = */infoItemTypeFromString(path[0])) {
236+
InfoType.STREAM -> ExtractorHelper.getStreamInfo(serviceId, url, false)
237+
.map { SinglePlayQueue(it) }
238+
239+
InfoType.PLAYLIST -> ExtractorHelper.getPlaylistInfo(serviceId, url, false)
240+
.map { PlaylistPlayQueue(it) }
241+
242+
InfoType.CHANNEL -> ExtractorHelper.getChannelInfo(serviceId, url, false)
243+
.map { info ->
244+
val playableTab = info.tabs
245+
.firstOrNull { ChannelTabHelper.isStreamsTab(it) }
246+
?: throw ContentNotAvailableException("No streams tab found")
247+
return@map ChannelTabPlayQueue(serviceId, ListLinkHandler(playableTab))
248+
}
249+
250+
else -> throw parseError(mediaId)
251+
}
252+
}
253+
//endregion
254+
255+
companion object {
256+
private val TAG = MediaBrowserPlaybackPreparer::class.simpleName
257+
}
258+
}

0 commit comments

Comments
 (0)