Biscuit
Biscuit

Reputation: 5247

Exoplayer seekbar preview

I'm trying to add a preview to my seekbar on my exoplayer just like in youtube or plex (see the image below)

enter image description here

I've found this library but it isn't up-to-date yet.

I already have the image per frame but I don't know how to integrate them in my Exoplayer, I'm looking for either a tutorial or explanation where I should begin because I'm kind of lost there.

I've found Timebar.onScrubListener while browsing the exoplayer doc. I'm guessing I'll be using these 3 listeners to fetch the position of the scrub and display the corresponding image.

Upvotes: 5

Views: 7789

Answers (1)

Biscuit
Biscuit

Reputation: 5247

UPDATE: The library is up-to-date as of May 2020 so you can use it directly.

I'll leave code below for those who don't want to use the library.


After searching and adapting it to my needs I found a way by looking at how previewSeekBar was doing and I ended up using the same thing so here it is:

My sprite is composed of 10 columns and 6 rows, each square represent 1 second

GlideTransformation

private const val MAX_LINES = 6
private const val MAX_COLUMNS = 10
private const val THUMBNAILS_EACH = 1000 // milliseconds
private const val ONE_MINUTE = 60000 // one minute in millisecond

class GlideThumbnailTransformation(position: Long) : BitmapTransformation() {

    private val x: Int
    private val y: Int

    init {
        // Remainder of position on one minute because we just need to know which square of the current miniature
        val square = position.rem(ONE_MINUTE).toInt() / THUMBNAILS_EACH
        y = square / MAX_COLUMNS
        x = square % MAX_COLUMNS
    }

    override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
        val width = toTransform.width / MAX_COLUMNS
        val height = toTransform.height / MAX_LINES
        return Bitmap.createBitmap(toTransform, x * width, y * height, width, height)
    }

    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
        val data: ByteArray = ByteBuffer.allocate(8).putInt(x).putInt(y).array()
        messageDigest.update(data)
    }

    override fun hashCode(): Int {
        return (x.toString() + y.toString()).hashCode()
    }

    override fun equals(other: Any?): Boolean {
        if (other !is GlideThumbnailTransformation) {
            return false
        }
        return other.x == x && other.y == y
    }
}

Activity


val thumbnailUrl = "https://bitdash-a.akamaihd.net/content/MI201109210084_1/thumbnails/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.jpg"
exo_progress.addListener(object : TimeBar.OnScrubListener {
    override fun onScrubMove(timeBar: TimeBar, position: Long) {
        previewFrameLayout.visibility = View.VISIBLE
        val targetX = updatePreviewX(position.toInt(), exoPlayer.duration.toInt())
        previewFrameLayout.x = targetX.toFloat()
        GlideApp.with(scrubbingPreview)
            .load(thumbnailUrl)
            .override(Target.SIZE_ORIGINAL,Target.SIZE_ORIGINAL)
            .transform(GlideThumbnailTransformation(position))
            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
            .into(scrubbingPreview)
    }

    override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
        previewFrameLayout.visibility = View.INVISIBLE
    }

    override fun onScrubStart(timeBar: TimeBar, position: Long) {}
})

private fun updatePreviewX(progress: Int, max: Int): Int {
    if (max == 0) { return 0 }

    val parent = previewFrameLayout.parent as ViewGroup
    val layoutParams = previewFrameLayout.layoutParams as MarginLayoutParams
    val offset = progress.toFloat() / max
    val minimumX: Int = previewFrameLayout.left
    val maximumX = (parent.width - parent.paddingRight - layoutParams.rightMargin)

// We remove the padding of the scrubbing, if you have a custom size juste use dimen to calculate this
    val previewPaddingRadius: Int = dpToPx(resources.displayMetrics, DefaultTimeBar.DEFAULT_SCRUBBER_DRAGGED_SIZE_DP).div(2)
    val previewLeftX = (exo_progress as View).left.toFloat()
    val previewRightX = (exo_progress as View).right.toFloat()
    val previewSeekBarStartX: Float = previewLeftX + previewPaddingRadius
    val previewSeekBarEndX: Float = previewRightX - previewPaddingRadius
    val currentX = (previewSeekBarStartX + (previewSeekBarEndX - previewSeekBarStartX) * offset)
    val startX: Float = currentX - previewFrameLayout.width / 2f
    val endX: Float = startX + previewFrameLayout.width

    // Clamp the moves
    return if (startX >= minimumX && endX <= maximumX) {
        startX.toInt()
    } else if (startX < minimumX) {
        minimumX
    } else {
        maximumX - previewFrameLayout.width
    }
}

private fun dpToPx(displayMetrics: DisplayMetrics, dps: Int): Int {
    return (dps * displayMetrics.density).toInt()
}

XML

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_gravity="bottom"
    android:layoutDirection="ltr"
    android:background="#CC000000"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:paddingTop="4dp"
        android:orientation="horizontal"
        android:id="@+id/controlsLayout"
        app:layout_constraintBottom_toBottomOf="parent">

        <ImageButton android:id="@id/exo_prev"
            style="@style/ExoMediaButton.Previous"/>

        <ImageButton android:id="@id/exo_rew"
            style="@style/ExoMediaButton.Rewind"/>

        <ImageButton android:id="@id/exo_repeat_toggle"
            style="@style/ExoMediaButton"/>

        <ImageButton android:id="@id/exo_play"
            style="@style/ExoMediaButton.Play"/>

        <ImageButton android:id="@id/exo_pause"
            style="@style/ExoMediaButton.Pause"/>

        <ImageButton android:id="@id/exo_ffwd"
            style="@style/ExoMediaButton.FastForward"/>

        <ImageButton android:id="@id/exo_next"
            style="@style/ExoMediaButton.Next"/>

    </LinearLayout>

    <TextView android:id="@id/exo_position"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textStyle="bold"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:includeFontPadding="false"
        android:textColor="#FFBEBEBE"
        app:layout_constraintBottom_toTopOf="@id/controlsLayout"
        app:layout_constraintStart_toStartOf="parent"/>

    <FrameLayout
        android:id="@+id/previewFrameLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:background="@drawable/video_frame"
        android:padding="2dp"
        android:visibility="invisible"
        app:layout_constraintBottom_toTopOf="@+id/exo_progress"
        app:layout_constraintDimensionRatio="16:9"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintWidth_default="percent"
        app:layout_constraintWidth_percent="0.25"
        tools:visibility="visible">

        <ImageView
            android:id="@+id/scrubbingPreview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="fitXY" />

    </FrameLayout>

    <com.google.android.exoplayer2.ui.DefaultTimeBar
        android:id="@id/exo_progress"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="26dp"
        app:layout_constraintBottom_toBottomOf="@id/exo_position"
        app:layout_constraintEnd_toStartOf="@id/exo_duration"
        app:layout_constraintStart_toEndOf="@+id/exo_position"
        app:layout_constraintTop_toTopOf="@+id/exo_position"/>

    <TextView android:id="@id/exo_duration"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textStyle="bold"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:includeFontPadding="false"
        android:textColor="#FFBEBEBE"
        app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

drawable/video_frame

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <stroke
        android:width="2dp"
        android:color="@android:color/white" />

    <solid android:color="@android:color/black" />
</shape>

there might be some improvement to make so feel free to comment

Upvotes: 4

Related Questions