Added trailers

This commit is contained in:
LagradOst 2022-06-16 03:04:24 +02:00
parent 573b3488a3
commit 91a14dac0f
15 changed files with 782 additions and 107 deletions

View file

@ -174,4 +174,7 @@ dependencies {
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet // used for subtitle decoding https://github.com/albfernandez/juniversalchardet
implementation 'com.github.albfernandez:juniversalchardet:2.4.0' implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
// play yt
implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT'
} }

View file

@ -901,6 +901,17 @@ interface LoadResponse {
} }
} }
fun LoadResponse.addTrailer(trailerUrls: List<String>?) {
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?) { fun LoadResponse.addImdbId(id: String?) {
// TODO add imdb sync // TODO add imdb sync
} }

View file

@ -356,6 +356,40 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
fun test() { 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 { /*runBlocking {
//https://test.api.anime-skip.com/graphiql //https://test.api.anime-skip.com/graphiql
val txt = app.get( val txt = app.get(

View file

@ -4,10 +4,12 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.uwetrottmann.tmdb2.Tmdb import com.uwetrottmann.tmdb2.Tmdb
import com.uwetrottmann.tmdb2.entities.* import com.uwetrottmann.tmdb2.entities.*
import com.uwetrottmann.tmdb2.enumerations.AppendToResponseItem import com.uwetrottmann.tmdb2.enumerations.AppendToResponseItem
import com.uwetrottmann.tmdb2.enumerations.VideoType
import retrofit2.awaitResponse import retrofit2.awaitResponse
import java.util.* import java.util.*
@ -144,6 +146,7 @@ open class TmdbProvider : MainAPI() {
tags = genres?.mapNotNull { it.name } tags = genres?.mapNotNull { it.name }
duration = episode_run_time?.average()?.toInt() duration = episode_run_time?.average()?.toInt()
rating = this@toLoadResponse.rating rating = this@toLoadResponse.rating
addTrailer(videos.toTrailers())
recommendations = (this@toLoadResponse.recommendations recommendations = (this@toLoadResponse.recommendations
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() } ?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
@ -151,6 +154,19 @@ open class TmdbProvider : MainAPI() {
} }
} }
private fun Videos?.toTrailers(): List<String>? {
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 { private fun Movie.toLoadResponse(): MovieLoadResponse {
return newMovieLoadResponse( return newMovieLoadResponse(
this.title ?: this.original_title, getUrl(id, false), TvType.Movie, TmdbLink( this.title ?: this.original_title, getUrl(id, false), TvType.Movie, TmdbLink(
@ -172,6 +188,7 @@ open class TmdbProvider : MainAPI() {
tags = genres?.mapNotNull { it.name } tags = genres?.mapNotNull { it.name }
duration = runtime duration = runtime
rating = this@toLoadResponse.rating rating = this@toLoadResponse.rating
addTrailer(videos.toTrailers())
recommendations = (this@toLoadResponse.recommendations recommendations = (this@toLoadResponse.recommendations
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() } ?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
@ -261,7 +278,16 @@ open class TmdbProvider : MainAPI() {
return if (useMetaLoadResponse) { return if (useMetaLoadResponse) {
return if (isTvSeries) { 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() val response = body?.toLoadResponse()
if (response != null) { if (response != null) {
if (response.recommendations.isNullOrEmpty()) if (response.recommendations.isNullOrEmpty())
@ -280,7 +306,16 @@ open class TmdbProvider : MainAPI() {
response response
} else { } 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() val response = body?.toLoadResponse()
if (response != null) { if (response != null) {
if (response.recommendations.isNullOrEmpty()) if (response.recommendations.isNullOrEmpty())

View file

@ -93,7 +93,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubProv
} }
private suspend fun initLogin(username: String, password: String): Boolean { 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( val response = app.post(
url = "$host/login", url = "$host/login",
headers = mapOf( headers = mapOf(
@ -105,8 +105,8 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubProv
"password" to password "password" to password
) )
) )
Log.i(TAG, "Responsecode = ${response.code}") //Log.i(TAG, "Responsecode = ${response.code}")
Log.i(TAG, "Result => ${response.text}") //Log.i(TAG, "Result => ${response.text}")
if (response.isSuccessful) { if (response.isSuccessful) {
AppUtils.tryParseJson<OAuthToken>(response.text)?.let { token -> AppUtils.tryParseJson<OAuthToken>(response.text)?.let { token ->

View file

@ -68,6 +68,8 @@ abstract class AbstractPlayerFragment(
var subStyle: SaveCaptionStyle? = null var subStyle: SaveCaptionStyle? = null
var subView: SubtitleView? = null var subView: SubtitleView? = null
var isBuffering = true var isBuffering = true
protected open var hasPipModeSupport = true
@LayoutRes @LayoutRes
protected var layout: Int = R.layout.fragment_player 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isInPIPMode) {
activity?.let { act -> activity?.let { act ->
PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow) PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow)
@ -213,7 +215,13 @@ abstract class AbstractPlayerFragment(
throw NotImplementedError() 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 val ctx = context ?: return
when (exception) { when (exception) {
is PlaybackException -> { 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) { private fun onSubStyleChanged(style: SaveCaptionStyle) {
if (player is CS3IPlayer) { if (player is CS3IPlayer) {
player.updateSubtitleStyle(style) player.updateSubtitleStyle(style)

View file

@ -1,11 +1,17 @@
package com.lagradost.cloudstream3.ui.player package com.lagradost.cloudstream3.ui.player
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.util.SparseArray
import android.widget.FrameLayout 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.*
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
@ -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.LeastRecentlyUsedCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache import com.google.android.exoplayer2.upstream.cache.SimpleCache
import com.google.android.exoplayer2.util.MimeTypes import com.google.android.exoplayer2.util.MimeTypes
import com.google.android.exoplayer2.video.VideoSize
import com.lagradost.cloudstream3.APIHolder.getApiFromName import com.lagradost.cloudstream3.APIHolder.getApiFromName
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.app 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.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.ExtractorUri
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import java.io.File import java.io.File
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
@ -153,7 +161,8 @@ class CS3IPlayer : IPlayer {
data: ExtractorUri?, data: ExtractorUri?,
startPosition: Long?, startPosition: Long?,
subtitles: Set<SubtitleData>, subtitles: Set<SubtitleData>,
subtitle: SubtitleData? subtitle: SubtitleData?,
autoPlay: Boolean?
) { ) {
Log.i(TAG, "loadPlayer") Log.i(TAG, "loadPlayer")
if (sameEpisode) { if (sameEpisode) {
@ -168,7 +177,7 @@ class CS3IPlayer : IPlayer {
} }
// we want autoplay because of TV and UX // we want autoplay because of TV and UX
isPlaying = true isPlaying = autoPlay ?: isPlaying
// release the current exoplayer and cache // release the current exoplayer and cache
releasePlayer() releasePlayer()
@ -322,6 +331,7 @@ class CS3IPlayer : IPlayer {
} }
companion object { companion object {
private var ytVideos: MutableMap<String, YtFile> = mutableMapOf()
private var simpleCache: SimpleCache? = null private var simpleCache: SimpleCache? = null
var requestSubtitleUpdate: (() -> Unit)? = null var requestSubtitleUpdate: (() -> Unit)? = null
@ -686,6 +696,14 @@ class CS3IPlayer : IPlayer {
isPlaying = exo.isPlaying isPlaying = exo.isPlaying
} }
when (playbackState) {
Player.STATE_READY -> {
onRenderFirst()
}
else -> {}
}
if (playWhenReady) { if (playWhenReady) {
when (playbackState) { when (playbackState) {
Player.STATE_READY -> { Player.STATE_READY -> {
@ -715,9 +733,50 @@ class CS3IPlayer : IPlayer {
// super.onCues(cues.map { cue -> cue.buildUpon().setText("Hello world").setSize(Cue.DIMEN_UNSET).build() }) // 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() { override fun onRenderedFirstFrame() {
updatedTime() updatedTime()
super.onRenderedFirstFrame()
onRenderFirst()
}
})
} catch (e: Exception) {
Log.e(TAG, "loadExo error", e)
playerError?.invoke(e)
}
}
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")
@ -725,6 +784,7 @@ class CS3IPlayer : IPlayer {
// 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
} ?: false } ?: false
if (invalid) { if (invalid) {
releasePlayer(saveTime = false) releasePlayer(saveTime = false)
playerError?.invoke(InvalidFileException("Too short playback")) playerError?.invoke(InvalidFileException("Too short playback"))
@ -753,13 +813,6 @@ class CS3IPlayer : IPlayer {
} }
} }
} }
super.onRenderedFirstFrame()
}
})
} catch (e: Exception) {
Log.e(TAG, "loadExo error", e)
playerError?.invoke(e)
}
} }
private fun loadOfflinePlayer(context: Context, data: ExtractorUri) { private fun loadOfflinePlayer(context: Context, data: ExtractorUri) {
@ -829,9 +882,55 @@ class CS3IPlayer : IPlayer {
return Pair(subSources, activeSubtitles) 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) { private fun loadOnlinePlayer(context: Context, link: ExtractorLink) {
Log.i(TAG, "loadOnlinePlayer") Log.i(TAG, "loadOnlinePlayer $link")
try { 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<YtFile>?,
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 currentLink = link
if (ignoreSSL) { if (ignoreSSL) {

View file

@ -51,6 +51,21 @@ import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
import com.lagradost.cloudstream3.utils.Vector2 import com.lagradost.cloudstream3.utils.Vector2
import kotlinx.android.synthetic.main.player_custom_layout.* 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.* import kotlin.math.*
const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking 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 // All the UI Logic for the player
open class FullScreenPlayer : AbstractPlayerFragment() { open class FullScreenPlayer : AbstractPlayerFragment() {
protected open var lockRotation = true
protected open var isFullScreenPlayer = true
// state of player UI // state of player UI
protected var isShowing = false protected var isShowing = false
protected var isLocked = false protected var isLocked = false
@ -100,11 +118,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
// screenWidth and screenHeight does always // screenWidth and screenHeight does always
// refer to the screen while in landscape mode // refer to the screen while in landscape mode
private val screenWidth: Int protected val screenWidth: Int
get() { get() {
return max(displayMetrics.widthPixels, displayMetrics.heightPixels) return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
} }
private val screenHeight: Int protected val screenHeight: Int
get() { get() {
return min(displayMetrics.widthPixels, displayMetrics.heightPixels) return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
} }
@ -159,7 +177,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
animateLayoutChanges() animateLayoutChanges()
} }
private fun animateLayoutChanges() { protected fun animateLayoutChanges() {
if (isShowing) { if (isShowing) {
updateUIVisibility() updateUIVisibility()
} else { } else {
@ -234,19 +252,24 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
override fun onResume() { override fun onResume() {
if (isFullScreenPlayer) {
activity?.hideSystemUI() activity?.hideSystemUI()
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && fullscreenNotch) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && fullscreenNotch) {
val params = activity?.window?.attributes val params = activity?.window?.attributes
params?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES params?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
activity?.window?.attributes = params activity?.window?.attributes = params
} }
}
if (lockRotation)
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
super.onResume() super.onResume()
} }
override fun onDestroy() { override fun onDestroy() {
activity?.showSystemUI() activity?.showSystemUI()
if (lockRotation)
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
// simply resets brightness and notch settings that might have been overridden // simply resets brightness and notch settings that might have been overridden
@ -336,6 +359,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
dialog.setOnDismissListener { dialog.setOnDismissListener {
if (isFullScreenPlayer)
activity?.hideSystemUI() activity?.hideSystemUI()
} }
applyButton.setOnClickListener { applyButton.setOnClickListener {
@ -374,8 +398,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
act.getString(R.string.player_speed), act.getString(R.string.player_speed),
false, false,
{ {
if (isFullScreenPlayer)
activity?.hideSystemUI() activity?.hideSystemUI()
}) { index -> }) { index ->
if (isFullScreenPlayer)
activity?.hideSystemUI() activity?.hideSystemUI()
setPlayBackSpeed(speedsNumbers[index]) setPlayBackSpeed(speedsNumbers[index])
} }
@ -455,8 +481,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
private fun onClickChange() { private fun onClickChange() {
isShowing = !isShowing isShowing = !isShowing
if (isShowing) { if (isShowing) {
player_intro_play?.isGone = true
autoHide() autoHide()
} }
if (isFullScreenPlayer)
activity?.hideSystemUI() activity?.hideSystemUI()
animateLayoutChanges() animateLayoutChanges()
player_pause_play?.requestFocus() player_pause_play?.requestFocus()
@ -692,7 +720,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
if (event == null || view == null) return false if (event == null || view == null) return false
val currentTouch = Vector2(event.x, event.y) val currentTouch = Vector2(event.x, event.y)
val startTouch = currentTouchStart val startTouch = currentTouchStart
player_intro_play?.isGone = true
when (event.action) { when (event.action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
// validates if the touch is inside of the player area // validates if the touch is inside of the player area
@ -717,7 +745,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
} }
MotionEvent.ACTION_UP -> { MotionEvent.ACTION_UP -> {
if (isCurrentTouchValid && !isLocked) { if (isCurrentTouchValid && !isLocked && isFullScreenPlayer) {
// seek time // seek time
if (swipeHorizontalEnabled && currentTouchAction == TouchAction.Time) { if (swipeHorizontalEnabled && currentTouchAction == TouchAction.Time) {
val startTime = currentTouchStartPlayerTime 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 if (doubleTapPauseEnabled) { // you can pause if your tap is in the middle of the screen
when { when {
currentTouch.x < screenWidth / 2 - (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> { currentTouch.x < screenWidth / 2 - (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> {
if (doubleTapEnabled) if (doubleTapEnabled && isFullScreenPlayer)
rewind() rewind()
} }
currentTouch.x > screenWidth / 2 + (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> { currentTouch.x > screenWidth / 2 + (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> {
if (doubleTapEnabled) if (doubleTapEnabled && isFullScreenPlayer)
fastForward() fastForward()
} }
else -> { else -> {
player.handleEvent(CSPlayerEvent.PlayPauseToggle) player.handleEvent(CSPlayerEvent.PlayPauseToggle)
} }
} }
} else if (doubleTapEnabled) { } else if (doubleTapEnabled && isFullScreenPlayer) {
if (currentTouch.x < screenWidth / 2) { if (currentTouch.x < screenWidth / 2) {
rewind() rewind()
} else { } else {
@ -798,7 +826,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
// if current touch is valid // if current touch is valid
if (startTouch != null && isCurrentTouchValid && !isLocked) { if (startTouch != null && isCurrentTouchValid && !isLocked && isFullScreenPlayer) {
// action is unassigned and can therefore be assigned // action is unassigned and can therefore be assigned
if (currentTouchAction == null) { if (currentTouchAction == null) {
val diffFromStart = startTouch - currentTouch val diffFromStart = startTouch - currentTouch
@ -1201,6 +1229,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
showMirrorsDialogue() 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 // 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 -> player_holder?.setOnTouchListener { callView, event ->
return@setOnTouchListener handleMotionEvent(callView, event) return@setOnTouchListener handleMotionEvent(callView, event)

View file

@ -98,6 +98,7 @@ interface IPlayer {
startPosition: Long? = null, startPosition: Long? = null,
subtitles : Set<SubtitleData>, subtitles : Set<SubtitleData>,
subtitle : SubtitleData?, subtitle : SubtitleData?,
autoPlay : Boolean? = true
) )
fun reloadPlayer(context: Context) fun reloadPlayer(context: Context)

View file

@ -28,7 +28,6 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager 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.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.UIHelper.setImage 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.getFileName
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import kotlinx.android.synthetic.main.fragment_result.* import kotlinx.android.synthetic.main.fragment_result.*
@ -183,7 +181,7 @@ fun ResultEpisode.getWatchProgress(): Float {
return (getDisplayPosition() / 1000).toFloat() / (duration / 1000).toFloat() return (getDisplayPosition() / 1000).toFloat() / (duration / 1000).toFloat()
} }
class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegionsListener { class ResultFragment : ResultTrailerPlayer() {
companion object { companion object {
const val URL_BUNDLE = "url" const val URL_BUNDLE = "url"
const val API_NAME_BUNDLE = "apiName" 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)) setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f))
} }
var currentTrailers: List<String> = 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<String>?) {
if(context?.isTvSettings() == true) return
currentTrailers = trailers ?: emptyList()
loadTrailer()
}
private fun setActors(actors: List<ActorData>?) { private fun setActors(actors: List<ActorData>?) {
if (actors.isNullOrEmpty()) { if (actors.isNullOrEmpty()) {
result_cast_text?.isVisible = false result_cast_text?.isVisible = false
@ -777,7 +822,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} else if (dy < -5) { } else if (dy < -5) {
result_bookmark_fab?.extend() result_bookmark_fab?.extend()
} }
result_poster_blur_holder?.translationY = -scrollY.toFloat() //result_poster_blur_holder?.translationY = -scrollY.toFloat()
}) })
result_back.setOnClickListener { result_back.setOnClickListener {
@ -1727,6 +1772,8 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
setRecommendations(d.recommendations, null) setRecommendations(d.recommendations, null)
setActors(d.actors) setActors(d.actors)
setTrailers(d.trailers)
if (syncModel.addSyncs(d.syncData)) { if (syncModel.addSyncs(d.syncData)) {
syncModel.updateMetaAndUser() syncModel.updateMetaAndUser()
syncModel.updateSynced() syncModel.updateSynced()
@ -1739,7 +1786,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
val posterImageLink = d.posterUrl val posterImageLink = d.posterUrl
if (!posterImageLink.isNullOrEmpty()) { if (!posterImageLink.isNullOrEmpty()) {
result_poster?.setImage(posterImageLink, d.posterHeaders) 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 //Full screen view of Poster image
if (context?.isTrueTvSettings() == false) // Poster not clickable on tv if (context?.isTrueTvSettings() == false) // Poster not clickable on tv
result_poster_holder?.setOnClickListener { result_poster_holder?.setOnClickListener {
@ -1768,7 +1815,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} else { } else {
result_poster?.setImageResource(R.drawable.default_cover) 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 result_poster_holder?.visibility = VISIBLE

View file

@ -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<Int, Int>? = null
override fun nextEpisode() {}
override fun prevEpisode() {}
override fun playerPositionChanged(posDur: Pair<Long, Long>) {}
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<Int, Int>) {
playerWidthHeight = widthHeight
fixPlayerSize()
}
override fun subtitlesChanged() {}
override fun embeddedSubtitlesFetched(subtitles: List<SubtitleData>) {}
override fun exitedPipMode() {}
override fun onGestureRegionsUpdate(gestureRegions: List<Rect>) {}
}

View file

@ -117,38 +117,13 @@
android:textColor="?attr/textColor" /> android:textColor="?attr/textColor" />
</LinearLayout> </LinearLayout>
<FrameLayout <LinearLayout
android:id="@+id/result_finish_loading" android:id="@+id/result_finish_loading"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible"
android:orientation="vertical">
<FrameLayout
android:id="@+id/result_poster_blur_holder"
android:layout_width="match_parent"
android:layout_height="230dp"
android:visibility="visible">
<ImageView
android:id="@+id/result_poster_blur"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0"
android:background="?attr/primaryGrayBackground"
android:scaleType="centerCrop"
tools:ignore="ContentDescription"
tools:src="@drawable/example_poster" />
<ImageView
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_gravity="bottom"
android:src="@drawable/background_shadow"
tools:ignore="ContentDescription" />
</FrameLayout>
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:id="@+id/result_scroll" android:id="@+id/result_scroll"
@ -163,6 +138,8 @@
android:background="?attr/primaryBlackBackground" android:background="?attr/primaryBlackBackground"
android:orientation="vertical"> android:orientation="vertical">
<include layout="@layout/fragment_trailer" />
<!-- <!--
<FrameLayout <FrameLayout
android:background="?attr/primaryGrayBackground" android:background="?attr/primaryGrayBackground"
@ -399,6 +376,7 @@
tools:text="Bookmark" tools:text="Bookmark"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView <TextView
android:id="@+id/result_cast_text" android:id="@+id/result_cast_text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -855,7 +833,7 @@
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</FrameLayout> </LinearLayout>
<!-- <!--

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="visible"
android:orientation="horizontal"
android:keepScreenOn="true"
android:id="@+id/player_background"
app:backgroundTint="@android:color/black"
android:background="@android:color/black"
android:screenOrientation="sensorLandscape"
app:surface_type="texture_view">
<!--
app:fastforward_increment="10000"
app:rewind_increment="10000"-->
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
app:show_timeout="0"
app:hide_on_touch="false"
app:auto_show="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:backgroundTint="@android:color/black"
android:background="@android:color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:controller_layout_id="@layout/trailer_custom_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,330 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/player_holder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:screenOrientation="landscape"
tools:orientation="vertical">
<FrameLayout
android:id="@+id/player_intro_play"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/player_gradient_tv" />
<TextView
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@android:color/white"
android:text="@string/trailer"
android:padding="10dp"
android:layout_gravity="start|bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:layout_gravity="center"
android:src="@drawable/play_button"
android:layout_width="60dp"
android:layout_height="60dp" />
</FrameLayout>
<FrameLayout
android:id="@+id/subtitle_holder"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/shadow_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_overlay" />
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/player_video_holder"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--use for thinner app:trackThickness="3dp" com.google.android.material.progressindicator.CircularProgressIndicator-->
<ProgressBar
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:focusable="false"
android:clickable="false"
android:focusableInTouchMode="false"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible"
android:id="@+id/player_buffering"
android:layout_gravity="center"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
<!-- This nested layout is necessary because of buffering and clicking-->
<FrameLayout
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="100dp"
android:layout_height="100dp"
android:id="@+id/player_pause_play_holder_holder">
<FrameLayout
tools:ignore="uselessParent"
android:id="@+id/player_pause_play_holder"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
app:tint="@color/white"
android:id="@+id/player_pause_play"
android:nextFocusLeft="@id/exo_rew"
android:nextFocusRight="@id/exo_ffwd"
android:nextFocusUp="@id/player_go_back"
android:nextFocusDown="@id/player_lock"
android:layout_gravity="center"
android:src="@drawable/netflix_pause"
android:background="@drawable/video_tap_button"
android:layout_width="70dp"
android:layout_height="70dp"
tools:ignore="ContentDescription" />
</FrameLayout>
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/player_center_menu"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<FrameLayout
android:id="@+id/player_rew_holder"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintWidth_percent="0.5"
android:layout_gravity="center_vertical|start"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/player_ffwd_holder"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/exo_rew_text"
android:layout_width="200dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:gravity="center"
android:textColor="@color/white"
android:textSize="19sp"
android:textStyle="bold"
tools:text="10" />
<ImageButton
android:id="@id/exo_rew"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_gravity="center"
android:background="@drawable/video_tap_button_skip"
android:nextFocusLeft="@id/exo_rew"
android:nextFocusUp="@id/player_go_back"
android:nextFocusDown="@id/player_lock"
android:padding="10dp"
android:scaleType="fitCenter"
android:scaleX="-1"
android:src="@drawable/netflix_skip_forward"
app:tint="@color/white"
android:tintMode="src_in"
tools:ignore="ContentDescription" />
</FrameLayout>
<FrameLayout
android:id="@+id/player_ffwd_holder"
android:layout_width="0dp"
app:layout_constraintWidth_percent="0.5"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/player_rew_holder"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/exo_ffwd_text"
android:layout_width="200dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:gravity="center"
android:textColor="@color/white"
android:textSize="19sp"
android:textStyle="bold"
tools:text="10" />
<ImageButton
android:id="@id/exo_ffwd"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_gravity="center"
android:background="@drawable/video_tap_button_skip"
android:nextFocusRight="@id/exo_rew"
android:nextFocusUp="@id/player_go_back"
android:nextFocusDown="@id/player_lock"
android:padding="10dp"
android:scaleType="fitCenter"
android:src="@drawable/netflix_skip_forward"
app:tint="@color/white"
android:tintMode="src_in"
tools:ignore="ContentDescription" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginBottom="20dp"
android:gravity="center"
android:orientation="horizontal"
android:paddingTop="4dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<ImageButton
android:id="@id/exo_prev"
style="@style/ExoMediaButton.Previous"
app:tint="?attr/colorPrimaryDark"
android:tintMode="src_in"
tools:ignore="ContentDescription" />
<ImageButton
android:id="@id/exo_repeat_toggle"
style="@style/ExoMediaButton"
app:tint="?attr/colorPrimaryDark"
android:tintMode="src_in"
tools:ignore="ContentDescription" />
<ImageButton
android:id="@id/exo_next"
style="@style/ExoMediaButton.Next"
app:tint="?attr/colorPrimaryDark"
android:tintMode="src_in"
tools:ignore="ContentDescription" />
<ImageButton
android:id="@id/exo_vr"
style="@style/ExoMediaButton.VR"
app:tint="?attr/colorPrimaryDark"
android:tintMode="src_in"
tools:ignore="ContentDescription" />
<ImageButton
android:id="@id/exo_play"
app:tint="?attr/colorPrimaryDark"
android:tintMode="src_in"
tools:ignore="ContentDescription"
android:layout_height="0dp"
android:layout_width="0dp" />
<ImageButton
android:id="@id/exo_pause"
app:tint="?attr/colorPrimaryDark"
android:tintMode="src_in"
tools:ignore="ContentDescription"
android:layout_height="0dp"
android:layout_width="0dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/bottom_player_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center_vertical"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:id="@+id/player_video_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:gravity="end"
android:includeFontPadding="false"
android:minWidth="50dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:textColor="@android:color/white"
android:textSize="14sp"
android:textStyle="normal"
tools:text="15:30" />
<!--app:buffered_color="@color/videoCache"-->
<com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@id/exo_progress"
android:layout_width="0dp"
android:layout_height="30dp"
android:layout_weight="1"
app:bar_height="2dp"
app:played_color="?attr/colorPrimary"
app:scrubber_color="?attr/colorPrimary"
app:scrubber_dragged_size="26dp"
app:scrubber_enabled_size="24dp"
app:unplayed_color="@color/videoProgress" />
<!-- exo_duration-->
<TextView
android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="20dp"
android:includeFontPadding="false"
android:minWidth="50dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:textColor="@android:color/white"
android:textSize="14sp"
android:textStyle="normal"
tools:text="23:20" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View file

@ -530,4 +530,5 @@
<string name="subtitles_remove_captions">Remove closed captions from subtitles</string> <string name="subtitles_remove_captions">Remove closed captions from subtitles</string>
<string name="subtitles_remove_bloat">Remove bloat from subtitles</string> <string name="subtitles_remove_bloat">Remove bloat from subtitles</string>
<string name="extras">Extras</string> <string name="extras">Extras</string>
<string name="trailer">Trailer</string>
</resources> </resources>