From 91a14dac0f19b5ff7647dd628847be1bba28d978 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 16 Jun 2022 03:04:24 +0200 Subject: [PATCH] Added trailers --- app/build.gradle | 3 + .../com/lagradost/cloudstream3/MainAPI.kt | 11 + .../lagradost/cloudstream3/MainActivity.kt | 34 ++ .../metaproviders/TmdbProvider.kt | 39 ++- .../providers/OpenSubtitlesApi.kt | 6 +- .../ui/player/AbstractPlayerFragment.kt | 18 +- .../cloudstream3/ui/player/CS3IPlayer.kt | 177 +++++++--- .../ui/player/FullScreenPlayer.kt | 73 ++-- .../cloudstream3/ui/player/IPlayer.kt | 1 + .../cloudstream3/ui/result/ResultFragment.kt | 59 +++- .../ui/result/ResultTrailerPlayer.kt | 68 ++++ app/src/main/res/layout/fragment_result.xml | 36 +- app/src/main/res/layout/fragment_trailer.xml | 33 ++ .../main/res/layout/trailer_custom_layout.xml | 330 ++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 15 files changed, 782 insertions(+), 107 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt create mode 100644 app/src/main/res/layout/fragment_trailer.xml create mode 100644 app/src/main/res/layout/trailer_custom_layout.xml diff --git a/app/build.gradle b/app/build.gradle index fa5b8757..1cd978ee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -174,4 +174,7 @@ dependencies { // used for subtitle decoding https://github.com/albfernandez/juniversalchardet implementation 'com.github.albfernandez:juniversalchardet:2.4.0' + + // play yt + implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT' } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index a21f897b..e66787d2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -901,6 +901,17 @@ interface LoadResponse { } } + fun LoadResponse.addTrailer(trailerUrls: List?) { + if(trailerUrls == null) return + if (this.trailers == null) { + this.trailers = trailerUrls + } else { + val update = this.trailers?.toMutableList() + update?.addAll(trailerUrls) + this.trailers = update + } + } + fun LoadResponse.addImdbId(id: String?) { // TODO add imdb sync } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 8971c05f..4c3b1933 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -356,6 +356,40 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } fun test() { + //val youtubeLink = "https://www.youtube.com/watch?v=TxB48MEAmZw" + + + /* + runBlocking { + + val query = """ + query { + searchShows(search: "spider", limit: 10) { + id + name + originalName + } + } + """ + val data = + mapOf( + "query" to query, + //"variables" to + // mapOf( + // "name" to name, + // ).toJson() + ) + val txt = app.post( + "http://api.anime-skip.com/graphql", + headers = mapOf( + "X-Client-ID" to "", + "Content-Type" to "application/json", + "Accept" to "application/json", + ), + json = data + ) + println("TEXT: $txt") + }*/ /*runBlocking { //https://test.api.anime-skip.com/graphiql val txt = app.get( diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt index b3edc337..fdec2fc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt @@ -4,10 +4,12 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.LoadResponse.Companion.addActors import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.uwetrottmann.tmdb2.Tmdb import com.uwetrottmann.tmdb2.entities.* import com.uwetrottmann.tmdb2.enumerations.AppendToResponseItem +import com.uwetrottmann.tmdb2.enumerations.VideoType import retrofit2.awaitResponse import java.util.* @@ -144,6 +146,7 @@ open class TmdbProvider : MainAPI() { tags = genres?.mapNotNull { it.name } duration = episode_run_time?.average()?.toInt() rating = this@toLoadResponse.rating + addTrailer(videos.toTrailers()) recommendations = (this@toLoadResponse.recommendations ?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() } @@ -151,6 +154,19 @@ open class TmdbProvider : MainAPI() { } } + private fun Videos?.toTrailers(): List? { + return this?.results?.filter { it.type != VideoType.OPENING_CREDITS && it.type != VideoType.FEATURETTE } + ?.sortedBy { it.type?.ordinal ?: 10000 } + ?.mapNotNull { + when (it.site?.trim()?.lowercase()) { + "youtube" -> { // TODO FILL SITES + "https://www.youtube.com/watch?v=${it.key}" + } + else -> null + } + } + } + private fun Movie.toLoadResponse(): MovieLoadResponse { return newMovieLoadResponse( this.title ?: this.original_title, getUrl(id, false), TvType.Movie, TmdbLink( @@ -172,6 +188,7 @@ open class TmdbProvider : MainAPI() { tags = genres?.mapNotNull { it.name } duration = runtime rating = this@toLoadResponse.rating + addTrailer(videos.toTrailers()) recommendations = (this@toLoadResponse.recommendations ?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() } @@ -261,7 +278,16 @@ open class TmdbProvider : MainAPI() { return if (useMetaLoadResponse) { return if (isTvSeries) { - val body = tmdb.tvService().tv(id, "en-US", AppendToResponse(AppendToResponseItem.EXTERNAL_IDS)).awaitResponse().body() + val body = tmdb.tvService() + .tv( + id, + "en-US", + AppendToResponse( + AppendToResponseItem.EXTERNAL_IDS, + AppendToResponseItem.VIDEOS + ) + ) + .awaitResponse().body() val response = body?.toLoadResponse() if (response != null) { if (response.recommendations.isNullOrEmpty()) @@ -280,7 +306,16 @@ open class TmdbProvider : MainAPI() { response } else { - val body = tmdb.moviesService().summary(id, "en-US", AppendToResponse(AppendToResponseItem.EXTERNAL_IDS)).awaitResponse().body() + val body = tmdb.moviesService() + .summary( + id, + "en-US", + AppendToResponse( + AppendToResponseItem.EXTERNAL_IDS, + AppendToResponseItem.VIDEOS + ) + ) + .awaitResponse().body() val response = body?.toLoadResponse() if (response != null) { if (response.recommendations.isNullOrEmpty()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt index 44140b1c..ae0c86e4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -93,7 +93,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubProv } private suspend fun initLogin(username: String, password: String): Boolean { - Log.i(TAG, "DATA = [$username] [$password]") + //Log.i(TAG, "DATA = [$username] [$password]") val response = app.post( url = "$host/login", headers = mapOf( @@ -105,8 +105,8 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubProv "password" to password ) ) - Log.i(TAG, "Responsecode = ${response.code}") - Log.i(TAG, "Result => ${response.text}") + //Log.i(TAG, "Responsecode = ${response.code}") + //Log.i(TAG, "Result => ${response.text}") if (response.isSuccessful) { AppUtils.tryParseJson(response.text)?.let { token -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 339553b5..d0371a4b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -68,6 +68,8 @@ abstract class AbstractPlayerFragment( var subStyle: SaveCaptionStyle? = null var subView: SubtitleView? = null var isBuffering = true + protected open var hasPipModeSupport = true + @LayoutRes protected var layout: Int = R.layout.fragment_player @@ -154,7 +156,7 @@ abstract class AbstractPlayerFragment( } } - canEnterPipMode = isPlayingRightNow + canEnterPipMode = isPlayingRightNow && hasPipModeSupport if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isInPIPMode) { activity?.let { act -> PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow) @@ -213,7 +215,13 @@ abstract class AbstractPlayerFragment( throw NotImplementedError() } - private fun playerError(exception: Exception) { + private fun requestAudioFocus() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + activity?.requestLocalAudioFocus(AppUtils.getFocusRequest()) + } + } + + open fun playerError(exception: Exception) { val ctx = context ?: return when (exception) { is PlaybackException -> { @@ -267,12 +275,6 @@ abstract class AbstractPlayerFragment( } } - private fun requestAudioFocus() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - activity?.requestLocalAudioFocus(AppUtils.getFocusRequest()) - } - } - private fun onSubStyleChanged(style: SaveCaptionStyle) { if (player is CS3IPlayer) { player.updateSubtitleStyle(style) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index e11ea630..06a2c3a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -1,11 +1,17 @@ package com.lagradost.cloudstream3.ui.player +import android.annotation.SuppressLint import android.content.Context import android.net.Uri import android.os.Handler import android.os.Looper import android.util.Log +import android.util.SparseArray import android.widget.FrameLayout +import androidx.core.util.forEach +import at.huber.youtubeExtractor.VideoMeta +import at.huber.youtubeExtractor.YouTubeExtractor +import at.huber.youtubeExtractor.YtFile import com.google.android.exoplayer2.* import com.google.android.exoplayer2.database.StandaloneDatabaseProvider import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource @@ -23,6 +29,7 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor import com.google.android.exoplayer2.upstream.cache.SimpleCache import com.google.android.exoplayer2.util.MimeTypes +import com.google.android.exoplayer2.video.VideoSize import com.lagradost.cloudstream3.APIHolder.getApiFromName import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app @@ -31,6 +38,7 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorUri +import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File import javax.net.ssl.HttpsURLConnection @@ -153,7 +161,8 @@ class CS3IPlayer : IPlayer { data: ExtractorUri?, startPosition: Long?, subtitles: Set, - subtitle: SubtitleData? + subtitle: SubtitleData?, + autoPlay: Boolean? ) { Log.i(TAG, "loadPlayer") if (sameEpisode) { @@ -168,7 +177,7 @@ class CS3IPlayer : IPlayer { } // we want autoplay because of TV and UX - isPlaying = true + isPlaying = autoPlay ?: isPlaying // release the current exoplayer and cache releasePlayer() @@ -322,6 +331,7 @@ class CS3IPlayer : IPlayer { } companion object { + private var ytVideos: MutableMap = mutableMapOf() private var simpleCache: SimpleCache? = null var requestSubtitleUpdate: (() -> Unit)? = null @@ -686,6 +696,14 @@ class CS3IPlayer : IPlayer { isPlaying = exo.isPlaying } + when (playbackState) { + Player.STATE_READY -> { + onRenderFirst() + } + else -> {} + } + + if (playWhenReady) { when (playbackState) { Player.STATE_READY -> { @@ -715,45 +733,41 @@ class CS3IPlayer : IPlayer { // super.onCues(cues.map { cue -> cue.buildUpon().setText("Hello world").setSize(Cue.DIMEN_UNSET).build() }) //} + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + if (isPlaying) { + onRenderFirst() + } + } + + override fun onPlaybackStateChanged(playbackState: Int) { + super.onPlaybackStateChanged(playbackState) + when (playbackState) { + Player.STATE_READY -> { + requestAutoFocus?.invoke() + } + Player.STATE_ENDED -> { + handleEvent(CSPlayerEvent.NextEpisode) + } + Player.STATE_BUFFERING -> { + updatedTime() + } + Player.STATE_IDLE -> { + // IDLE + } + else -> Unit + } + } + + override fun onVideoSizeChanged(videoSize: VideoSize) { + super.onVideoSizeChanged(videoSize) + playerDimensionsLoaded?.invoke(Pair(videoSize.width, videoSize.height)) + } + override fun onRenderedFirstFrame() { updatedTime() - - 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 - } ?: false - if (invalid) { - releasePlayer(saveTime = false) - playerError?.invoke(InvalidFileException("Too short playback")) - return - } - - setPreferredSubtitles(currentSubtitles) - hasUsedFirstRender = true - val format = exoPlayer?.videoFormat - val width = format?.width - val height = format?.height - if (height != null && width != null) { - playerDimensionsLoaded?.invoke(Pair(width, height)) - updatedTime() - exoPlayer?.apply { - requestedListeningPercentages?.forEach { percentage -> - createMessage { _, _ -> - updatedTime() - } - .setLooper(Looper.getMainLooper()) - .setPosition( /* positionMs= */contentDuration * percentage / 100) - // .setPayload(customPayloadData) - .setDeleteAfterDelivery(false) - .send() - } - } - } - } super.onRenderedFirstFrame() + onRenderFirst() } }) } catch (e: Exception) { @@ -762,6 +776,45 @@ 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 + } ?: false + + if (invalid) { + releasePlayer(saveTime = false) + playerError?.invoke(InvalidFileException("Too short playback")) + return + } + + setPreferredSubtitles(currentSubtitles) + hasUsedFirstRender = true + val format = exoPlayer?.videoFormat + val width = format?.width + val height = format?.height + if (height != null && width != null) { + playerDimensionsLoaded?.invoke(Pair(width, height)) + updatedTime() + exoPlayer?.apply { + requestedListeningPercentages?.forEach { percentage -> + createMessage { _, _ -> + updatedTime() + } + .setLooper(Looper.getMainLooper()) + .setPosition( /* positionMs= */contentDuration * percentage / 100) + // .setPayload(customPayloadData) + .setDeleteAfterDelivery(false) + .send() + } + } + } + } + } + private fun loadOfflinePlayer(context: Context, data: ExtractorUri) { Log.i(TAG, "loadOfflinePlayer") try { @@ -829,9 +882,55 @@ class CS3IPlayer : IPlayer { return Pair(subSources, activeSubtitles) } + + fun loadYtFile(context: Context, yt: YtFile) { + loadOnlinePlayer( + context, + ExtractorLink( + "YouTube", + "", + yt.url, + "", + yt.format?.height ?: Qualities.Unknown.value + ) + ) + } + private fun loadOnlinePlayer(context: Context, link: ExtractorLink) { - Log.i(TAG, "loadOnlinePlayer") + Log.i(TAG, "loadOnlinePlayer $link") try { + if (link.url.contains("youtube.com")) { + val ytLink = link.url.replace("/embed/", "/watch?v=") + ytVideos[ytLink]?.let { + loadYtFile(context, it) + return + } + val ytExtractor = + @SuppressLint("StaticFieldLeak") + object : YouTubeExtractor(context) { + override fun onExtractionComplete( + ytFiles: SparseArray?, + videoMeta: VideoMeta? + ) { + var yt: YtFile? = null + ytFiles?.forEach { _, value -> + if ((yt?.format?.height ?: 0) < (value.format?.height + ?: -1) && (value.format?.audioBitrate ?: -1) > 0 + ) { + yt = value + } + } + yt?.let { ytf -> + ytVideos[ytLink] = ytf + loadYtFile(context, ytf) + } + } + } + Log.i(TAG, "YouTube extraction on $ytLink") + ytExtractor.extract(ytLink) + return + } + currentLink = link if (ignoreSSL) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index a5e6a416..c0dc49e0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -51,6 +51,21 @@ import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.Vector2 import kotlinx.android.synthetic.main.player_custom_layout.* +import kotlinx.android.synthetic.main.player_custom_layout.bottom_player_bar +import kotlinx.android.synthetic.main.player_custom_layout.exo_ffwd +import kotlinx.android.synthetic.main.player_custom_layout.exo_ffwd_text +import kotlinx.android.synthetic.main.player_custom_layout.exo_progress +import kotlinx.android.synthetic.main.player_custom_layout.exo_rew +import kotlinx.android.synthetic.main.player_custom_layout.exo_rew_text +import kotlinx.android.synthetic.main.player_custom_layout.player_center_menu +import kotlinx.android.synthetic.main.player_custom_layout.player_ffwd_holder +import kotlinx.android.synthetic.main.player_custom_layout.player_holder +import kotlinx.android.synthetic.main.player_custom_layout.player_pause_play +import kotlinx.android.synthetic.main.player_custom_layout.player_pause_play_holder +import kotlinx.android.synthetic.main.player_custom_layout.player_rew_holder +import kotlinx.android.synthetic.main.player_custom_layout.player_video_bar +import kotlinx.android.synthetic.main.player_custom_layout.shadow_overlay +import kotlinx.android.synthetic.main.trailer_custom_layout.* import kotlin.math.* const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking @@ -64,6 +79,9 @@ const val DOUBLE_TAB_PAUSE_PERCENTAGE = 0.15 // in both directions // All the UI Logic for the player open class FullScreenPlayer : AbstractPlayerFragment() { + protected open var lockRotation = true + protected open var isFullScreenPlayer = true + // state of player UI protected var isShowing = false protected var isLocked = false @@ -100,11 +118,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() { // screenWidth and screenHeight does always // refer to the screen while in landscape mode - private val screenWidth: Int + protected val screenWidth: Int get() { return max(displayMetrics.widthPixels, displayMetrics.heightPixels) } - private val screenHeight: Int + protected val screenHeight: Int get() { return min(displayMetrics.widthPixels, displayMetrics.heightPixels) } @@ -159,7 +177,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { animateLayoutChanges() } - private fun animateLayoutChanges() { + protected fun animateLayoutChanges() { if (isShowing) { updateUIVisibility() } else { @@ -234,20 +252,25 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } override fun onResume() { - activity?.hideSystemUI() - activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && fullscreenNotch) { - val params = activity?.window?.attributes - params?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - activity?.window?.attributes = params + if (isFullScreenPlayer) { + activity?.hideSystemUI() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && fullscreenNotch) { + val params = activity?.window?.attributes + params?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + activity?.window?.attributes = params + } } + if (lockRotation) + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + super.onResume() } override fun onDestroy() { activity?.showSystemUI() - activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER + if (lockRotation) + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER // simply resets brightness and notch settings that might have been overridden val lp = activity?.window?.attributes @@ -336,7 +359,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } dialog.setOnDismissListener { - activity?.hideSystemUI() + if (isFullScreenPlayer) + activity?.hideSystemUI() } applyButton.setOnClickListener { dialog.dismissSafe(activity) @@ -374,9 +398,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() { act.getString(R.string.player_speed), false, { - activity?.hideSystemUI() + if (isFullScreenPlayer) + activity?.hideSystemUI() }) { index -> - activity?.hideSystemUI() + if (isFullScreenPlayer) + activity?.hideSystemUI() setPlayBackSpeed(speedsNumbers[index]) } } @@ -455,9 +481,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun onClickChange() { isShowing = !isShowing if (isShowing) { + player_intro_play?.isGone = true autoHide() } - activity?.hideSystemUI() + if (isFullScreenPlayer) + activity?.hideSystemUI() animateLayoutChanges() player_pause_play?.requestFocus() } @@ -692,7 +720,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { if (event == null || view == null) return false val currentTouch = Vector2(event.x, event.y) val startTouch = currentTouchStart - + player_intro_play?.isGone = true when (event.action) { MotionEvent.ACTION_DOWN -> { // validates if the touch is inside of the player area @@ -717,7 +745,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } MotionEvent.ACTION_UP -> { - if (isCurrentTouchValid && !isLocked) { + if (isCurrentTouchValid && !isLocked && isFullScreenPlayer) { // seek time if (swipeHorizontalEnabled && currentTouchAction == TouchAction.Time) { val startTime = currentTouchStartPlayerTime @@ -749,18 +777,18 @@ open class FullScreenPlayer : AbstractPlayerFragment() { if (doubleTapPauseEnabled) { // you can pause if your tap is in the middle of the screen when { currentTouch.x < screenWidth / 2 - (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> { - if (doubleTapEnabled) + if (doubleTapEnabled && isFullScreenPlayer) rewind() } currentTouch.x > screenWidth / 2 + (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> { - if (doubleTapEnabled) + if (doubleTapEnabled && isFullScreenPlayer) fastForward() } else -> { player.handleEvent(CSPlayerEvent.PlayPauseToggle) } } - } else if (doubleTapEnabled) { + } else if (doubleTapEnabled && isFullScreenPlayer) { if (currentTouch.x < screenWidth / 2) { rewind() } else { @@ -798,7 +826,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } MotionEvent.ACTION_MOVE -> { // if current touch is valid - if (startTouch != null && isCurrentTouchValid && !isLocked) { + if (startTouch != null && isCurrentTouchValid && !isLocked && isFullScreenPlayer) { // action is unassigned and can therefore be assigned if (currentTouchAction == null) { val diffFromStart = startTouch - currentTouch @@ -1201,6 +1229,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() { showMirrorsDialogue() } + player_intro_play?.setOnClickListener { + player_intro_play?.isGone = true + player.handleEvent(CSPlayerEvent.Play) + } + // it is !not! a bug that you cant touch the right side, it does not register inputs on navbar or status bar player_holder?.setOnTouchListener { callView, event -> return@setOnTouchListener handleMotionEvent(callView, event) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index 0f7cea4f..5dd05399 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -98,6 +98,7 @@ interface IPlayer { startPosition: Long? = null, subtitles : Set, subtitle : SubtitleData?, + autoPlay : Boolean? = true ) fun reloadPlayer(context: Context) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 8b1d9f6e..2ee9b719 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -28,7 +28,6 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView import androidx.core.widget.doOnTextChanged -import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager @@ -85,7 +84,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.setImage -import com.lagradost.cloudstream3.utils.UIHelper.setImageBlur import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFileName import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename import kotlinx.android.synthetic.main.fragment_result.* @@ -183,7 +181,7 @@ fun ResultEpisode.getWatchProgress(): Float { return (getDisplayPosition() / 1000).toFloat() / (duration / 1000).toFloat() } -class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegionsListener { +class ResultFragment : ResultTrailerPlayer() { companion object { const val URL_BUNDLE = "url" const val API_NAME_BUNDLE = "apiName" @@ -601,6 +599,53 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f)) } + var currentTrailers: List = emptyList() + var currentTrailerIndex = 0 + + override fun nextMirror() { + currentTrailerIndex++ + loadTrailer() + } + + override fun playerError(exception: Exception) { + if (player.getIsPlaying()) // because we dont want random toasts in player + super.playerError(exception) + } + + private fun loadTrailer(index: Int? = null) { + currentTrailers.getOrNull(index ?: currentTrailerIndex)?.let { trailer -> + //if(trailer.contains("youtube.com")) { // wont load in exo + // nextMirror() + // return + //} + context?.let { ctx -> + player.onPause() + player.loadPlayer( + ctx, + false, + ExtractorLink( + "", + "Trailer", + trailer, + "", + Qualities.Unknown.value + ), + null, + startPosition = 0L, + subtitles = emptySet(), + subtitle = null, + autoPlay = false + ) + } + } + } + + private fun setTrailers(trailers: List?) { + if(context?.isTvSettings() == true) return + currentTrailers = trailers ?: emptyList() + loadTrailer() + } + private fun setActors(actors: List?) { if (actors.isNullOrEmpty()) { result_cast_text?.isVisible = false @@ -777,7 +822,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio } else if (dy < -5) { result_bookmark_fab?.extend() } - result_poster_blur_holder?.translationY = -scrollY.toFloat() + //result_poster_blur_holder?.translationY = -scrollY.toFloat() }) result_back.setOnClickListener { @@ -1727,6 +1772,8 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio setRecommendations(d.recommendations, null) setActors(d.actors) + setTrailers(d.trailers) + if (syncModel.addSyncs(d.syncData)) { syncModel.updateMetaAndUser() syncModel.updateSynced() @@ -1739,7 +1786,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio val posterImageLink = d.posterUrl if (!posterImageLink.isNullOrEmpty()) { result_poster?.setImage(posterImageLink, d.posterHeaders) - result_poster_blur?.setImageBlur(posterImageLink, 10, 3, d.posterHeaders) + //result_poster_blur?.setImageBlur(posterImageLink, 10, 3, d.posterHeaders) //Full screen view of Poster image if (context?.isTrueTvSettings() == false) // Poster not clickable on tv result_poster_holder?.setOnClickListener { @@ -1768,7 +1815,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio } else { result_poster?.setImageResource(R.drawable.default_cover) - result_poster_blur?.setImageResource(R.drawable.default_cover) + //result_poster_blur?.setImageResource(R.drawable.default_cover) } result_poster_holder?.visibility = VISIBLE diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt new file mode 100644 index 00000000..66ac2dc2 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -0,0 +1,68 @@ +package com.lagradost.cloudstream3.ui.result + +import android.content.res.Configuration +import android.graphics.Rect +import android.widget.LinearLayout +import androidx.core.view.isVisible +import com.discord.panels.PanelsChildGestureRegionObserver +import com.lagradost.cloudstream3.ui.player.SubtitleData +import kotlinx.android.synthetic.main.fragment_trailer.* + +open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreenPlayer(), + PanelsChildGestureRegionObserver.GestureRegionsListener { + + override var lockRotation = false + override var isFullScreenPlayer = false + override var hasPipModeSupport = false + + companion object { + const val TAG = "RESULT_TRAILER" + } + + var playerWidthHeight: Pair? = null + + override fun nextEpisode() {} + + override fun prevEpisode() {} + + override fun playerPositionChanged(posDur: Pair) {} + + override fun nextMirror() {} + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + uiReset() + fixPlayerSize() + } + + private fun fixPlayerSize() { + playerWidthHeight?.let { (w, h) -> + val orientation = this.resources.configuration?.orientation ?: return + + val sw = if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + screenWidth + } else { + screenHeight + } + + player_background?.apply { + isVisible = true + layoutParams = + LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, sw * h / w) + } + } + } + + override fun playerDimensionsLoaded(widthHeight: Pair) { + playerWidthHeight = widthHeight + fixPlayerSize() + } + + override fun subtitlesChanged() {} + + override fun embeddedSubtitlesFetched(subtitles: List) {} + + override fun exitedPipMode() {} + + override fun onGestureRegionsUpdate(gestureRegions: List) {} +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 132f9065..cb95b991 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -117,38 +117,13 @@ android:textColor="?attr/textColor" /> - - - - - - - - + tools:visibility="visible" + android:orientation="vertical"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/trailer_custom_layout.xml b/app/src/main/res/layout/trailer_custom_layout.xml new file mode 100644 index 00000000..3bd27e2a --- /dev/null +++ b/app/src/main/res/layout/trailer_custom_layout.xml @@ -0,0 +1,330 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 36802938..775f3501 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -530,4 +530,5 @@ Remove closed captions from subtitles Remove bloat from subtitles Extras + Trailer