Android Kotlin+ExoPlayer视频有效观看进度持久化问题求助
解决ExoPlayer有效观看进度持久化与续播问题
你的核心问题是没有在合适的时机读写持久化数据,导致重启后所有进度变量重置为初始值。下面分别提供轻量的SharedPreferences方案和适合多视频的Room方案,直接修改你的代码即可生效。
一、SharedPreferences 快速实现(适合单/少量视频)
步骤说明
- 用视频ID作为存储键的前缀,区分不同视频的进度数据
- 在视频初始化时读取历史数据,恢复
visitedSeconds、totalPlaybackTime、播放位置 - 在页面暂停/销毁时保存当前进度数据
- 实现续播逻辑:读取上次播放位置并跳转
修改后的完整代码
class ViewWatchLessonsActivity( private val context: Context ) { val binding = ActivityWatchLessonsBinding.inflate(LayoutInflater.from(context)) private val handler = Handler(Looper.getMainLooper()) private val player = ExoPlayer.Builder(context).build() private val textViewProgress = binding.txtProgressNumber private val progressBar = binding.progressBar // 新增:SharedPreferences实例 private val sp = context.getSharedPreferences("VideoProgress", Context.MODE_PRIVATE) private var videoDuration: Long = 0 private var totalPlaybackTime: Long = 0 private var isUserSeeking: Boolean = false private var progress: Int = 0 private val visitedSeconds = mutableSetOf<Int>() // 新增:记录当前视频ID,用于区分存储 private lateinit var currentVideoId: String fun initVideoView(videoAddress: Int) { currentVideoId = videoAddress.toString() binding.playerViewOfLessons.player = player val mediaItem = MediaItem.fromUri("android.resource://${context.packageName}/${videoAddress}") player.setMediaItem(mediaItem) player.prepare() // 新增:读取历史进度数据 loadProgressData() player.addListener(object : Player.Listener { override fun onPlaybackStateChanged(state: Int) { when (state) { Player.STATE_READY -> { videoDuration = player.duration // 新增:续播逻辑,跳转到上次播放位置 val lastPosition = sp.getLong("${currentVideoId}_position", 0) if (lastPosition > 0) { player.seekTo(lastPosition) } updateProgressBar() } Player.STATE_ENDED -> { handler.removeCallbacksAndMessages(null) } } } override fun onPositionDiscontinuity(reason: Int) { if (reason == Player.DISCONTINUITY_REASON_SEEK) { isUserSeeking = true // 修正:用户开始跳转时标记为true,避免统计跳转的片段 } } }) } private fun updateProgressBar() { handler.post(object : Runnable { @SuppressLint("SetTextI18n") override fun run() { val currentPosition = player.currentPosition if (!isUserSeeking) { val currentSecond = (currentPosition / 1000).toInt() if (!visitedSeconds.contains(currentSecond)) { visitedSeconds.add(currentSecond) totalPlaybackTime += 1000 // 可选:实时保存,避免崩溃丢失数据 saveProgressData() } progress = (totalPlaybackTime / videoDuration.toFloat() * 100).toInt() textViewProgress.text = "$progress/100%" progressBar.progress = progress } handler.postDelayed(this, 1000) } }) } // 新增:读取历史进度数据 private fun loadProgressData() { totalPlaybackTime = sp.getLong("${currentVideoId}_total", 0) progress = sp.getInt("${currentVideoId}_progress", 0) // 解析保存的visitedSeconds字符串为Set val visitedStr = sp.getString("${currentVideoId}_visited", "") ?: "" if (visitedStr.isNotEmpty()) { visitedSeconds.addAll(visitedStr.split(",").map { it.toInt() }) } } // 新增:保存进度数据到SharedPreferences private fun saveProgressData() { sp.edit() .putLong("${currentVideoId}_total", totalPlaybackTime) .putInt("${currentVideoId}_progress", progress) .putString("${currentVideoId}_visited", visitedSeconds.joinToString(",")) .putLong("${currentVideoId}_position", player.currentPosition) .apply() } fun onPause() { handler.removeCallbacksAndMessages(null) player.pause() // 暂停时保存数据 saveProgressData() } fun onResume() { updateProgressBar() } fun onDestroy() { handler.removeCallbacksAndMessages(null) // 销毁前最终保存 saveProgressData() player.release() } }
关键修正点
- 修正了
isUserSeeking的逻辑:用户开始跳转时标记为true,避免统计跳转过程的片段(原代码只在跳转结束时设为false,逻辑反了) - 用视频ID作为存储键前缀,支持多视频的进度区分
- 将
visitedSeconds集合转为字符串存储,解决SharedPreferences无法直接存Set的问题 - 在
STATE_READY时自动跳转到上次播放位置,实现续播
二、Room 数据库实现(适合大量视频场景)
如果需要管理多个视频的进度,SharedPreferences会显得杂乱,推荐用Room数据库存储。
1. 定义实体类
@Entity(tableName = "video_progress") data class VideoProgress( @PrimaryKey val videoId: String, // 视频唯一标识,比如你的videoAddress转字符串 val totalPlaybackTime: Long, val progress: Int, val visitedSeconds: String, // 同样用逗号分隔的字符串存储已观看秒数 val lastPosition: Long // 上次播放位置 )
2. 定义Dao接口
@Dao interface VideoProgressDao { @Query("SELECT * FROM video_progress WHERE videoId = :videoId") suspend fun getProgress(videoId: String): VideoProgress? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveProgress(progress: VideoProgress) }
3. 定义数据库类
@Database(entities = [VideoProgress::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun videoProgressDao(): VideoProgressDao companion object { @Volatile private var INSTANCE: AppDatabase? = null fun getInstance(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "app_database" ).build() INSTANCE = instance instance } } } }
4. 修改ViewWatchLessonsActivity中的读写逻辑
替换SharedPreferences的读写为Room的Coroutine调用:
// 替换SharedPreferences实例为Room相关 private val db = AppDatabase.getInstance(context) private val progressDao = db.videoProgressDao() // 读取数据改为 private fun loadProgressData() { CoroutineScope(Dispatchers.IO).launch { val savedProgress = progressDao.getProgress(currentVideoId) savedProgress?.let { totalPlaybackTime = it.totalPlaybackTime progress = it.progress val visitedStr = it.visitedSeconds if (visitedStr.isNotEmpty()) { visitedSeconds.addAll(visitedStr.split(",").map { it.toInt() }) } // 续播位置在主线程设置 withContext(Dispatchers.Main) { if (it.lastPosition > 0) { player.seekTo(it.lastPosition) } } } } } // 保存数据改为 private fun saveProgressData() { CoroutineScope(Dispatchers.IO).launch { val progressEntity = VideoProgress( videoId = currentVideoId, totalPlaybackTime = totalPlaybackTime, progress = progress, visitedSeconds = visitedSeconds.joinToString(","), lastPosition = player.currentPosition ) progressDao.saveProgress(progressEntity) } }
常见问题排查
- 存储后读取不到:检查键名是否和视频ID绑定,避免不同视频数据覆盖
- 续播不生效:确保在
player.prepare()之后调用seekTo,且读取的lastPosition不为0 - 多线程问题:Room操作必须在子线程执行,所以要用Coroutine或AsyncTask
内容的提问来源于stack exchange,提问作者mah beh
相关产品推荐
相关产品推荐

