Aravind OR
Aravind OR

Reputation: 97

How to create a video seek bar with thumbnails like in kinemaster ? In Android?

kinemaster

I am trying to achieve the above with exoplayer.

Creating a thumbnail list from the video at a certain interval . Say 10 seconds And displaying it to the seekbar along with the time .

How to accomplish this ? What are the things to consider when we are dealing with large files ? Is it better creating all thumbnails at first , or generating thumbnails as we seek through the video ?

enter image description here

How do we associate time and corresponding thumbnail like in the above image . Here these images should show between 4s-8s How do we do that ? I don't know how to achieve that using a regular recyclerview. How can we do that with a custom view ?

That's a lot of questions, any helps will be appreciated . Than u

Upvotes: 1

Views: 2485

Answers (1)

Amr
Amr

Reputation: 1322

here is the custom view from video timmer library with some modification and use of coroutine, also code contains useful comments

    // This file from video trimmer library with modifications
    // https://github.com/titansgroup/k4l-video-trimmer/blob/develop/k4l-video-trimmer/src/main/java/life/knowledge4/videotrimmer/view/TimeLineView.java
    class TimeLineView @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet?,
        defStyleAttr: Int = 0
    ) : View(context, attrs, defStyleAttr) {
        private var mVideoUri: Uri? = null
        private var mHeightView = 0
        private var mBitmapList: LongSparseArray<Bitmap?>? = null
        private var onListReady: (LongSparseArray<Bitmap?>) -> Unit = {}
    
        private fun init() {
            mHeightView = context.resources.getDimensionPixelOffset(R.dimen.frames_video_height)
        }
    
        val handler = CoroutineExceptionHandler { _, exception ->
            Timber.e("From CoroutineExceptionHandler", exception.message.toString())
        }
    
        override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
            val minW = paddingLeft + paddingRight + suggestedMinimumWidth
            val w = resolveSizeAndState(minW, widthMeasureSpec, 1)
            val minH = paddingBottom + paddingTop + mHeightView
            val h = resolveSizeAndState(minH, heightMeasureSpec, 1)
            setMeasuredDimension(w, h)
        }
    
        override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) {
            super.onSizeChanged(w, h, oldW, oldH)
            if (w != oldW) {
                getBitmap(w)
            }
        }
    
        var job: Job? = null
        private fun getBitmap(viewWidth: Int) {
            if (mBitmapList != null) { // if already got the thumbnails then don't do it again.
                return
            }
            job?.cancel()
            job = viewScope.launch(Dispatchers.IO + handler) {
                try {
                    val thumbnailList = LongSparseArray<Bitmap?>()
                    val mediaMetadataRetriever = MediaMetadataRetriever()
                    mediaMetadataRetriever.setDataSource(context, mVideoUri)
                    // Retrieve media data
                    val videoLengthInMs =
                        (mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)!!
                            .toInt() * 1000).toLong()
                    // Set thumbnail properties (Thumbs are squares)
                    val thumbWidth = mHeightView
                    val thumbHeight = mHeightView
                    val numThumbs = ceil((viewWidth.toFloat() / thumbWidth).toDouble())
                        .toInt()
                    val interval = videoLengthInMs / numThumbs
                    for (i in 0 until numThumbs) {
                        val bitmap: Bitmap? = mediaMetadataRetriever.getFrameAtTime(
                            i * interval,
                            MediaMetadataRetriever.OPTION_CLOSEST_SYNC
                        )?.run {
                            Bitmap.createScaledBitmap(
                                this,
                                thumbWidth,
                                thumbHeight,
                                false
                            )
                        }
                        thumbnailList.put(i.toLong(), bitmap)
                    }
                    mediaMetadataRetriever.release()
                    returnBitmaps(thumbnailList)
                } catch (e: Throwable) {
                }
            }
        }
    
        private fun returnBitmaps(thumbnailList: LongSparseArray<Bitmap?>) {
            onListReady.invoke(thumbnailList)
            this.onListReady = {} // here i reset the listener so that it doesn't get called again
    
            viewScope.launch(Dispatchers.Main) {
                mBitmapList = thumbnailList
                invalidate()
            }
        }
    
        override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            if (mBitmapList != null) {
                canvas.save()
                var x = 0
                for (i in 0 until mBitmapList!!.size()) {
                    val bitmap = mBitmapList!![i.toLong()]
                    if (bitmap != null) {
                        canvas.drawBitmap(bitmap, x.toFloat(), 0f, null)
                        x += bitmap.width
                    }
                }
            }
        }
       //this method recieves the thumbnails list if it's already generated so that you don't generate them twice.
        fun setVideo(data: Uri, thumbnailList: LongSparseArray<Bitmap?>? = null) {
            mVideoUri = data
            mBitmapList = thumbnailList
        }
         // this method is used to get the thumbnails once they are ready, to save them so that i don't recreate them again when onBindViewholder is called again.
        fun getThumbnailListOnce(onListReady: (LongSparseArray<Bitmap?>) -> Unit) {
            this.onListReady = onListReady
        }
    
        init {
            init()
        }
    }

