mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 22:30:30 +05:30
commit
a0ad5a1837
@ -0,0 +1,9 @@
|
||||
package com.github.libretube.obj
|
||||
|
||||
data class PreviewFrame(
|
||||
val previewUrl: String,
|
||||
val positionX: Int,
|
||||
val positionY: Int,
|
||||
val framesPerPageX: Int,
|
||||
val framesPerPageY: Int
|
||||
)
|
@ -86,6 +86,7 @@ import com.github.libretube.util.NowPlayingNotification
|
||||
import com.github.libretube.util.PlayerHelper
|
||||
import com.github.libretube.util.PlayingQueue
|
||||
import com.github.libretube.util.PreferenceHelper
|
||||
import com.github.libretube.util.SeekbarPreviewListener
|
||||
import com.github.libretube.util.TextUtils
|
||||
import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.DefaultLoadControl
|
||||
@ -645,7 +646,8 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
||||
// set media sources for the player
|
||||
setResolutionAndSubtitles()
|
||||
prepareExoPlayerView()
|
||||
initializePlayerView(streams)
|
||||
initializePlayerView()
|
||||
setupSeekbarPreview()
|
||||
if (!isLive) seekToWatchPosition()
|
||||
exoPlayer.prepare()
|
||||
if (!PreferenceHelper.getBoolean(PreferenceKeys.DATA_SAVER_MODE, false)) exoPlayer.play()
|
||||
@ -798,7 +800,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun initializePlayerView(response: Streams) {
|
||||
private fun initializePlayerView() {
|
||||
// initialize the player view actions
|
||||
binding.player.initialize(
|
||||
this,
|
||||
@ -809,36 +811,36 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
||||
|
||||
binding.apply {
|
||||
playerViewsInfo.text =
|
||||
context?.getString(R.string.views, response.views.formatShort()) +
|
||||
if (!isLive) TextUtils.SEPARATOR + response.uploadDate else ""
|
||||
context?.getString(R.string.views, streams.views.formatShort()) +
|
||||
if (!isLive) TextUtils.SEPARATOR + streams.uploadDate else ""
|
||||
|
||||
textLike.text = response.likes.formatShort()
|
||||
textDislike.text = response.dislikes.formatShort()
|
||||
ImageHelper.loadImage(response.uploaderAvatar, binding.playerChannelImage)
|
||||
playerChannelName.text = response.uploader
|
||||
textLike.text = streams.likes.formatShort()
|
||||
textDislike.text = streams.dislikes.formatShort()
|
||||
ImageHelper.loadImage(streams.uploaderAvatar, binding.playerChannelImage)
|
||||
playerChannelName.text = streams.uploader
|
||||
|
||||
titleTextView.text = response.title
|
||||
titleTextView.text = streams.title
|
||||
|
||||
playerTitle.text = response.title
|
||||
playerDescription.text = response.description
|
||||
playerTitle.text = streams.title
|
||||
playerDescription.text = streams.description
|
||||
|
||||
playerChannelSubCount.text = context?.getString(
|
||||
R.string.subscribers,
|
||||
response.uploaderSubscriberCount?.formatShort()
|
||||
streams.uploaderSubscriberCount?.formatShort()
|
||||
)
|
||||
}
|
||||
|
||||
// duration that's not greater than 0 indicates that the video is live
|
||||
if (response.duration!! <= 0) {
|
||||
if (streams.duration!! <= 0) {
|
||||
isLive = true
|
||||
handleLiveVideo()
|
||||
}
|
||||
|
||||
playerBinding.exoTitle.text = response.title
|
||||
playerBinding.exoTitle.text = streams.title
|
||||
|
||||
// init the chapters recyclerview
|
||||
if (response.chapters != null) {
|
||||
chapters = response.chapters
|
||||
if (streams.chapters != null) {
|
||||
chapters = streams.chapters.orEmpty()
|
||||
initializeChapters()
|
||||
}
|
||||
|
||||
@ -922,7 +924,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
||||
})
|
||||
|
||||
binding.relPlayerDownload.setOnClickListener {
|
||||
if (response.duration <= 0) {
|
||||
if (streams.duration!! <= 0) {
|
||||
Toast.makeText(context, R.string.cannotDownload, Toast.LENGTH_SHORT).show()
|
||||
} else if (!DownloadService.IS_DOWNLOAD_RUNNING) {
|
||||
val newFragment = DownloadDialog(videoId!!)
|
||||
@ -933,7 +935,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
||||
}
|
||||
}
|
||||
|
||||
if (response.hls != null) {
|
||||
if (streams.hls != null) {
|
||||
binding.relPlayerPip.setOnClickListener {
|
||||
if (SDK_INT < Build.VERSION_CODES.O) return@setOnClickListener
|
||||
try {
|
||||
@ -943,9 +945,9 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
||||
}
|
||||
}
|
||||
}
|
||||
initializeRelatedVideos(response.relatedStreams)
|
||||
initializeRelatedVideos(streams.relatedStreams)
|
||||
// set video description
|
||||
val description = response.description!!
|
||||
val description = streams.description!!
|
||||
|
||||
// detect whether the description is html formatted
|
||||
if (description.contains("<") && description.contains(">")) {
|
||||
@ -958,7 +960,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
||||
|
||||
binding.playerChannel.setOnClickListener {
|
||||
val activity = view?.context as MainActivity
|
||||
val bundle = bundleOf(IntentData.channelId to response.uploaderUrl)
|
||||
val bundle = bundleOf(IntentData.channelId to streams.uploaderUrl)
|
||||
activity.navController.navigate(R.id.channelFragment, bundle)
|
||||
activity.binding.mainMotionLayout.transitionToEnd()
|
||||
binding.playerMotionLayout.transitionToEnd()
|
||||
@ -966,8 +968,8 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
||||
|
||||
// update the subscribed state
|
||||
binding.playerSubscribe.setupSubscriptionButton(
|
||||
streams.uploaderUrl?.toID(),
|
||||
streams.uploader
|
||||
this.streams.uploaderUrl?.toID(),
|
||||
this.streams.uploader
|
||||
)
|
||||
|
||||
binding.relPlayerSave.setOnClickListener {
|
||||
@ -1446,6 +1448,17 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
||||
}
|
||||
.build()
|
||||
|
||||
private fun setupSeekbarPreview() {
|
||||
playerBinding.seekbarPreview.visibility = View.GONE
|
||||
playerBinding.exoProgress.addListener(
|
||||
SeekbarPreviewListener(
|
||||
streams.previewFrames.orEmpty(),
|
||||
playerBinding.seekbarPreview,
|
||||
streams.duration!! * 1000
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun shouldStartPiP(): Boolean {
|
||||
if (!PlayerHelper.pipEnabled || SDK_INT >= Build.VERSION_CODES.S) {
|
||||
return false
|
||||
|
@ -0,0 +1,110 @@
|
||||
package com.github.libretube.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.ImageView
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import coil.request.ImageRequest
|
||||
import com.github.libretube.api.obj.PreviewFrames
|
||||
import com.github.libretube.obj.PreviewFrame
|
||||
import com.google.android.exoplayer2.ui.TimeBar
|
||||
|
||||
class SeekbarPreviewListener(
|
||||
private val previewFrames: List<PreviewFrames>,
|
||||
private val previewIv: ImageView,
|
||||
private val duration: Long
|
||||
) : TimeBar.OnScrubListener {
|
||||
private var moving = false
|
||||
|
||||
override fun onScrubStart(timeBar: TimeBar, position: Long) {}
|
||||
|
||||
/**
|
||||
* Show a preview of the scrubber position
|
||||
*/
|
||||
override fun onScrubMove(timeBar: TimeBar, position: Long) {
|
||||
moving = true
|
||||
val preview = getPreviewFrame(position) ?: return
|
||||
updatePreviewX(position)
|
||||
|
||||
val request = ImageRequest.Builder(previewIv.context)
|
||||
.data(preview.previewUrl)
|
||||
.target {
|
||||
if (!moving) return@target
|
||||
val bitmap = it.toBitmap()
|
||||
val heightPerFrame = bitmap.height / preview.framesPerPageY
|
||||
val widthPerFrame = bitmap.width / preview.framesPerPageX
|
||||
val frame = Bitmap.createBitmap(
|
||||
bitmap,
|
||||
preview.positionX * widthPerFrame,
|
||||
preview.positionY * heightPerFrame,
|
||||
widthPerFrame,
|
||||
heightPerFrame
|
||||
)
|
||||
previewIv.setImageBitmap(frame)
|
||||
previewIv.visibility = View.VISIBLE
|
||||
}
|
||||
.build()
|
||||
|
||||
ImageHelper.imageLoader.enqueue(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the seekbar preview with a short delay
|
||||
*/
|
||||
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
|
||||
moving = false
|
||||
previewIv.animate()
|
||||
.alpha(0f)
|
||||
.translationYBy(30f)
|
||||
.setDuration(200)
|
||||
.withEndAction {
|
||||
previewIv.visibility = View.GONE
|
||||
previewIv.translationY = previewIv.translationY - 30f
|
||||
previewIv.alpha = 1f
|
||||
}
|
||||
.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preview frame according to the current position
|
||||
*/
|
||||
private fun getPreviewFrame(position: Long): PreviewFrame? {
|
||||
var startPosition: Long = 0
|
||||
val frames = previewFrames.sortedBy { it.frameHeight }.lastOrNull()
|
||||
frames?.urls?.forEach { url ->
|
||||
for (y in 0 until frames.framesPerPageY!!) {
|
||||
for (x in 0 until frames.framesPerPageX!!) {
|
||||
val endPosition = startPosition + frames.durationPerFrame!!.toLong()
|
||||
if (position in startPosition until endPosition) {
|
||||
return PreviewFrame(url, x, y, frames.framesPerPageX, frames.framesPerPageY)
|
||||
}
|
||||
startPosition = endPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun updatePreviewX(position: Long) {
|
||||
val params = previewIv.layoutParams as MarginLayoutParams
|
||||
val parentWidth = (previewIv.parent as View).width
|
||||
// calculate the center-offset of the preview image view
|
||||
val offset = parentWidth * (position.toFloat() / duration.toFloat()) - previewIv.width / 2
|
||||
// normalize the offset to keep a minimum distance at left and right
|
||||
params.marginStart = normalizeOffset(
|
||||
offset.toInt(),
|
||||
MIN_PADDING,
|
||||
parentWidth - MIN_PADDING - previewIv.width
|
||||
)
|
||||
previewIv.layoutParams = params
|
||||
}
|
||||
|
||||
private fun normalizeOffset(offset: Int, min: Int, max: Int): Int {
|
||||
return maxOf(min, minOf(max, offset))
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MIN_PADDING = 20
|
||||
}
|
||||
}
|
@ -95,6 +95,96 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@id/exo_center_controls"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="@android:color/transparent"
|
||||
android:gravity="center"
|
||||
android:padding="20dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/skip_prev"
|
||||
style="@style/PlayerControlCenter"
|
||||
android:background="?android:selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_prev"
|
||||
android:visibility="invisible"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/rewindBTN"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:scaleX="0.8"
|
||||
android:scaleY="0.8"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_rewind"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rewindTV"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/playPauseBTN"
|
||||
style="@style/ExoStyledControls.Button.Center.PlayPause"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:background="?android:selectableItemBackgroundBorderless"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/forwardBTN"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:scaleX="0.8"
|
||||
android:scaleY="0.8"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_forward"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/forwardTV"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/skip_next"
|
||||
style="@style/PlayerControlCenter"
|
||||
android:background="?android:selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_next"
|
||||
android:visibility="invisible"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@id/exo_bottom_bar"
|
||||
android:layout_width="match_parent"
|
||||
@ -103,6 +193,17 @@
|
||||
android:layout_marginTop="10dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/seekbar_preview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="70dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:translationY="27dp"
|
||||
android:translationZ="1dp"
|
||||
android:visibility="gone"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.Corner.Small" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -231,94 +332,4 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@id/exo_center_controls"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="@android:color/transparent"
|
||||
android:gravity="center"
|
||||
android:padding="20dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/skip_prev"
|
||||
style="@style/PlayerControlCenter"
|
||||
android:background="?android:selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_prev"
|
||||
android:visibility="invisible"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/rewindBTN"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:scaleX="0.8"
|
||||
android:scaleY="0.8"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_rewind"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rewindTV"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/playPauseBTN"
|
||||
style="@style/ExoStyledControls.Button.Center.PlayPause"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:background="?android:selectableItemBackgroundBorderless"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/forwardBTN"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:scaleX="0.8"
|
||||
android:scaleY="0.8"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_forward"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/forwardTV"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/skip_next"
|
||||
style="@style/PlayerControlCenter"
|
||||
android:background="?android:selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_next"
|
||||
android:visibility="invisible"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</merge>
|
Loading…
Reference in New Issue
Block a user