Exoplayer2實現邊播放邊緩存

前言

ExoPlayer是Google開源的一款Android應用程序級的媒體播放器。它提供了Android MediaPlayer API的替代方法,能夠在本地和Internet上播放音頻和視頻。ExoPlayer支持Android MediaPlayer API當前不支持的功能,包括DASH和SmoothStreaming自適應播放。與MediaPlayer API不一樣,ExoPlayer易於自定義和擴展。這裏主要使用 ExoPlayer + AndroidVideoCache 實現邊播放邊緩存。下面點擊可查看對應庫和文檔。java


效果

在這裏插入圖片描述

ExoPlayer使用

添加ExoPlayer與AndroidVideoCache依賴,我這邊使用的是ExoPlayer v2.10.5和AndroidVideoCache v2.7.1版本,可根據自身需求升級或降級。android

// ExoPlayer
implementation 'com.google.android.exoplayer:exoplayer-core:2.10.5'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.10.5'
// AndroidVideoCache
implementation 'com.danikula:videocache:2.7.1'

依賴添加完成後在Layout中使用ExoPlayer庫中的PlayerView組件。git

<androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.google.android.exoplayer2.ui.PlayerView
            android:id="@+id/media_player_view"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.core.widget.ContentLoadingProgressBar
            android:id="@+id/media_video_progress"
            style="?android:attr/progressBarStyleLarge"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:indeterminateTint="@color/colorAccent"
            android:indeterminateTintMode="src_atop"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

具體使用,使用了DataBinding,不明白的朋友可在評論區評論,儘可能及時給你回覆。github

class MainActivity : AppCompatActivity() { 
 
   

    companion object { 
 
   
        private const val MEDIA_URI: String = "http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400"
    }

    private lateinit var binding: ActivityMainBinding

    private var mStartPosition = 0
    private var mPlaybackProgressPosition = 0L

    // 自適應音軌
    private val mTrackSelectionFactory by lazy { 
 
    AdaptiveTrackSelection.Factory() }

    // 建立播放器
    private val mPlayer by lazy { 
 
   
        ExoPlayerFactory.newSimpleInstance(
            this,
            DefaultRenderersFactory(this),
            DefaultTrackSelector(mTrackSelectionFactory),
            DefaultLoadControl()
        )
    }

    private val mOnVideoEventListener by lazy { 
 
    OnVideoEventListener() }

    override fun onCreate(savedInstanceState: Bundle?) { 
 
   
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.data = this
        binding.executePendingBindings()
        initView()
    }

    private fun initView() { 
 
   
        // 將播放器鏈接到視圖
        binding.mediaPlayerView.player = mPlayer
        mPlayer?.addListener(mOnVideoEventListener)
    }

    override fun onResume() { 
 
   
        super.onResume()
        updatePlaybackProgress()
        playbackVideo()
    }

    override fun onPause() { 
 
   
        super.onPause()
        mPlayer?.stop()
    }

    override fun onDestroy() { 
 
   
        super.onDestroy()
        mPlayer?.removeListener(mOnVideoEventListener)
    }

    /** * 播放視頻 */
    private fun playbackVideo() { 
 
   
        clearPlaybackProgress()
        if (mPlayer != null) { 
 
   
            // 設置播放進度
            val mHaveStartPosition = mStartPosition != C.INDEX_UNSET
            if (mHaveStartPosition){ 
 
   
                mPlayer.seekTo(mStartPosition, mPlaybackProgressPosition)
            }
            @C.ContentType
            val mMediaSourceType = Util.inferContentType(Uri.parse(MEDIA_URI), null)
            if(mMediaSourceType == C.TYPE_OTHER) { 
 
   
                // 獲取構建後的媒體資源
                val mMediaSource = MediaPlayerManager.getDefault().buildDataSource(this, MEDIA_URI)
                // 將媒體資源設置給播放器
                mPlayer.prepare(mMediaSource, !mHaveStartPosition, true)
                // 是不是自動播放
                mPlayer.playWhenReady = true
            } else { 
 
   
                Log.e(javaClass.simpleName, "播放媒體資源出錯,類型不支持,錯誤類型:$mMediaSourceType")
                return
            }
        }
    }

