前言
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存儲路徑探索緩存
案例
本文分享 CSDN - 秦川小將。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。app