AquaStream/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt

1397 lines
53 KiB
Kotlin
Raw Normal View History

2022-01-07 19:27:25 +00:00
package com.lagradost.cloudstream3.ui.player
2023-09-06 18:53:43 +00:00
import android.annotation.SuppressLint
2022-01-07 19:27:25 +00:00
import android.content.Context
2023-10-02 15:44:06 +00:00
import android.graphics.Bitmap
2022-01-07 19:27:25 +00:00
import android.net.Uri
2022-02-12 18:18:43 +00:00
import android.os.Handler
2022-01-07 19:27:25 +00:00
import android.os.Looper
import android.util.Log
import android.util.Rational
2022-01-07 19:27:25 +00:00
import android.widget.FrameLayout
import androidx.media3.common.C.*
import androidx.media3.common.Format
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.TrackGroup
import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.Tracks
import androidx.media3.common.VideoSize
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSourceFactory
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.SeekParameters
2023-09-06 18:53:43 +00:00
import androidx.media3.exoplayer.dash.DashMediaSource
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager
import androidx.media3.exoplayer.drm.FrameworkMediaDrm
import androidx.media3.exoplayer.drm.LocalMediaDrmCallback
import androidx.media3.exoplayer.source.ClippingMediaSource
import androidx.media3.exoplayer.source.ConcatenatingMediaSource
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.MergingMediaSource
import androidx.media3.exoplayer.source.SingleSampleMediaSource
import androidx.media3.exoplayer.text.TextRenderer
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.exoplayer.trackselection.TrackSelector
import androidx.media3.ui.SubtitleView
import androidx.preference.PreferenceManager
2022-07-28 15:49:44 +00:00
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
2022-01-07 19:27:25 +00:00
import com.lagradost.cloudstream3.USER_AGENT
2022-04-02 17:50:16 +00:00
import com.lagradost.cloudstream3.app
2023-09-14 10:53:35 +00:00
import com.lagradost.cloudstream3.mvvm.debugAssert
2022-01-07 19:27:25 +00:00
import com.lagradost.cloudstream3.mvvm.logError
2022-05-20 18:25:56 +00:00
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
2022-01-07 19:27:25 +00:00
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
2023-09-06 18:53:43 +00:00
import com.lagradost.cloudstream3.utils.DrmExtractorLink
import com.lagradost.cloudstream3.utils.EpisodeSkip
2022-01-07 19:27:25 +00:00
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
import com.lagradost.cloudstream3.utils.ExtractorLinkType
2022-01-07 19:27:25 +00:00
import com.lagradost.cloudstream3.utils.ExtractorUri
2022-05-20 18:25:56 +00:00
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
2022-01-07 19:27:25 +00:00
import java.io.File
import java.lang.IllegalArgumentException
2023-09-06 18:53:43 +00:00
import java.util.UUID
2022-01-07 19:27:25 +00:00
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSession
const val TAG = "CS3ExoPlayer"
const val PREFERRED_AUDIO_LANGUAGE_KEY = "preferred_audio_language"
2022-01-07 19:27:25 +00:00
/** toleranceBeforeUs The maximum time that the actual position seeked to may precede the
* requested seek position, in microseconds. Must be non-negative. */
const val toleranceBeforeUs = 300_000L
/**
* toleranceAfterUs The maximum time that the actual position seeked to may exceed the requested
* seek position, in microseconds. Must be non-negative.
*/
const val toleranceAfterUs = 300_000L
2022-02-12 18:18:43 +00:00
class CS3IPlayer : IPlayer {
2022-01-07 19:27:25 +00:00
private var isPlaying = false
private var exoPlayer: ExoPlayer? = null
2023-09-14 10:53:35 +00:00
set(value) {
// If the old value is not null then the player has not been properly released.
2023-10-02 15:44:06 +00:00
debugAssert(
{ field != null && value != null },
{ "Previous player instance should be released!" })
2023-09-14 10:53:35 +00:00
field = value
}
2022-03-16 21:31:21 +00:00
var cacheSize = 0L
var simpleCacheSize = 0L
var videoBufferMs = 0L
2022-01-07 19:27:25 +00:00
2023-10-11 16:31:46 +00:00
val imageGenerator = IPreviewGenerator.new()
2023-10-02 15:44:06 +00:00
2022-01-07 19:27:25 +00:00
private val seekActionTime = 30000L
private var ignoreSSL: Boolean = true
private var playBackSpeed: Float = 1.0f
private var lastMuteVolume: Float = 1.0f
private var currentLink: ExtractorLink? = null
private var currentDownloadedFile: ExtractorUri? = null
private var hasUsedFirstRender = false
private var currentWindow: Int = 0
private var playbackPosition: Long = 0
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,
2023-09-06 18:53:43 +00:00
val durationUs: Long,
val drm: DrmMetadata? = null
)
data class DrmMetadata(
val kid: String,
val key: String,
val uuid: UUID,
val kty: String,
val keyRequestParameters: HashMap<String, String>,
)
2022-01-07 19:27:25 +00:00
override fun getDuration(): Long? = exoPlayer?.duration
override fun getPosition(): Long? = exoPlayer?.currentPosition
override fun getIsPlaying(): Boolean = isPlaying
override fun getPlaybackSpeed(): Float = playBackSpeed
/**
* Tracks reported to be used by exoplayer, since sometimes it has a mind of it's own when selecting subs.
* String = id
2022-01-07 19:27:25 +00:00
* Boolean = if it's active
* */
private var playerSelectedSubtitleTracks = listOf<Pair<String, Boolean>>()
2022-01-07 19:27:25 +00:00
private var requestedListeningPercentages: List<Int>? = null
2023-09-09 21:18:21 +00:00
private var eventHandler: ((PlayerEvent) -> Unit)? = null
2022-01-07 19:27:25 +00:00
2023-09-09 21:18:21 +00:00
fun event(event: PlayerEvent) {
eventHandler?.invoke(event)
}
2022-01-07 19:27:25 +00:00
override fun releaseCallbacks() {
2023-09-09 21:18:21 +00:00
eventHandler = null
}
2022-01-07 19:27:25 +00:00
override fun initCallbacks(
2023-09-09 21:18:21 +00:00
eventHandler: ((PlayerEvent) -> Unit),
2022-01-07 19:27:25 +00:00
requestedListeningPercentages: List<Int>?,
) {
this.requestedListeningPercentages = requestedListeningPercentages
2023-09-09 21:18:21 +00:00
this.eventHandler = eventHandler
2022-01-07 19:27:25 +00:00
}
2022-02-12 18:18:43 +00:00
// I know, this is not a perfect solution, however it works for fixing subs
private fun reloadSubs() {
exoPlayer?.applicationLooper?.let {
try {
Handler(it).post {
try {
2023-09-09 21:18:21 +00:00
seekTime(1L, source = PlayerEventSource.Player)
} catch (e: Exception) {
2022-02-12 18:18:43 +00:00
logError(e)
}
}
} catch (e: Exception) {
2022-02-12 18:18:43 +00:00
logError(e)
}
}
}
2022-01-07 19:27:25 +00:00
fun initSubtitles(subView: SubtitleView?, subHolder: FrameLayout?, style: SaveCaptionStyle?) {
subtitleHelper.initSubtitles(subView, subHolder, style)
}
2023-10-02 15:44:06 +00:00
override fun getPreview(fraction: Float): Bitmap? {
return imageGenerator.getPreviewImage(fraction)
}
override fun hasPreview(): Boolean {
return imageGenerator.hasPreview()
}
2022-01-07 19:27:25 +00:00
override fun loadPlayer(
context: Context,
sameEpisode: Boolean,
link: ExtractorLink?,
data: ExtractorUri?,
startPosition: Long?,
subtitles: Set<SubtitleData>,
2022-06-16 01:04:24 +00:00
subtitle: SubtitleData?,
2023-10-02 15:44:06 +00:00
autoPlay: Boolean?,
2023-10-11 16:31:46 +00:00
preview: Boolean,
2022-01-07 19:27:25 +00:00
) {
Log.i(TAG, "loadPlayer")
if (sameEpisode) {
saveData()
} else {
currentSubtitles = subtitle
2022-01-07 19:27:25 +00:00
playbackPosition = 0
}
startPosition?.let {
playbackPosition = it
}
// we want autoplay because of TV and UX
2022-06-16 01:04:24 +00:00
isPlaying = autoPlay ?: isPlaying
2022-01-07 19:27:25 +00:00
// release the current exoplayer and cache
releasePlayer()
2023-10-11 16:31:46 +00:00
2022-01-07 19:27:25 +00:00
if (link != null) {
2023-10-02 15:44:06 +00:00
// only video support atm
2023-10-11 16:31:46 +00:00
(imageGenerator as? PreviewGenerator)?.let { gen ->
if (preview) {
gen.load(link, sameEpisode)
} else {
gen.clear(sameEpisode)
}
2023-10-02 15:44:06 +00:00
}
2022-01-07 19:27:25 +00:00
loadOnlinePlayer(context, link)
} else if (data != null) {
2023-10-11 16:31:46 +00:00
(imageGenerator as? PreviewGenerator)?.let { gen ->
if (preview) {
gen.load(context, data, sameEpisode)
} else {
gen.clear(sameEpisode)
}
2023-10-02 15:44:06 +00:00
}
2022-01-07 19:27:25 +00:00
loadOfflinePlayer(context, data)
2023-10-02 15:44:06 +00:00
} else {
throw IllegalArgumentException("Requires link or uri")
2022-01-07 19:27:25 +00:00
}
2023-10-11 16:31:46 +00:00
2022-01-07 19:27:25 +00:00
}
override fun setActiveSubtitles(subtitles: Set<SubtitleData>) {
Log.i(TAG, "setActiveSubtitles ${subtitles.size}")
subtitleHelper.setAllSubtitles(subtitles)
}
2023-09-09 21:18:21 +00:00
private var currentSubtitles: SubtitleData? = null
2022-07-28 15:49:44 +00:00
2023-09-09 21:18:21 +00:00
@SuppressLint("UnsafeOptInUsageError")
private fun List<Tracks.Group>.getTrack(id: String?): Pair<TrackGroup, Int>? {
if (id == null) return null
// This beast of an expression does:
// 1. Filter all audio tracks
// 2. Get all formats in said audio tacks
// 3. Gets all ids of the formats
// 4. Filters to find the first audio track with the same id as the audio track we are looking for
// 5. Returns the media group and the index of the audio track in the group
return this.firstNotNullOfOrNull { group ->
(0 until group.mediaTrackGroup.length).map {
group.getTrackFormat(it) to it
}.firstOrNull { it.first.id == id }
?.let { group.mediaTrackGroup to it.second }
}
}
override fun setMaxVideoSize(width: Int, height: Int, id: String?) {
if (id != null) {
val videoTrack =
exoPlayer?.currentTracks?.groups?.filter { it.type == TRACK_TYPE_VIDEO }
?.getTrack(id)
if (videoTrack != null) {
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
?.buildUpon()
?.setOverrideForType(
TrackSelectionOverride(
videoTrack.first,
videoTrack.second
)
)
?.build()
?: return
return
}
}
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
?.buildUpon()
?.setMaxVideoSize(width, height)
?.build()
?: return
}
override fun setPreferredAudioTrack(trackLanguage: String?, id: String?) {
preferredAudioTrackLanguage = trackLanguage
if (id != null) {
val audioTrack =
exoPlayer?.currentTracks?.groups?.filter { it.type == TRACK_TYPE_AUDIO }
?.getTrack(id)
if (audioTrack != null) {
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
?.buildUpon()
?.setOverrideForType(
TrackSelectionOverride(
audioTrack.first,
audioTrack.second
)
)
?.build()
?: return
return
}
}
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
?.buildUpon()
?.setPreferredAudioLanguage(trackLanguage)
?.build()
?: return
}
/**
* Gets all supported formats in a list
* */
private fun List<Tracks.Group>.getFormats(): List<Pair<Format, Int>> {
return this.map {
it.getFormats()
}.flatten()
}
2023-09-06 18:53:43 +00:00
@SuppressLint("UnsafeOptInUsageError")
private fun Tracks.Group.getFormats(): List<Pair<Format, Int>> {
return (0 until this.mediaTrackGroup.length).mapNotNull { i ->
if (this.isSupported)
this.mediaTrackGroup.getFormat(i) to i
else null
}
}
private fun Format.toAudioTrack(): AudioTrack {
return AudioTrack(
this.id,
this.label,
// isPlaying,
this.language
)
}
private fun Format.toVideoTrack(): VideoTrack {
return VideoTrack(
this.id,
this.label,
// isPlaying,
this.language,
this.width,
this.height
)
}
2023-09-06 18:53:43 +00:00
@SuppressLint("UnsafeOptInUsageError")
override fun getVideoTracks(): CurrentTracks {
val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList()
val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO }
.getFormats()
.map { it.first.toVideoTrack() }
val audioTracks = allTracks.filter { it.type == TRACK_TYPE_AUDIO }.getFormats()
.map { it.first.toAudioTrack() }
return CurrentTracks(
exoPlayer?.videoFormat?.toVideoTrack(),
exoPlayer?.audioFormat?.toAudioTrack(),
videoTracks,
audioTracks
)
}
/**
* @return True if the player should be reloaded
* */
2023-09-06 18:53:43 +00:00
@SuppressLint("UnsafeOptInUsageError")
2022-01-07 19:27:25 +00:00
override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean {
2022-01-08 19:39:22 +00:00
Log.i(TAG, "setPreferredSubtitles init $subtitle")
2022-01-07 19:27:25 +00:00
currentSubtitles = subtitle
fun getTextTrack(id: String) =
exoPlayer?.currentTracks?.groups?.filter { it.type == TRACK_TYPE_TEXT }
?.getTrack(id)
2022-01-07 19:27:25 +00:00
return (exoPlayer?.trackSelector as? DefaultTrackSelector?)?.let { trackSelector ->
if (subtitle == null) {
2022-01-07 19:27:25 +00:00
trackSelector.setParameters(
trackSelector.buildUponParameters()
.setTrackTypeDisabled(TRACK_TYPE_TEXT, true)
.clearOverridesOfType(TRACK_TYPE_TEXT)
2022-01-07 19:27:25 +00:00
)
} else {
when (subtitleHelper.subtitleStatus(subtitle)) {
SubtitleStatus.REQUIRES_RELOAD -> {
2022-01-08 19:39:22 +00:00
Log.i(TAG, "setPreferredSubtitles REQUIRES_RELOAD")
2022-01-07 19:27:25 +00:00
return@let true
}
2022-01-07 19:27:25 +00:00
SubtitleStatus.IS_ACTIVE -> {
2022-01-08 19:39:22 +00:00
Log.i(TAG, "setPreferredSubtitles IS_ACTIVE")
2022-01-07 19:27:25 +00:00
trackSelector.setParameters(
trackSelector.buildUponParameters()
2022-05-20 18:25:56 +00:00
.apply {
val track = getTextTrack(subtitle.getId())
if (track != null) {
setTrackTypeDisabled(TRACK_TYPE_TEXT, false)
setOverrideForType(
TrackSelectionOverride(
track.first,
track.second
)
)
}
2022-05-20 18:25:56 +00:00
}
2022-01-07 19:27:25 +00:00
)
2022-02-12 18:18:43 +00:00
// ugliest code I have written, it seeks 1ms to *update* the subtitles
//exoPlayer?.applicationLooper?.let {
// Handler(it).postDelayed({
// seekTime(1L)
// }, 1)
//}
2022-01-07 19:27:25 +00:00
}
2022-01-07 19:27:25 +00:00
SubtitleStatus.NOT_FOUND -> {
2022-01-08 19:39:22 +00:00
Log.i(TAG, "setPreferredSubtitles NOT_FOUND")
2022-01-07 19:27:25 +00:00
return@let true
}
}
}
return false
} ?: false
}
2022-02-27 01:03:01 +00:00
var currentSubtitleOffset: Long = 0
2022-02-13 18:06:36 +00:00
override fun setSubtitleOffset(offset: Long) {
currentSubtitleOffset = offset
currentTextRenderer?.setRenderOffsetMs(offset)
}
override fun getSubtitleOffset(): Long {
2022-05-20 18:25:56 +00:00
return currentSubtitleOffset //currentTextRenderer?.getRenderOffsetMs() ?: currentSubtitleOffset
2022-02-13 18:06:36 +00:00
}
2022-01-07 19:27:25 +00:00
override fun getCurrentPreferredSubtitle(): SubtitleData? {
return subtitleHelper.getAllSubtitles().firstOrNull { sub ->
playerSelectedSubtitleTracks.any { (id, isSelected) ->
isSelected && sub.getId() == id
2022-01-07 19:27:25 +00:00
}
}
}
2023-09-06 18:53:43 +00:00
@SuppressLint("UnsafeOptInUsageError")
override fun getAspectRatio(): Rational? {
return exoPlayer?.videoFormat?.let { format ->
Rational(format.width, format.height)
}
}
2022-01-07 19:27:25 +00:00
override fun updateSubtitleStyle(style: SaveCaptionStyle) {
subtitleHelper.setSubStyle(style)
}
2023-09-06 18:53:43 +00:00
@SuppressLint("UnsafeOptInUsageError")
override fun saveData() {
2022-01-07 19:27:25 +00:00
Log.i(TAG, "saveData")
updatedTime()
exoPlayer?.let { exo ->
playbackPosition = exo.currentPosition
currentWindow = exo.currentWindowIndex
isPlaying = exo.isPlaying
}
}
2022-01-31 20:47:59 +00:00
private fun releasePlayer(saveTime: Boolean = true) {
2022-01-07 19:27:25 +00:00
Log.i(TAG, "releasePlayer")
2022-01-31 20:47:59 +00:00
if (saveTime)
updatedTime()
2022-01-07 19:27:25 +00:00
exoPlayer?.apply {
setPlayWhenReady(false)
stop()
release()
}
2022-05-06 11:33:10 +00:00
//simpleCache?.release()
2022-02-13 18:06:36 +00:00
currentTextRenderer = null
2022-01-07 19:27:25 +00:00
exoPlayer = null
2022-05-06 11:33:10 +00:00
//simpleCache = null
2022-01-07 19:27:25 +00:00
}
override fun onStop() {
Log.i(TAG, "onStop")
saveData()
2023-09-09 21:18:21 +00:00
handleEvent(CSPlayerEvent.Pause, PlayerEventSource.Player)
2022-06-20 13:17:34 +00:00
//releasePlayer()
2022-01-07 19:27:25 +00:00
}
override fun onPause() {
Log.i(TAG, "onPause")
saveData()
2023-09-09 21:18:21 +00:00
handleEvent(CSPlayerEvent.Pause, PlayerEventSource.Player)
2022-06-20 13:17:34 +00:00
//releasePlayer()
2022-01-07 19:27:25 +00:00
}
override fun onResume(context: Context) {
if (exoPlayer == null)
reloadPlayer(context)
}
override fun release() {
2023-10-02 15:44:06 +00:00
imageGenerator.release()
2022-01-07 19:27:25 +00:00
releasePlayer()
}
override fun setPlaybackSpeed(speed: Float) {
exoPlayer?.setPlaybackSpeed(speed)
playBackSpeed = speed
}
companion object {
/**
* Setting this variable is permanent across app sessions.
**/
var preferredAudioTrackLanguage: String? = null
get() {
2023-10-11 16:31:46 +00:00
return field ?: getKey(
"$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY",
field
)?.also {
field = it
}
}
set(value) {
setKey("$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", value)
field = value
}
2022-05-06 11:55:56 +00:00
private var simpleCache: SimpleCache? = null
var requestSubtitleUpdate: (() -> Unit)? = null
2022-02-12 18:18:43 +00:00
2023-09-06 18:53:43 +00:00
@SuppressLint("UnsafeOptInUsageError")
private fun createOnlineSource(headers: Map<String, String>): HttpDataSource.Factory {
val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT)
return source.apply {
setDefaultRequestProperties(headers)
}
}
2023-09-06 18:53:43 +00:00
@SuppressLint("UnsafeOptInUsageError")
private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory {
2022-07-28 15:49:44 +00:00
val provider = getApiFromNameNull(link.source)
val interceptor = provider?.getVideoInterceptor(link)
2022-04-02 17:50:16 +00:00
2022-05-07 11:37:35 +00:00
val source = if (interceptor == null) {
2022-05-07 11:39:36 +00:00
DefaultHttpDataSource.Factory() //TODO USE app.baseClient
.setUserAgent(USER_AGENT)
.setAllowCrossProtocolRedirects(true) //https://stackoverflow.com/questions/69040127/error-code-io-bad-http-status-exoplayer-android
2022-05-07 11:37:35 +00:00
} else {
val client = app.baseClient.newBuilder()
.addInterceptor(interceptor)
.build()
OkHttpDataSource.Factory(client).setUserAgent(USER_AGENT)
}
2022-04-02 17:50:16 +00:00
2023-02-24 17:47:54 +00:00
// Do no include empty referer, if the provider wants those they can use the header map.
2023-03-17 14:46:11 +00:00
val refererMap =
if (link.referer.isBlank()) emptyMap() else mapOf("referer" to link.referer)
2022-05-07 11:37:35 +00:00
val headers = mapOf(
"accept" to "*/*",
"sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"",
"sec-ch-ua-mobile" to "?0",
"sec-fetch-user" to "?1",
"sec-fetch-mode" to "navigate",
"sec-fetch-dest" to "video"
2023-02-24 17:47:54 +00:00
) + refererMap + link.headers // Adds the headers from the provider, e.g Authorization
2022-05-07 11:37:35 +00:00
return source.apply {
2022-01-07 19:27:25 +00:00
setDefaultRequestProperties(headers)
}
}
2023-09-09 21:18:21 +00:00
@SuppressLint("UnsafeOptInUsageError")
2022-01-07 19:27:25 +00:00
private fun Context.createOfflineSource(): DataSource.Factory {
return DefaultDataSourceFactory(this, USER_AGENT)
}
2022-01-12 16:54:19 +00:00
/*private fun getSubSources(
2022-01-07 19:27:25 +00:00
onlineSourceFactory: DataSource.Factory?,
offlineSourceFactory: DataSource.Factory?,
subHelper: PlayerSubtitleHelper,
): Pair<List<SingleSampleMediaSource>, List<SubtitleData>> {
val activeSubtitles = ArrayList<SubtitleData>()
val subSources = subHelper.getAllSubtitles().mapNotNull { sub ->
val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url))
.setMimeType(sub.mimeType)
.setLanguage("_${sub.name}")
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.build()
when (sub.origin) {
SubtitleOrigin.DOWNLOADED_FILE -> {
if (offlineSourceFactory != null) {
activeSubtitles.add(sub)
SingleSampleMediaSource.Factory(offlineSourceFactory)
.createMediaSource(subConfig, C.TIME_UNSET)
} else {
null
}
}
SubtitleOrigin.URL -> {
if (onlineSourceFactory != null) {
activeSubtitles.add(sub)
SingleSampleMediaSource.Factory(onlineSourceFactory)
.createMediaSource(subConfig, C.TIME_UNSET)
} else {
null
}
}
SubtitleOrigin.OPEN_SUBTITLES -> {
// TODO
throw NotImplementedError()
}
}
}
println("SUBSRC: ${subSources.size} activeSubtitles : ${activeSubtitles.size} of ${subHelper.getAllSubtitles().size} ")
return Pair(subSources, activeSubtitles)
2022-01-12 16:54:19 +00:00
}*/
2022-01-07 19:27:25 +00:00
2023-09-06 18:53:43 +00:00
@SuppressLint("UnsafeOptInUsageError")
2022-01-07 19:27:25 +00:00
private fun getCache(context: Context, cacheSize: Long): SimpleCache? {
return try {
val databaseProvider = StandaloneDatabaseProvider(context)
SimpleCache(
File(
context.cacheDir, "exoplayer"
).also { deleteFileOnExit(it) }, // Ensures always fresh file
2022-01-07 19:27:25 +00:00
LeastRecentlyUsedCacheEvictor(cacheSize),
databaseProvider
)
} catch (e: Exception) {
logError(e)
null
}
}
private fun getMediaItemBuilder(mimeType: String):
MediaItem.Builder {
return MediaItem.Builder()
//Replace needed for android 6.0.0 https://github.com/google/ExoPlayer/issues/5983
.setMimeType(mimeType)
}
private fun getMediaItem(mimeType: String, uri: Uri): MediaItem {
return getMediaItemBuilder(mimeType).setUri(uri).build()
}
private fun getMediaItem(mimeType: String, url: String): MediaItem {
return getMediaItemBuilder(mimeType).setUri(url).build()
}
2023-09-06 18:53:43 +00:00
@SuppressLint("UnsafeOptInUsageError")
private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector {
2022-01-07 19:27:25 +00:00
val trackSelector = DefaultTrackSelector(context)
trackSelector.parameters = trackSelector.buildUponParameters()
// This will not force higher quality videos to fail
// but will make the m3u8 pick the correct preferred
.setMaxVideoSize(Int.MAX_VALUE, maxVideoHeight ?: Int.MAX_VALUE)
.setPreferredAudioLanguage(null)
2022-01-07 19:27:25 +00:00
.build()
return trackSelector
}
2022-02-13 18:06:36 +00:00
var currentTextRenderer: CustomTextRenderer? = null
2022-02-12 18:18:43 +00:00
2023-09-06 18:53:43 +00:00
@SuppressLint("UnsafeOptInUsageError")
2022-01-07 19:27:25 +00:00
private fun buildExoPlayer(
context: Context,
mediaItemSlices: List<MediaItemSlice>,
subSources: List<SingleSampleMediaSource>,
2022-01-07 19:27:25 +00:00
currentWindow: Int,
playbackPosition: Long,
playBackSpeed: Float,
2022-02-27 01:03:01 +00:00
subtitleOffset: Long,
cacheSize: Long,
2022-03-16 21:31:21 +00:00
videoBufferMs: Long,
2022-01-07 19:27:25 +00:00
playWhenReady: Boolean = true,
cacheFactory: CacheDataSource.Factory? = null,
trackSelector: TrackSelector? = null,
/**
* Sets the m3u8 preferred video quality, will not force stop anything with higher quality.
* Does not work if trackSelector is defined.
**/
maxVideoHeight: Int? = null
2022-01-07 19:27:25 +00:00
): ExoPlayer {
val exoPlayerBuilder =
ExoPlayer.Builder(context)
.setRenderersFactory { eventHandler, videoRendererEventListener, audioRendererEventListener, textRendererOutput, metadataRendererOutput ->
2023-03-17 14:46:11 +00:00
DefaultRenderersFactory(context).apply {
setEnableDecoderFallback(true)
2023-03-17 14:46:11 +00:00
// Enable Ffmpeg extension
setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON)
2023-03-17 14:46:11 +00:00
}.createRenderers(
eventHandler,
videoRendererEventListener,
audioRendererEventListener,
textRendererOutput,
metadataRendererOutput
).map {
if (it is TextRenderer) {
2023-09-14 10:53:35 +00:00
val currentTextRenderer = CustomTextRenderer(
subtitleOffset,
textRendererOutput,
eventHandler.looper,
CustomSubtitleDecoderFactory()
2023-09-14 10:53:35 +00:00
).also { this.currentTextRenderer = it }
currentTextRenderer
} else it
}.toTypedArray()
}
.setTrackSelector(
trackSelector ?: getTrackSelector(
context,
maxVideoHeight
)
)
// Allows any seeking to be +- 0.3s to allow for faster seeking
.setSeekParameters(SeekParameters(toleranceBeforeUs, toleranceAfterUs))
.setLoadControl(
DefaultLoadControl.Builder()
2022-03-04 17:49:36 +00:00
.setTargetBufferBytes(
2022-05-07 11:37:35 +00:00
if (cacheSize <= 0) {
2022-03-04 17:49:36 +00:00
DefaultLoadControl.DEFAULT_TARGET_BUFFER_BYTES
} else {
if (cacheSize > Int.MAX_VALUE) Int.MAX_VALUE else cacheSize.toInt()
}
)
.setBackBuffer(
30000,
true
)
.setBufferDurationsMs(
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
2022-05-07 11:37:35 +00:00
if (videoBufferMs <= 0) {
2022-03-16 21:31:21 +00:00
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS
} else {
videoBufferMs.toInt()
},
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
).build()
)
2022-01-07 19:27:25 +00:00
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) {
2023-09-06 18:53:43 +00:00
val item = mediaItemSlices.first()
item.drm?.let { drm ->
val drmCallback =
LocalMediaDrmCallback("{\"keys\":[{\"kty\":\"${drm.kty}\",\"k\":\"${drm.key}\",\"kid\":\"${drm.kid}\"}],\"type\":\"temporary\"}".toByteArray())
val manager = DefaultDrmSessionManager.Builder()
.setPlayClearSamplesWithoutKeys(true)
.setMultiSession(false)
.setKeyRequestParameters(drm.keyRequestParameters)
.setUuidAndExoMediaDrmProvider(drm.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(drmCallback)
val manifestDataSourceFactory = DefaultHttpDataSource.Factory()
DashMediaSource.Factory(manifestDataSourceFactory)
.setDrmSessionManagerProvider { manager }
.createMediaSource(item.mediaItem)
} ?: run {
factory.createMediaSource(item.mediaItem)
}
} else {
val source = ConcatenatingMediaSource()
2023-09-06 18:53:43 +00:00
mediaItemSlices.map { item ->
source.addMediaSource(
// The duration MUST be known for it to work properly, see https://github.com/google/ExoPlayer/issues/4727
ClippingMediaSource(
2023-09-06 18:53:43 +00:00
factory.createMediaSource(item.mediaItem),
item.durationUs
)
)
}
source
}
2022-01-07 19:27:25 +00:00
//println("PLAYBACK POS $playbackPosition")
2022-01-07 19:27:25 +00:00
return exoPlayerBuilder.build().apply {
setPlayWhenReady(playWhenReady)
seekTo(currentWindow, playbackPosition)
setMediaSource(
MergingMediaSource(
videoMediaSource, *subSources.toTypedArray()
),
playbackPosition
)
setHandleAudioBecomingNoisy(true)
setPlaybackSpeed(playBackSpeed)
}
}
}
private fun getCurrentTimestamp(writePosition: Long? = null): EpisodeSkip.SkipStamp? {
val position = writePosition ?: this@CS3IPlayer.getPosition() ?: return null
for (lastTimeStamp in lastTimeStamps) {
if (lastTimeStamp.startMs <= position && (position + (toleranceBeforeUs / 1000L) + 1) < lastTimeStamp.endMs) {
return lastTimeStamp
}
}
return null
}
2023-09-09 21:18:21 +00:00
fun updatedTime(
writePosition: Long? = null,
source: PlayerEventSource = PlayerEventSource.Player
) {
val position = writePosition ?: exoPlayer?.currentPosition
getCurrentTimestamp(position)?.let { timestamp ->
2023-09-09 21:18:21 +00:00
event(TimestampInvokedEvent(timestamp, source))
}
2022-01-07 19:27:25 +00:00
val duration = exoPlayer?.contentDuration
if (duration != null && position != null) {
2023-09-09 21:18:21 +00:00
event(
PositionEvent(
source,
fromMs = exoPlayer?.currentPosition ?: 0,
position,
duration
)
)
2022-01-07 19:27:25 +00:00
}
}
2023-09-09 21:18:21 +00:00
override fun seekTime(time: Long, source: PlayerEventSource) {
exoPlayer?.seekTime(time, source)
2022-01-07 19:27:25 +00:00
}
2023-09-09 21:18:21 +00:00
override fun seekTo(time: Long, source: PlayerEventSource) {
updatedTime(time, source)
2022-01-07 19:27:25 +00:00
exoPlayer?.seekTo(time)
}
2023-09-09 21:18:21 +00:00
private fun ExoPlayer.seekTime(time: Long, source: PlayerEventSource) {
updatedTime(currentPosition + time, source)
2022-01-07 19:27:25 +00:00
seekTo(currentPosition + time)
}
2023-09-09 21:18:21 +00:00
override fun handleEvent(event: CSPlayerEvent, source: PlayerEventSource) {
2022-01-07 19:27:25 +00:00
Log.i(TAG, "handleEvent ${event.name}")
try {
exoPlayer?.apply {
when (event) {
CSPlayerEvent.Play -> {
2023-09-09 21:18:21 +00:00
event(PlayEvent(source))
2022-01-07 19:27:25 +00:00
play()
}
2022-01-07 19:27:25 +00:00
CSPlayerEvent.Pause -> {
2023-09-09 21:18:21 +00:00
event(PauseEvent(source))
2022-01-07 19:27:25 +00:00
pause()
}
2022-01-07 19:27:25 +00:00
CSPlayerEvent.ToggleMute -> {
if (volume <= 0) {
//is muted
volume = lastMuteVolume
} else {
// is not muted
lastMuteVolume = volume
volume = 0f
}
}
2022-01-07 19:27:25 +00:00
CSPlayerEvent.PlayPauseToggle -> {
if (isPlaying) {
2023-09-09 21:18:21 +00:00
handleEvent(CSPlayerEvent.Pause, source)
2022-01-07 19:27:25 +00:00
} else {
2023-09-09 21:18:21 +00:00
handleEvent(CSPlayerEvent.Play, source)
2022-01-07 19:27:25 +00:00
}
}
2023-09-09 21:18:21 +00:00
CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source)
CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source)
2023-10-02 15:44:06 +00:00
CSPlayerEvent.NextEpisode -> event(
EpisodeSeekEvent(
offset = 1,
source = source
)
)
CSPlayerEvent.PrevEpisode -> event(
EpisodeSeekEvent(
offset = -1,
source = source
)
)
CSPlayerEvent.SkipCurrentChapter -> {
//val dur = this@CS3IPlayer.getDuration() ?: return@apply
getCurrentTimestamp()?.let { lastTimeStamp ->
if (lastTimeStamp.skipToNextEpisode) {
2023-09-09 21:18:21 +00:00
handleEvent(CSPlayerEvent.NextEpisode, source)
} else {
seekTo(lastTimeStamp.endMs + 1L)
}
2023-09-09 21:18:21 +00:00
event(TimestampSkippedEvent(timestamp = lastTimeStamp, source = source))
}
}
2022-01-07 19:27:25 +00:00
}
}
2023-09-09 21:18:21 +00:00
} catch (t: Throwable) {
Log.e(TAG, "handleEvent error", t)
event(ErrorEvent(t))
2022-01-07 19:27:25 +00:00
}
}
private fun loadExo(
context: Context,
mediaSlices: List<MediaItemSlice>,
subSources: List<SingleSampleMediaSource>,
2022-01-07 19:27:25 +00:00
cacheFactory: CacheDataSource.Factory? = null
) {
Log.i(TAG, "loadExo")
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
val maxVideoHeight = settingsManager.getInt(
context.getString(if (context.isUsingMobileData()) com.lagradost.cloudstream3.R.string.quality_pref_mobile_data_key else com.lagradost.cloudstream3.R.string.quality_pref_key),
Int.MAX_VALUE
)
2022-01-07 19:27:25 +00:00
try {
hasUsedFirstRender = false
// ye this has to be a val for whatever reason
// this makes no sense
exoPlayer = buildExoPlayer(
context,
mediaSlices,
2022-01-07 19:27:25 +00:00
subSources,
currentWindow,
playbackPosition,
playBackSpeed,
cacheSize = cacheSize,
2022-03-16 21:31:21 +00:00
videoBufferMs = videoBufferMs,
2022-01-07 19:27:25 +00:00
playWhenReady = isPlaying, // this keep the current state of the player
2022-02-13 18:06:36 +00:00
cacheFactory = cacheFactory,
subtitleOffset = currentSubtitleOffset,
maxVideoHeight = maxVideoHeight
2022-01-07 19:27:25 +00:00
)
2022-02-12 18:18:43 +00:00
requestSubtitleUpdate = ::reloadSubs
2023-09-09 21:18:21 +00:00
event(PlayerAttachedEvent(exoPlayer))
2022-01-07 19:27:25 +00:00
exoPlayer?.prepare()
exoPlayer?.let { exo ->
2023-09-09 21:18:21 +00:00
event(StatusEvent(CSPlayerLoading.IsBuffering, CSPlayerLoading.IsBuffering))
2022-01-07 19:27:25 +00:00
isPlaying = exo.isPlaying
}
2023-09-09 21:18:21 +00:00
2022-01-07 19:27:25 +00:00
exoPlayer?.addListener(object : Player.Listener {
override fun onTracksChanged(tracks: Tracks) {
2022-05-20 18:25:56 +00:00
normalSafeApiCall {
val textTracks = tracks.groups.filter { it.type == TRACK_TYPE_TEXT }
2022-05-20 18:25:56 +00:00
playerSelectedSubtitleTracks =
textTracks.map { group ->
group.getFormats().mapNotNull { (format, _) ->
(format.id ?: return@mapNotNull null) to group.isSelected
}
}.flatten()
val exoPlayerReportedTracks =
tracks.groups.filter { it.type == TRACK_TYPE_TEXT }.getFormats()
.mapNotNull { (format, _) ->
// Filter out non subs, already used subs and subs without languages
if (format.id == null ||
format.language == null ||
format.language?.startsWith("-") == true
) return@mapNotNull null
return@mapNotNull SubtitleData(
// Nicer looking displayed names
fromTwoLettersToLanguage(format.language!!)
?: format.language!!,
// See setPreferredTextLanguage
format.id!!,
SubtitleOrigin.EMBEDDED_IN_VIDEO,
format.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP,
2023-11-12 15:36:21 +00:00
emptyMap(),
format.language
)
}
2022-05-20 18:25:56 +00:00
2023-09-09 21:18:21 +00:00
event(EmbeddedSubtitlesFetchedEvent(tracks = exoPlayerReportedTracks))
event(TracksChangedEvent())
event(SubtitlesUpdatedEvent())
2022-05-20 18:25:56 +00:00
}
2022-01-07 19:27:25 +00:00
}
2023-09-09 21:18:21 +00:00
@SuppressLint("UnsafeOptInUsageError")
2022-01-07 19:27:25 +00:00
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
exoPlayer?.let { exo ->
2023-09-09 21:18:21 +00:00
event(
StatusEvent(
wasPlaying = if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused,
isPlaying = if (playbackState == Player.STATE_BUFFERING) CSPlayerLoading.IsBuffering else if (exo.isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused
2022-01-07 19:27:25 +00:00
)
)
isPlaying = exo.isPlaying
}
2022-06-16 01:04:24 +00:00
when (playbackState) {
Player.STATE_READY -> {
onRenderFirst()
}
2022-06-16 01:04:24 +00:00
else -> {}
}
2022-01-07 19:27:25 +00:00
if (playWhenReady) {
when (playbackState) {
Player.STATE_READY -> {
2022-06-18 13:23:56 +00:00
2022-01-07 19:27:25 +00:00
}
2022-01-07 19:27:25 +00:00
Player.STATE_ENDED -> {
2023-09-09 21:18:21 +00:00
event(VideoEndedEvent())
2022-01-07 19:27:25 +00:00
}
2022-01-07 19:27:25 +00:00
Player.STATE_BUFFERING -> {
2023-09-09 21:18:21 +00:00
updatedTime(source = PlayerEventSource.Player)
2022-01-07 19:27:25 +00:00
}
2022-01-07 19:27:25 +00:00
Player.STATE_IDLE -> {
2023-09-09 21:18:21 +00:00
2022-01-07 19:27:25 +00:00
}
2022-01-07 19:27:25 +00:00
else -> Unit
}
}
}
override fun onPlayerError(error: PlaybackException) {
// If the Network fails then ignore the exception if the duration is set.
// This is to switch mirrors automatically if the stream has not been fetched, but
// allow playing the buffer without internet as then the duration is fetched.
when {
error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED
&& exoPlayer?.duration != TIME_UNSET -> {
exoPlayer?.prepare()
}
error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> {
// Re-initialize player at the current live window default position.
exoPlayer?.seekToDefaultPosition()
exoPlayer?.prepare()
}
else -> {
2023-09-09 21:18:21 +00:00
event(ErrorEvent(error))
}
}
2022-01-07 19:27:25 +00:00
super.onPlayerError(error)
}
2022-02-11 14:04:03 +00:00
//override fun onCues(cues: MutableList<Cue>) {
2022-02-27 01:03:01 +00:00
// super.onCues(cues.map { cue -> cue.buildUpon().setText("Hello world").setSize(Cue.DIMEN_UNSET).build() })
2022-02-11 14:04:03 +00:00
//}
2022-06-16 01:04:24 +00:00
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
if (isPlaying) {
2023-09-09 21:18:21 +00:00
event(RequestAudioFocusEvent())
2022-06-16 01:04:24 +00:00
onRenderFirst()
}
}
2022-02-12 18:18:43 +00:00
2022-06-16 01:04:24 +00:00
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
when (playbackState) {
Player.STATE_READY -> {
2022-01-31 19:39:24 +00:00
}
2022-06-16 01:04:24 +00:00
Player.STATE_ENDED -> {
// Only play next episode if autoplay is on (default)
if (PreferenceManager.getDefaultSharedPreferences(context)
?.getBoolean(
context.getString(com.lagradost.cloudstream3.R.string.autoplay_next_key),
true
) == true
) {
2023-09-09 21:18:21 +00:00
handleEvent(
CSPlayerEvent.NextEpisode,
source = PlayerEventSource.Player
)
}
2022-06-16 01:04:24 +00:00
}
2022-06-16 01:04:24 +00:00
Player.STATE_BUFFERING -> {
2023-09-09 21:18:21 +00:00
updatedTime(source = PlayerEventSource.Player)
2022-01-07 19:27:25 +00:00
}
2022-06-16 01:04:24 +00:00
Player.STATE_IDLE -> {
// IDLE
}
2022-06-16 01:04:24 +00:00
else -> Unit
2022-01-07 19:27:25 +00:00
}
2022-06-16 01:04:24 +00:00
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
super.onVideoSizeChanged(videoSize)
2023-09-09 21:18:21 +00:00
event(ResizedEvent(height = videoSize.height, width = videoSize.width))
2022-06-16 01:04:24 +00:00
}
override fun onRenderedFirstFrame() {
2022-01-07 19:27:25 +00:00
super.onRenderedFirstFrame()
2022-06-16 01:04:24 +00:00
onRenderFirst()
2023-09-09 21:18:21 +00:00
updatedTime(source = PlayerEventSource.Player)
2022-01-07 19:27:25 +00:00
}
})
2023-09-09 21:18:21 +00:00
} catch (t: Throwable) {
Log.e(TAG, "loadExo error", t)
event(ErrorEvent(t))
2022-01-07 19:27:25 +00:00
}
}
private var lastTimeStamps: List<EpisodeSkip.SkipStamp> = emptyList()
2023-09-06 18:53:43 +00:00
@SuppressLint("UnsafeOptInUsageError")
override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) {
lastTimeStamps = timeStamps
timeStamps.forEach { timestamp ->
exoPlayer?.createMessage { _, _ ->
2023-09-09 21:18:21 +00:00
updatedTime(source = PlayerEventSource.Player)
//if (payload is EpisodeSkip.SkipStamp) // this should always be true
// onTimestampInvoked?.invoke(payload)
}
?.setLooper(Looper.getMainLooper())
?.setPosition(timestamp.startMs)
//?.setPayload(timestamp)
?.setDeleteAfterDelivery(false)
?.send()
}
2023-09-09 21:18:21 +00:00
updatedTime(source = PlayerEventSource.Player)
}
2023-09-06 18:53:43 +00:00
@SuppressLint("UnsafeOptInUsageError")
2022-06-16 01:04:24 +00:00
fun onRenderFirst() {
if (hasUsedFirstRender) { // this insures that we only call this once per player load
return
}
Log.i(TAG, "Rendered first frame")
hasUsedFirstRender = true
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
&& exoPlayer?.isCurrentMediaItemLive != true
} ?: false
2022-06-16 01:04:24 +00:00
if (invalid) {
releasePlayer(saveTime = false)
2023-09-09 21:18:21 +00:00
event(ErrorEvent(InvalidFileException("Too short playback")))
return
}
setPreferredSubtitles(currentSubtitles)
val format = exoPlayer?.videoFormat
val width = format?.width
val height = format?.height
if (height != null && width != null) {
2023-09-09 21:18:21 +00:00
event(ResizedEvent(width = width, height = height))
updatedTime()
exoPlayer?.apply {
requestedListeningPercentages?.forEach { percentage ->
createMessage { _, _ ->
updatedTime()
2022-06-16 01:04:24 +00:00
}
.setLooper(Looper.getMainLooper())
.setPosition(contentDuration * percentage / 100)
// .setPayload(customPayloadData)
.setDeleteAfterDelivery(false)
.send()
2022-06-16 01:04:24 +00:00
}
}
}
}
2022-01-07 19:27:25 +00:00
private fun loadOfflinePlayer(context: Context, data: ExtractorUri) {
Log.i(TAG, "loadOfflinePlayer")
try {
currentDownloadedFile = data
val mediaItem = getMediaItem(MimeTypes.VIDEO_MP4, data.uri)
val offlineSourceFactory = context.createOfflineSource()
val onlineSourceFactory = createOnlineSource(emptyMap())
2022-01-12 16:54:19 +00:00
val (subSources, activeSubtitles) = getSubSources(
onlineSourceFactory = onlineSourceFactory,
offlineSourceFactory = offlineSourceFactory,
subtitleHelper,
2022-01-07 19:27:25 +00:00
)
2022-01-07 19:27:25 +00:00
subtitleHelper.setActiveSubtitles(activeSubtitles.toSet())
loadExo(context, listOf(MediaItemSlice(mediaItem, Long.MIN_VALUE)), subSources)
2023-09-09 21:18:21 +00:00
} catch (t: Throwable) {
Log.e(TAG, "loadOfflinePlayer error", t)
event(ErrorEvent(t))
2022-01-07 19:27:25 +00:00
}
}
2023-09-06 18:53:43 +00:00
@SuppressLint("UnsafeOptInUsageError")
private fun getSubSources(
onlineSourceFactory: HttpDataSource.Factory?,
offlineSourceFactory: DataSource.Factory?,
subHelper: PlayerSubtitleHelper,
): Pair<List<SingleSampleMediaSource>, List<SubtitleData>> {
val activeSubtitles = ArrayList<SubtitleData>()
val subSources = subHelper.getAllSubtitles().mapNotNull { sub ->
val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url))
.setMimeType(sub.mimeType)
.setLanguage("_${sub.name}")
.setId(sub.getId())
.setSelectionFlags(0)
.build()
when (sub.origin) {
SubtitleOrigin.DOWNLOADED_FILE -> {
if (offlineSourceFactory != null) {
activeSubtitles.add(sub)
SingleSampleMediaSource.Factory(offlineSourceFactory)
.createMediaSource(subConfig, TIME_UNSET)
} else {
null
}
}
SubtitleOrigin.URL -> {
if (onlineSourceFactory != null) {
activeSubtitles.add(sub)
SingleSampleMediaSource.Factory(onlineSourceFactory.apply {
if (sub.headers.isNotEmpty())
this.setDefaultRequestProperties(sub.headers)
})
.createMediaSource(subConfig, TIME_UNSET)
} else {
null
}
}
2022-05-20 18:25:56 +00:00
SubtitleOrigin.EMBEDDED_IN_VIDEO -> {
if (offlineSourceFactory != null) {
activeSubtitles.add(sub)
SingleSampleMediaSource.Factory(offlineSourceFactory)
.createMediaSource(subConfig, TIME_UNSET)
2022-05-20 18:25:56 +00:00
} else {
null
}
}
}
}
return Pair(subSources, activeSubtitles)
}
override fun isActive(): Boolean {
return exoPlayer != null
}
2023-09-06 18:53:43 +00:00
@SuppressLint("UnsafeOptInUsageError")
2022-01-07 19:27:25 +00:00
private fun loadOnlinePlayer(context: Context, link: ExtractorLink) {
2022-06-16 01:04:24 +00:00
Log.i(TAG, "loadOnlinePlayer $link")
2022-01-07 19:27:25 +00:00
try {
currentLink = link
if (ignoreSSL) {
// Disables ssl check
val sslContext: SSLContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(SSLTrustManager()), java.security.SecureRandom())
sslContext.createSSLEngine()
HttpsURLConnection.setDefaultHostnameVerifier { _: String, _: SSLSession ->
true
}
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
}
2023-09-06 18:53:43 +00:00
val mime = when (link.type) {
ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8
ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD
ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4
ExtractorLinkType.TORRENT -> throw IllegalArgumentException("No torrent support")
ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet support")
2022-01-07 19:27:25 +00:00
}
2023-09-06 18:53:43 +00:00
val mediaItems = when (link) {
is ExtractorLinkPlayList -> link.playlist.map {
MediaItemSlice(getMediaItem(mime, it.url), it.durationUs)
}
2023-09-06 18:53:43 +00:00
is DrmExtractorLink -> {
listOf(
// Single sliced list with unset length
MediaItemSlice(
getMediaItem(mime, link.url), Long.MIN_VALUE,
drm = DrmMetadata(
kid = link.kid,
key = link.key,
2023-09-06 20:42:22 +00:00
uuid = link.uuid,
2023-09-06 18:53:43 +00:00
kty = link.kty,
keyRequestParameters = link.keyRequestParameters
)
)
)
}
else -> listOf(
// Single sliced list with unset length
MediaItemSlice(getMediaItem(mime, link.url), Long.MIN_VALUE)
)
}
2022-01-07 19:27:25 +00:00
val onlineSourceFactory = createOnlineSource(link)
val offlineSourceFactory = context.createOfflineSource()
val (subSources, activeSubtitles) = getSubSources(
onlineSourceFactory = onlineSourceFactory,
offlineSourceFactory = offlineSourceFactory,
subtitleHelper
2022-01-07 19:27:25 +00:00
)
2022-01-07 19:27:25 +00:00
subtitleHelper.setActiveSubtitles(activeSubtitles.toSet())
2022-01-08 19:39:22 +00:00
if (simpleCache == null)
simpleCache = getCache(context, simpleCacheSize)
2022-01-07 19:27:25 +00:00
val cacheFactory = CacheDataSource.Factory().apply {
simpleCache?.let { setCache(it) }
setUpstreamDataSourceFactory(onlineSourceFactory)
}
loadExo(context, mediaItems, subSources, cacheFactory)
2023-09-09 21:18:21 +00:00
} catch (t: Throwable) {
Log.e(TAG, "loadOnlinePlayer error", t)
event(ErrorEvent(t))
2022-01-07 19:27:25 +00:00
}
}
override fun reloadPlayer(context: Context) {
Log.i(TAG, "reloadPlayer")
2023-09-14 10:53:35 +00:00
releasePlayer(false)
2022-01-07 19:27:25 +00:00
currentLink?.let {
loadOnlinePlayer(context, it)
} ?: currentDownloadedFile?.let {
loadOfflinePlayer(context, it)
}
}
}