forked from recloudstream/cloudstream
Backend: Added rudimentary support for segmented streams for #1266
This commit is contained in:
parent
c798b641cd
commit
2ca8f6f870
3 changed files with 104 additions and 23 deletions
|
@ -295,6 +295,8 @@ abstract class AbstractPlayerFragment(
|
|||
}
|
||||
}
|
||||
|
||||
// Necessary for multiple combined videos
|
||||
player_view?.setShowMultiWindowTimeBar(true)
|
||||
player_view?.player = player
|
||||
player_view?.performClick()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue