From 2ca8f6f870ae7703cd63b13c714143b030a906ee Mon Sep 17 00:00:00 2001 From: Blatzar <46196380+Blatzar@users.noreply.github.com> Date: Sun, 10 Jul 2022 04:08:11 +0200 Subject: [PATCH] Backend: Added rudimentary support for segmented streams for #1266 --- .../ui/player/AbstractPlayerFragment.kt | 2 + .../cloudstream3/ui/player/CS3IPlayer.kt | 67 ++++++++++++++----- .../cloudstream3/utils/ExtractorApi.kt | 58 ++++++++++++++-- 3 files changed, 104 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index d0371a4b..072bd501 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -295,6 +295,8 @@ abstract class AbstractPlayerFragment( } } + // Necessary for multiple combined videos + player_view?.setShowMultiWindowTimeBar(true) player_view?.player = player player_view?.performClick() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 283497ed..a52b2ed1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -9,9 +9,7 @@ import android.widget.FrameLayout import com.google.android.exoplayer2.* import com.google.android.exoplayer2.database.StandaloneDatabaseProvider import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource -import com.google.android.exoplayer2.source.DefaultMediaSourceFactory -import com.google.android.exoplayer2.source.MergingMediaSource -import com.google.android.exoplayer2.source.SingleSampleMediaSource +import com.google.android.exoplayer2.source.* import com.google.android.exoplayer2.text.TextRenderer import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.trackselection.TrackSelector @@ -31,6 +29,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File @@ -65,6 +64,15 @@ class CS3IPlayer : IPlayer { private val subtitleHelper = PlayerSubtitleHelper() + /** + * This is a way to combine the MediaItem and its duration for the concatenating MediaSource. + * @param durationUs does not matter if only one slice is present, since it will not concatenate + * */ + data class MediaItemSlice( + val mediaItem: MediaItem, + val durationUs: Long + ) + override fun getDuration(): Long? = exoPlayer?.duration override fun getPosition(): Long? = exoPlayer?.currentPosition override fun getIsPlaying(): Boolean = isPlaying @@ -450,7 +458,7 @@ class CS3IPlayer : IPlayer { private fun buildExoPlayer( context: Context, - mediaItem: MediaItem, + mediaItemSlices: List, subSources: List, currentWindow: Int, playbackPosition: Long, @@ -505,12 +513,27 @@ class CS3IPlayer : IPlayer { ).build() ) - val videoMediaSource = - (if (cacheFactory == null) DefaultMediaSourceFactory(context) else DefaultMediaSourceFactory( - cacheFactory - )).createMediaSource( - mediaItem - ) + + val factory = + if (cacheFactory == null) DefaultMediaSourceFactory(context) + else DefaultMediaSourceFactory(cacheFactory) + + // If there is only one item then treat it as normal, if multiple: concatenate the items. + val videoMediaSource = if (mediaItemSlices.size == 1) { + factory.createMediaSource(mediaItemSlices.first().mediaItem) + } else { + val source = ConcatenatingMediaSource() + mediaItemSlices.map { + source.addMediaSource( + // The duration MUST be known for it to work properly, see https://github.com/google/ExoPlayer/issues/4727 + ClippingMediaSource( + factory.createMediaSource(it.mediaItem), + it.durationUs + ) + ) + } + source + } println("PLAYBACK POS $playbackPosition") return exoPlayerBuilder.build().apply { @@ -592,7 +615,7 @@ class CS3IPlayer : IPlayer { private fun loadExo( context: Context, - mediaItem: MediaItem, + mediaSlices: List, subSources: List, cacheFactory: CacheDataSource.Factory? = null ) { @@ -604,7 +627,7 @@ class CS3IPlayer : IPlayer { // this makes no sense exoPlayer = buildExoPlayer( context, - mediaItem, + mediaSlices, subSources, currentWindow, playbackPosition, @@ -773,10 +796,12 @@ class CS3IPlayer : IPlayer { fun onRenderFirst() { if (!hasUsedFirstRender) { // this insures that we only call this once per player load Log.i(TAG, "Rendered first frame") - val invalid = exoPlayer?.duration?.let { duration -> // Only errors short playback when not playing downloaded files duration < 20_000L && currentDownloadedFile == null + // Concatenated sources (non 1 periodCount) bypasses the invalid check as exoPlayer.duration gives only the current period + // If you can get the total time that'd be better, but this is already niche. + && exoPlayer?.currentTimeline?.periodCount == 1 } ?: false if (invalid) { @@ -824,7 +849,7 @@ class CS3IPlayer : IPlayer { ) subtitleHelper.setActiveSubtitles(activeSubtitles.toSet()) - loadExo(context, mediaItem, subSources) + loadExo(context, listOf(MediaItemSlice(mediaItem, Long.MIN_VALUE)), subSources) } catch (e: Exception) { Log.e(TAG, "loadOfflinePlayer error", e) playerError?.invoke(e) @@ -901,7 +926,17 @@ class CS3IPlayer : IPlayer { } else { MimeTypes.VIDEO_MP4 } - val mediaItem = getMediaItem(mime, link.url) + + val mediaItems = if (link is ExtractorLinkPlayList) { + link.playlist.map { + MediaItemSlice(getMediaItem(mime, it.url), it.durationUs) + } + } else { + listOf( + // Single sliced list with unset length + MediaItemSlice(getMediaItem(mime, link.url), Long.MIN_VALUE) + ) + } val onlineSourceFactory = createOnlineSource(link) val offlineSourceFactory = context.createOfflineSource() @@ -922,7 +957,7 @@ class CS3IPlayer : IPlayer { setUpstreamDataSourceFactory(onlineSourceFactory) } - loadExo(context, mediaItem, subSources, cacheFactory) + loadExo(context, mediaItems, subSources, cacheFactory) } catch (e: Exception) { Log.e(TAG, "loadOnlinePlayer error", e) playerError?.invoke(e) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index aaeaabdf..a5a605a5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -9,16 +9,60 @@ import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import kotlinx.coroutines.delay import org.jsoup.Jsoup -data class ExtractorLink( - val source: String, - val name: String, - override val url: String, +/** + * For use in the ConcatenatingMediaSource. + * If features are missing (headers), please report and we can add it. + * @param durationUs use Long.toUs() for easier input + * */ +data class PlayListItem( + val url: String, + val durationUs: Long, +) + +/** + * Converts Seconds to MicroSeconds, multiplication by 1_000_000 + * */ +fun Long.toUs(): Long { + return this * 1_000_000 +} + +/** + * If your site has an unorthodox m3u8-like system where there are multiple smaller videos concatenated + * use this. + * */ +data class ExtractorLinkPlayList( + override val source: String, + override val name: String, + val playlist: List, override val referer: String, - val quality: Int, - val isM3u8: Boolean = false, + override val quality: Int, + override val isM3u8: Boolean = false, override val headers: Map = mapOf(), /** Used for getExtractorVerifierJob() */ - val extractorData: String? = null + override val extractorData: String? = null, + ) : ExtractorLink( + source, + name, + // Blank as un-used + "", + referer, + quality, + isM3u8, + headers, + extractorData +) + + +open class ExtractorLink( + open val source: String, + open val name: String, + override val url: String, + override val referer: String, + open val quality: Int, + open val isM3u8: Boolean = false, + override val headers: Map = mapOf(), + /** Used for getExtractorVerifierJob() */ + open val extractorData: String? = null, ) : VideoDownloadManager.IDownloadableMinimum data class ExtractorUri(