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?.performClick()
}

View file

@ -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<MediaItemSlice>,
subSources: List<SingleSampleMediaSource>,
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<MediaItemSlice>,
subSources: List<SingleSampleMediaSource>,
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)

View file

@ -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<PlayListItem>,
override val referer: String,
val quality: Int,
val isM3u8: Boolean = false,
override val quality: Int,
override val isM3u8: Boolean = false,
override val headers: Map<String, String> = 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<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */
open val extractorData: String? = null,
) : VideoDownloadManager.IDownloadableMinimum
data class ExtractorUri(