    /** * 視頻播放事件監聽 */
    private inner class OnVideoEventListener: Player.EventListener{ 
 
   

        override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { 
 
   
            if (playbackState == ExoPlayer.STATE_BUFFERING){ 
 
   
                binding.mediaVideoProgress.show()
            }else { 
 
   
                binding.mediaVideoProgress.hide()
            }
        }

        override fun onIsPlayingChanged(isPlaying: Boolean) { 
 
   
            val mPlayerChange = !isPlaying && mPlayer?.playbackState == Player.STATE_ENDED
            if (mPlayerChange) clearPlaybackProgress()
            if (mPlayerChange) { 
 
   
                Toast.makeText(this@MainActivity, "播放完成!", Toast.LENGTH_SHORT).show()
            }
        }

        override fun onPlayerError(error: ExoPlaybackException?) { 
 
   
            binding.mediaVideoProgress.hide()
            clearPlaybackProgress()
            Toast.makeText(this@MainActivity, "播放出現錯,${ 
     error?.message}!", Toast.LENGTH_SHORT).show()
        }
    }

    /** * 更新播放進度 */
    private fun updatePlaybackProgress() { 
 
   
        mStartPosition = mPlayer?.currentWindowIndex ?: 0
        mPlaybackProgressPosition = max(0, (mPlayer?.contentPosition ?: 0))
    }

    /** * 清除播放位置 */
    private fun clearPlaybackProgress() { 
 
   
        mStartPosition = C.INDEX_UNSET
        mPlaybackProgressPosition = C.TIME_UNSET
        mPlayer?.stop()
    }
}

MediaPlayerManager定義

主要使用AndroidVideoCache中的HttpProxyCacheServer來緩存數據資源,使用ExoPlayer中的DefaultDataSourceFactory來決定數據加載策略。web

class MediaPlayerManager { 
 
   

    private var mUserAgent = this.javaClass.simpleName

    // 視頻加載代理
    @Volatile
    private var mProxyCacheServer: HttpProxyCacheServer? = null

    companion object{ 
 
   

        private const val DISK_CACHE_DIR_NAME = "Video"

        @Volatile
        private var INSTANCES: MediaPlayerManager? = null

        fun getDefault(): MediaPlayerManager = INSTANCES ?: synchronized(this){ 
 
    MediaPlayerManager().also  { 
 
    INSTANCES = it }}
    }

    fun init(context: Context, userAgent: String) { 
 
   
        mProxyCacheServer = createProxyCacheServer(context)
        mUserAgent = Util.getUserAgent(context, userAgent)
    }

    /** * 將傳入的uri構建爲一個規媒體資源 * * DashMediaSource DASH. * SsMediaSource SmoothStreaming. * HlsMediaSource HLS. * ProgressiveMediaSource 常規媒體文件. * * @return 返回一個常規媒體資源 */
    fun buildDataSource(context: Context, uri: String): MediaSource { 
 
   
        // 構建一個默認的Http數據資源處理工廠
        val mHttpDataSourceFactory = DefaultHttpDataSourceFactory(mUserAgent)
        // DefaultDataSourceFactory決定數據加載模式,是從網絡加載仍是本地緩存加載
        val mDataSourceFactory = DefaultDataSourceFactory(context, mHttpDataSourceFactory)
        // AndroidVideoCache庫不支持DASH, SS(Smooth Streaming:平滑流媒體,如直播流), HLS數據格式,因此這裏使用一個常見媒體轉換數據資源工廠
        return ProgressiveMediaSource.Factory(mDataSourceFactory).createMediaSource(Uri.parse(getProxyUrl(uri)))
    }

    /** * 建立視頻加載代理 */
    private fun createProxyCacheServer(context: Context): HttpProxyCacheServer { 
 
   
        return HttpProxyCacheServer.Builder(context)
            .cacheDirectory(getDiskCacheDirectory(context)) // 設置磁盤存儲地址
            .maxCacheSize(1024 * 1024 * 1024)     // 設置可存儲1G資源
            .build()
    }

