如何像 kinemaster 一样创建带有缩略图的视频搜索栏?在 Android?
How to create a video seek bar with thumbnails like in kinemaster ? In Android?
我正在尝试使用 exoplayer 实现上述目标。
以特定间隔从视频创建缩略图列表。说10秒
并将其与时间一起显示到搜索栏。
如何实现?
处理大文件时需要考虑哪些事项?
是先创建所有缩略图好,还是在我们搜索视频时生成缩略图好?
我们如何像上图那样关联时间和相应的缩略图。
此处这些图像应显示在 4s-8s 之间
我们该怎么做?我不知道如何使用常规的 recyclerview 来实现。我们如何使用自定义视图来做到这一点?
问题很多,如有帮助将不胜感激。比你
这里是video timmer library的自定义视图,对协程进行了一些修改和使用,代码中也包含有用的注释
// 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()
}
}
我按照建议在自定义视图中使用了 corotuine here
这里的扩展函数供参考
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)
}
}
我在 viewPager 中使用它,所以这里是 item_video.xml,它在 recyclerview 适配器中使用
<?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>
在你的custom_exo_overlay_controller_view里面你会有这样的东西
<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>
请注意,DefaultTimeBar 具有一些透明属性,以便缩略图显示在其下方。
在 viewHolder 里面我有这个
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) }
}
}
}
我正在尝试使用 exoplayer 实现上述目标。
以特定间隔从视频创建缩略图列表。说10秒 并将其与时间一起显示到搜索栏。
如何实现? 处理大文件时需要考虑哪些事项? 是先创建所有缩略图好,还是在我们搜索视频时生成缩略图好?
我们如何像上图那样关联时间和相应的缩略图。 此处这些图像应显示在 4s-8s 之间 我们该怎么做?我不知道如何使用常规的 recyclerview 来实现。我们如何使用自定义视图来做到这一点?
问题很多,如有帮助将不胜感激。比你
这里是video timmer library的自定义视图,对协程进行了一些修改和使用,代码中也包含有用的注释
// 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()
}
}
我按照建议在自定义视图中使用了 corotuine here 这里的扩展函数供参考
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)
}
}
我在 viewPager 中使用它,所以这里是 item_video.xml,它在 recyclerview 适配器中使用
<?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>
在你的custom_exo_overlay_controller_view里面你会有这样的东西
<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>
请注意,DefaultTimeBar 具有一些透明属性,以便缩略图显示在其下方。
在 viewHolder 里面我有这个
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) }
}
}
}