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?.player = player
|
||||||
player_view?.performClick()
|
player_view?.performClick()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue