基于timeupdate与localStorage的视频断点续播:从基础实现到性能优化
1. 视频断点续播的核心原理
每次看视频看到一半被迫退出,再打开又要从头开始,这种体验实在太糟糕了。作为开发者,我们可以用HTML5 Video API的timeupdate事件配合localStorage轻松解决这个问题。想象一下,这就像看书时夹了个书签,下次打开直接翻到上次看到的位置。
核心原理其实很简单:当视频播放时,持续监听播放进度变化(timeupdate事件),把当前时间点(currentTime)存到浏览器本地(localStorage)。下次打开页面时,先从本地读取保存的时间点,让视频直接跳转到这个位置继续播放。整个过程就像这样:
// 获取视频元素 const video = document.querySelector('video'); // 监听播放进度变化 video.addEventListener('timeupdate', () => { // 把当前播放时间存入本地存储 localStorage.setItem('videoProgress', video.currentTime); }); // 页面加载时检查是否有保存的进度 window.addEventListener('load', () => { const savedTime = localStorage.getItem('videoProgress'); if (savedTime) { video.currentTime = parseFloat(savedTime); } });2. 基础实现方案
2.1 最小可用版本
先来看个最基础的实现,我用这个方案帮朋友快速解决了他的在线课程网站需求:
<video id="myVideo" controls src="lecture.mp4"></video> <script> const video = document.getElementById('myVideo'); const videoId = 'lecture_101'; // 每个视频唯一标识 // 恢复播放进度 const savedTime = localStorage.getItem(`videoTime_${videoId}`); if (savedTime) { video.currentTime = parseFloat(savedTime); } // 实时保存进度(简单版) video.addEventListener('timeupdate', () => { localStorage.setItem(`videoTime_${videoId}`, video.currentTime); }); </script>这个版本虽然简单,但有几个明显问题:
- 频繁写入localStorage可能影响性能
- 没有考虑多标签页同时操作的情况
- 视频源变化时可能错误恢复进度
2.2 关键参数说明
- currentTime:视频当前播放位置(秒),可读可写
- timeupdate事件:播放位置变化时触发,频率约4-66Hz
- localStorage:浏览器本地存储,容量约5MB
实测发现,在Chrome上timeupdate默认每秒触发4-8次,Safari会更频繁些。这就引出了我们的第一个优化点。
3. 性能优化实战
3.1 事件节流控制
直接监听timeupdate会导致存储操作太频繁。我曾在项目中遇到过因此导致的页面卡顿,后来加了节流控制:
let lastSaveTime = 0; const SAVE_INTERVAL = 1000; // 每秒最多存一次 video.addEventListener('timeupdate', () => { const now = Date.now(); if (now - lastSaveTime > SAVE_INTERVAL) { localStorage.setItem(videoId, video.currentTime); lastSaveTime = now; } });更优雅的方式是用lodash的throttle:
import { throttle } from 'lodash'; video.addEventListener('timeupdate', throttle(() => { localStorage.setItem(videoId, video.currentTime); }, 1000));3.2 存储策略优化
localStorage是同步操作,可能阻塞主线程。我们可以:
- 使用sessionStorage替代,页面关闭后自动清除
- 改用IndexedDB进行异步存储
- 添加try-catch防止超出容量限制
function safeSetStorage(key, value) { try { localStorage.setItem(key, value); } catch (e) { console.warn('本地存储已满,自动清理旧数据'); localStorage.clear(); localStorage.setItem(key, value); } }3.3 多视频管理
当页面有多个视频时,需要为每个视频创建唯一标识。我推荐这种方案:
<video>window.addEventListener('storage', (event) => { if (event.key === videoId) { video.currentTime = parseFloat(event.newValue); } });4.2 视频源变更处理
当视频src变化时,应该清除旧进度。我踩过这个坑:
const observer = new MutationObserver(() => { if (video.src !== lastVideoSrc) { localStorage.removeItem(videoId); lastVideoSrc = video.src; } }); observer.observe(video, { attributes: true, attributeFilter: ['src'] });4.3 移动端适配技巧
在iOS上,自动播放受限且页面卸载时timeupdate可能来不及触发。解决方案:
- 添加页面可见性监听
- 在beforeunload事件中强制保存
// 页面隐藏时立即保存 document.addEventListener('visibilitychange', () => { if (document.hidden) { localStorage.setItem(videoId, video.currentTime); } }); // 页面关闭前保存 window.addEventListener('beforeunload', () => { localStorage.setItem(videoId, video.currentTime); });5. 替代方案对比
5.1 各方案性能测试
| 方案 | 写入频率 | 内存占用 | 兼容性 | 适用场景 |
|---|---|---|---|---|
| 基础实现 | 高频 | 低 | 最好 | 简单页面 |
| 节流控制 | 1Hz | 低 | 好 | 大多数场景 |
| IndexedDB | 高频 | 中 | 较好 | 复杂应用 |
| Service Worker缓存 | 低频 | 高 | 一般 | PWA应用 |
5.2 IndexedDB实现示例
对于需要保存大量观看记录的应用,IndexedDB是更好的选择:
// 初始化数据库 const dbPromise = idb.open('VideoDB', 1, upgradeDB => { upgradeDB.createObjectStore('progress'); }); // 保存进度 async function saveProgress(videoId, time) { const db = await dbPromise; const tx = db.transaction('progress', 'readwrite'); tx.objectStore('progress').put(time, videoId); return tx.complete; } // 读取进度 async function getProgress(videoId) { const db = await dbPromise; return db.transaction('progress') .objectStore('progress').get(videoId); }6. 完整实现代码
下面是我在一个在线教育平台实际使用的增强版代码,包含了所有最佳实践:
<video id="lectureVideo">video.addEventListener('loadedmetadata', () => { const savedTime = localStorage.getItem(videoId); if (savedTime) { // 确保不会跳转到超过视频时长 video.currentTime = Math.min(parseFloat(savedTime), video.duration); } });Q:移动端失效怎么办?A:iOS的省电模式可能限制localStorage使用,可以:
- 添加try-catch处理
- 改用cookie作为fallback
- 提示用户关闭省电模式
8. 用户体验增强
除了基础功能,我还添加了这些提升体验的功能:
- 进度预览:跳转前显示"继续观看从XX分XX秒开始"
- 自动续播倒计时:3秒内无操作则自动续播
- 多设备同步:配合后端API实现跨设备进度同步
// 续播提示示例 function showResumePrompt(time) { const mins = Math.floor(time / 60); const secs = Math.floor(time % 60); const prompt = document.createElement('div'); prompt.innerHTML = ` <div class="resume-prompt"> 上次观看到 ${mins}分${secs}秒 <button id="resumeBtn">继续播放</button> <button id="restartBtn">重新开始</button> </div> `; document.body.appendChild(prompt); document.getElementById('resumeBtn').addEventListener('click', () => { video.currentTime = time; prompt.remove(); }); document.getElementById('restartBtn').addEventListener('click', () => { localStorage.removeItem(videoId); prompt.remove(); }); }实现视频断点续播看似简单,但要做得健壮可靠需要考虑各种边界情况。经过多个项目的迭代,现在这套方案已经能处理99%的使用场景。如果你有特殊需求,比如需要保存播放速度、音量等状态,也可以扩展这个模式。
