短视频场景的简单实现

 

起因

在某些业务场景下,可能会有刷短视频的需求,方便用户看到各式各样的效果,方便触达到用户,好处都不多说,第一个想到短视频这种产品形态的人简直就是天才

在我参与开发的某个项目的发展阶段,当然也碰到了要做这种竖屏短视频的需求,项目是做比较火的SD图片处理的,当然也涉及到一些视频生成的功能,有不同的视频模板提供个用户观看并选择.

在该场景中,需求是要求做有限刷新的视频流,当然其实要做无限刷新的也挺简单 两种方法,一种是intMaxValue,一种是54123这两种方法孰优孰劣本文就不过多赘述了,本文是阐述如何进行视频播放的相关实现的

实现方案阐述

整体架构采用ViewPager2嵌套item实现,没有采用ViewPager2嵌套Fragment,item封装了不同的状态,断网,加载,显示视频等页面状态

在item中采用了texture来进行播放的相关显示,然后在Activity中采用ExoPlayer来进行视频的解码播放等职能

在一些头部APP中,会采用多个解码器来进行解码,有一个解码器的Pool吗,这样能使得播放与更加流畅,但是我没使用这个方案,主要是开发时间比较短导致的,而且也没有预载的需求,索性就不考虑预载机制了

ExoPlayer配置

由于新版的Exoplayer更名为Media3了,使用全新的包名来导入依赖

implementation 'androidx.media3:media3-exoplayer:1.1.0'
implementation 'androidx.media3:media3-ui:1.1.0'

因为media3仍在项目初期,有一些bug,在写这篇文章的时候,进行以下代码的编写会报错调用不稳定的Api,添加注释 @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)即可

ExoPlayer.Builder(this)
    .setLoadControl(VideoCacheSingleton.loadControl).build().also { exoPlayer ->
        exoPlayer.volume = 0F
        exoPlayer.repeatMode = ExoPlayer.REPEAT_MODE_ONE
    }

在这里主要是配置了加载器部分,也就是LoadControl的部分,

val minBufferMs = 10000 // 10 seconds
val maxBufferMs = 30000 // 30 seconds

val loadControl = DefaultLoadControl.Builder()
    .setBufferDurationsMs(minBufferMs, maxBufferMs, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
    .build()

这里主要是用于控制播放器的缓存秒数,因为我配置为在内存中最大缓存30s,最小缓存10s,为什么要这样做呢,因为下方我们配置媒体源的时候需要使用渐进式的媒体源,这种做法的好处是方便缓存到本地,坏处是回吧所有的视频缓存进内存和本地,内存是很宝贵的资源,如果不加以限制缓存时间,内存触顶率绝对会相当的高,没开几个页面就会崩溃到怀疑人生

下面是媒体源的缓存配置

val MAX_CACHE_BYTE = 512 * 1024 * 1024L // 给512m的本地缓存
val cacheFile = File(PathUtils.getExternalAppCachePath()).resolve("video_cache")
dataSourceFactory = SimpleCache(cacheFile, LeastRecentlyUsedCacheEvictor(MAX_CACHE_BYTE), StandaloneDatabaseProvider(this))

下面是生成媒体源的配置

val mediaItem = MediaItem.fromUri(videoUrl)
val dataSourceFactory = CacheDataSource.Factory().setCache(VideoCacheSingleton.pageCache)
    .setUpstreamDataSourceFactory(DefaultDataSource.Factory(this))
ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)

之后切换视频的时候动态生成媒体源并且设置进去

exoplayer.setMediaSource(mediaSource)
exoplayer.setVideoTextureView(texture)

这些是基本的渐进式媒体源和LoadContel的基本配置

下面将用一个Demo来进行说明

  val bitmap = BitmapFactory.decodeStream(inputStream) val time = System.currentTimeMillis() val rect = getHasPixelRectSize(bitmap) Log.i(“TAG”, “onCreate: 耗时${System.currentTimeMillis() - time}”) Log.i(“TAG”, “onCreate: ${Rect(0, 0, bitmap.width, bitmap.height)}”) Log.i(“TAG”, “onCreate: $rect”) val ret: Bitmap = Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height()) binding.sampleImageView.setImageBitmap(ret)kotlin

Demo的内存消耗

image-20230716183238005

这个波形我还是比较满意的,在界面退出后内存都及时释放掉了,还有一部分内存没有释放是Native层的内存,是ExoPlayer管理的弱引用

package org.phcbest.viewpager2viewtest

import android.os.Bundle
import android.view.TextureView
import androidx.appcompat.app.AppCompatActivity
import androidx.media3.common.MediaItem
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.blankj.utilcode.util.PathUtils
import org.phcbest.viewpager2viewtest.exo.ExoplayerTextureAdapter
import java.io.File


class ViewPagerActivity : AppCompatActivity() {

    private val mVpView: ViewPager2 by lazy { findViewById<ViewPager2>(R.id.vp_view) }

    private var exoplayer: ExoPlayer? = null
    private var simpleCache: SimpleCache? = null

    @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_view_pager)

        val minBufferMs = 10000 // 10 seconds
        val maxBufferMs = 30000 // 30 seconds

        val loadControl = DefaultLoadControl.Builder()
            .setBufferDurationsMs(
                minBufferMs,
                maxBufferMs,
                DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
                DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
            )
            .build()


        exoplayer = ExoPlayer.Builder(this)
            .setLoadControl(loadControl).build().also { exoPlayer ->
                exoPlayer.volume = 0F
                exoPlayer.repeatMode = ExoPlayer.REPEAT_MODE_ONE
            }

        val MAX_CACHE_BYTE = 512 * 1024 * 1024L // 给512m的本地缓存
        val cacheFile = File(PathUtils.getExternalAppCachePath()).resolve("video_cache")
        simpleCache = SimpleCache(
            cacheFile,
            LeastRecentlyUsedCacheEvictor(MAX_CACHE_BYTE),
            StandaloneDatabaseProvider(this)
        )

        val exoplayerTextureAdapter = ExoplayerTextureAdapter()
        mVpView.adapter = exoplayerTextureAdapter
        exoplayerTextureAdapter.setNewInstance(VideoPath.pathList.toMutableList())
        mVpView.registerOnPageChangeCallback(object : OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                val mediaItem = MediaItem.fromUri(VideoPath.pathList[position])
                val dataSourceFactory = CacheDataSource.Factory().setCache(simpleCache!!)
                    .setUpstreamDataSourceFactory(DefaultDataSource.Factory(this@ViewPagerActivity))
                val mediaSource =
                    ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
                exoplayer!!.setMediaSource(mediaSource)
                val texture =
                    exoplayerTextureAdapter.getViewByPosition(position, R.id.texture) as TextureView
                exoplayer!!.setVideoTextureView(texture)
                exoplayer!!.prepare()
                exoplayer!!.play()
            }
        })
    }

    @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
    override fun onDestroy() {
        super.onDestroy()
        exoplayer?.release()
        simpleCache?.release()
        exoplayer = null
    }
}