i used corotuine in custom view as suggested here here the extension function for reference

    val View.viewScope: CoroutineScope
        get() {
            val storedScope = getTag(R.string.view_coroutine_scope) as? CoroutineScope
            if (storedScope != null) return storedScope

            val newScope = ViewCoroutineScope()
            if (isAttachedToWindow) {
                addOnAttachStateChangeListener(newScope)
                setTag(R.string.view_coroutine_scope, newScope)
            } else newScope.cancel()

            return newScope
        }

    private class ViewCoroutineScope : CoroutineScope, View.OnAttachStateChangeListener {
        override val coroutineContext = SupervisorJob() + Dispatchers.Main

        override fun onViewAttachedToWindow(view: View) = Unit

        override fun onViewDetachedFromWindow(view: View) {
            coroutineContext.cancel()
            view.setTag(R.string.view_coroutine_scope, null)
        }
    }

i am using this inside viewPager so here is item_video.xml which used in recyclerview adapter

    <?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"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <com.google.android.exoplayer2.ui.StyledPlayerView
            android:id="@+id/video_view"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_gravity="center"
            app:auto_show="true"
            app:controller_layout_id="@layout/custom_exo_overlay_controller_view"
            app:layout_constraintBottom_toTopOf="@id/exoBottomControls"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="1.0"
            app:repeat_toggle_modes="none"
            app:resize_mode="fixed_width"
            app:surface_type="surface_view"
            app:use_controller="true" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>

and inside your custom_exo_overlay_controller_view you would have something like this

    <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="wrap_content">
    <!--        other controls-->
        <com.myAppName.presentation.widget.TimeLineView
            android:id="@+id/timeLineView"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginTop="6dp"
            app:layout_constraintBottom_toBottomOf="@id/exo_progress"
            app:layout_constraintEnd_toEndOf="@id/exo_progress"
            app:layout_constraintStart_toStartOf="@+id/exo_progress"
            app:layout_constraintTop_toTopOf="@+id/exo_progress"
            tools:background="@drawable/orange_button_selector" />
    
        <com.google.android.exoplayer2.ui.DefaultTimeBar
            android:id="@id/exo_progress"
            android:layout_width="0dp"
            android:layout_height="52dp"
            app:buffered_color="@android:color/transparent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:played_color="@android:color/transparent"
            app:scrubber_drawable="@drawable/ic_scrubber"
            app:touch_target_height="52dp"
            app:unplayed_color="@android:color/transparent" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>

note that DefaultTimeBar has some attributes as transparent so that thumbnails appears under it.

and inside viewHolder i have this

    fun bind(video: ChatMediaFile.Video) {
        initializePlayer(video)
        showThumbnailTimeLine(video)
        handleSoundIcon(video)
    }
    private fun showThumbnailTimeLine(video: ChatMediaFile.Video) {
        binding.videoView.findViewById<TimeLineView?>(R.id.timeLineView)?.let {
            if (video.thumbnailList == null) {
                it.getThumbnailListOnce { thumbnailList ->
                    video.thumbnailList = thumbnailList
                }
                video.url.let { url -> it.setVideo(Uri.parse(url)) }
            } else {
                video.url.let { url -> it.setVideo(Uri.parse(url), video.thumbnailList) }
            }
        }
    }

Upvotes: 1

Related Questions