Backend: Added rudimentary support for segmented streams for #1266

This commit is contained in:
Blatzar 2022-07-10 04:08:11 +02:00
parent c798b641cd
commit 2ca8f6f870
3 changed files with 104 additions and 23 deletions

View file

@ -295,6 +295,8 @@ abstract class AbstractPlayerFragment(
} }
} }
// Necessary for multiple combined videos
player_view?.setShowMultiWindowTimeBar(true)
player_view?.player = player player_view?.player = player
player_view?.performClick() player_view?.performClick()
} }

View file

@ -9,9 +9,7 @@ import android.widget.FrameLayout
import com.google.android.exoplayer2.* import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory import com.google.android.exoplayer2.source.*
import com.google.android.exoplayer2.source.MergingMediaSource
import com.google.android.exoplayer2.source.SingleSampleMediaSource
import com.google.android.exoplayer2.text.TextRenderer import com.google.android.exoplayer2.text.TextRenderer
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.trackselection.TrackSelector 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.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.ExtractorUri
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import java.io.File import java.io.File
@ -65,6 +64,15 @@ class CS3IPlayer : IPlayer {
private val subtitleHelper = PlayerSubtitleHelper() 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 getDuration(): Long? = exoPlayer?.duration
override fun getPosition(): Long? = exoPlayer?.currentPosition override fun getPosition(): Long? = exoPlayer?.currentPosition
override fun getIsPlaying(): Boolean = isPlaying override fun getIsPlaying(): Boolean = isPlaying
@ -450,7 +458,7 @@ class CS3IPlayer : IPlayer {
private fun buildExoPlayer( private fun buildExoPlayer(
context: Context, context: Context,
mediaItem: MediaItem, mediaItemSlices: List<MediaItemSlice>,
subSources: List<SingleSampleMediaSource>, subSources: List<SingleSampleMediaSource>,
currentWindow: Int, currentWindow: Int,
playbackPosition: Long, playbackPosition: Long,
@ -505,12 +513,27 @@ class CS3IPlayer : IPlayer {
).build() ).build()
) )
val videoMediaSource =
(if (cacheFactory == null) DefaultMediaSourceFactory(context) else DefaultMediaSourceFactory( val factory =
cacheFactory if (cacheFactory == null) DefaultMediaSourceFactory(context)
)).createMediaSource( else DefaultMediaSourceFactory(cacheFactory)
mediaItem
// 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") println("PLAYBACK POS $playbackPosition")
return exoPlayerBuilder.build().apply { return exoPlayerBuilder.build().apply {
@ -592,7 +615,7 @@ class CS3IPlayer : IPlayer {
private fun loadExo( private fun loadExo(
context: Context, context: Context,
mediaItem: MediaItem, mediaSlices: List<MediaItemSlice>,
subSources: List<SingleSampleMediaSource>, subSources: List<SingleSampleMediaSource>,
cacheFactory: CacheDataSource.Factory? = null cacheFactory: CacheDataSource.Factory? = null
) { ) {
@ -604,7 +627,7 @@ class CS3IPlayer : IPlayer {
// this makes no sense // this makes no sense
exoPlayer = buildExoPlayer( exoPlayer = buildExoPlayer(
context, context,
mediaItem, mediaSlices,
subSources, subSources,
currentWindow, currentWindow,
playbackPosition, playbackPosition,
@ -773,10 +796,12 @@ class CS3IPlayer : IPlayer {
fun onRenderFirst() { fun onRenderFirst() {
if (!hasUsedFirstRender) { // this insures that we only call this once per player load if (!hasUsedFirstRender) { // this insures that we only call this once per player load
Log.i(TAG, "Rendered first frame") Log.i(TAG, "Rendered first frame")
val invalid = exoPlayer?.duration?.let { duration -> val invalid = exoPlayer?.duration?.let { duration ->
// Only errors short playback when not playing downloaded files // Only errors short playback when not playing downloaded files
duration < 20_000L && currentDownloadedFile == null 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 } ?: false
if (invalid) { if (invalid) {
@ -824,7 +849,7 @@ class CS3IPlayer : IPlayer {
) )
subtitleHelper.setActiveSubtitles(activeSubtitles.toSet()) subtitleHelper.setActiveSubtitles(activeSubtitles.toSet())
loadExo(context, mediaItem, subSources) loadExo(context, listOf(MediaItemSlice(mediaItem, Long.MIN_VALUE)), subSources)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "loadOfflinePlayer error", e) Log.e(TAG, "loadOfflinePlayer error", e)
playerError?.invoke(e) playerError?.invoke(e)
@ -901,7 +926,17 @@ class CS3IPlayer : IPlayer {
} else { } else {
MimeTypes.VIDEO_MP4 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 onlineSourceFactory = createOnlineSource(link)
val offlineSourceFactory = context.createOfflineSource() val offlineSourceFactory = context.createOfflineSource()
@ -922,7 +957,7 @@ class CS3IPlayer : IPlayer {
setUpstreamDataSourceFactory(onlineSourceFactory) setUpstreamDataSourceFactory(onlineSourceFactory)
} }
loadExo(context, mediaItem, subSources, cacheFactory) loadExo(context, mediaItems, subSources, cacheFactory)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "loadOnlinePlayer error", e) Log.e(TAG, "loadOnlinePlayer error", e)
playerError?.invoke(e) playerError?.invoke(e)

View file

@ -9,16 +9,60 @@ import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.jsoup.Jsoup import org.jsoup.Jsoup
data class ExtractorLink( /**
val source: String, * For use in the ConcatenatingMediaSource.
val name: String, * If features are missing (headers), please report and we can add it.
override val url: String, * @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<PlayListItem>,
override val referer: String, override val referer: String,
val quality: Int, override val quality: Int,
val isM3u8: Boolean = false, override val isM3u8: Boolean = false,
override val headers: Map<String, String> = mapOf(), override val headers: Map<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */ /** 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<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */
open val extractorData: String? = null,
) : VideoDownloadManager.IDownloadableMinimum ) : VideoDownloadManager.IDownloadableMinimum
data class ExtractorUri( data class ExtractorUri(