    /** * 獲取代理地址 */
    fun getProxyUrl(url: String): String? = mProxyCacheServer?.getProxyUrl(url)

    /** * 是否緩存 * @return true:已經緩存 */
    fun isCached(url: String) = mProxyCacheServer?.isCached(url) ?: false

    /** * 視頻磁盤緩存地址 */
    @SuppressLint("SdCardPath")
    fun getDiskCacheDirectory(context: Context): File { 
 
   
        var cacheDir: File? = null
        if (Environment.MEDIA_MOUNTED == getExternalStorageState()) { 
 
   
            cacheDir = getExternalCacheDir(context)
        }
        if (cacheDir == null) { 
 
   
            cacheDir = context.cacheDir
        }
        if (cacheDir == null) { 
 
   
            val cacheDirPath = "/data/data/${ 
     context.packageName}/cache/"
            cacheDir = File(cacheDirPath)
        }
        return File(cacheDir, DISK_CACHE_DIR_NAME)
    }

    private fun getExternalStorageState(): String { 
 
   
        return try { 
 
   
            Environment.getExternalStorageState()
        } catch (e: NullPointerException) { 
 
   
            ""
        }
    }

    private fun getExternalCacheDir(context: Context): File? { 
 
   
        val cacheDir = context.getExternalFilesDir("Cache")
        if (!cacheDir?.exists()!!) { 
 
   
            if (!cacheDir.mkdirs()) { 
 
   
                return null
            }
        }
        return cacheDir
    }

    /** * 刪除全部視頻緩存 */
    @Throws(IOException::class)
    fun deleteAllCache(context: Context) { 
 
   
        val mFile = getDiskCacheDirectory(context)
        if (!mFile.exists()) return
        val mFiles = mFile.listFiles()
        if (!mFiles.isNullOrEmpty() && mFiles.isNotEmpty()) { 
 
   
            mFiles.forEach { 
 
   
                deleteVideoCache(it)
            }
        }
    }

    /** * 刪除視頻緩存 */
    @Throws(IOException::class)
    private fun deleteVideoCache(file: File) { 
 
   
        if (file.isFile && file.exists()) { 
 
   
            val isDeleted = file.delete()
            Log.e(javaClass.simpleName, "刪除視頻緩存:${ 
     file.path}\t刪除狀態:$isDeleted")
        }
    }

    /** * 獲取磁盤緩存的數據大小,單位:KB */
    fun getDiskCacheSize(context: Context): Long { 
 
   
        val file = getDiskCacheDirectory(context)
        var blockSize = 0L
        try { 
 
   
            blockSize = if (file.isDirectory) getFileSizes(file) else getFileSize(file)
        } catch (e: Exception) { 
 
   
            e.printStackTrace()
        }
        return blockSize
    }

    private fun getFileSizes(file: File): Long { 
 
   
        var size = 0L
        file.listFiles()?.forEach { 
 
   
            if (it.isDirectory) { 
 
   
                size += getFileSizes(it)
            } else { 
 
   
                try { 
 
   
                    size += getFileSize(it)
                } catch (e: Exception) { 
 
   
                    e.printStackTrace()
                }
            }
        }
        return size
    }

    private fun getFileSize(file: File): Long { 
 
   
        var size = 0L
        if (file.exists()) { 
 
   
            FileInputStream(file).use { 
 
   
                size = it.available().toLong()
            }
        }
        return size
    }
}

Tips:

  • MediaPlayerManager 類中定義了緩存函數 getDiskCacheDirectory
  • 緩存目標默認地址爲:/storage/emulated/0/Android/data/應用包名/files/Cache/Video/目標文件

對Android本地磁盤有興趣的能夠看看我這篇:Android存儲路徑探索緩存

案例

碼雲Gitee
Github網絡

本文分享 CSDN - 秦川小將。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。app

相關文章
相關標籤/搜索