🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏19年编写主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
🌛《开源项目》本专栏主要介绍目前热门的开源项目,带大家快速了解并轻松上手使用
🍎 《前端技术》专栏以实战为主介绍日常开发中前端应用的一些功能以及技巧,均附有完整的代码示例
✨《开发技巧》本专栏包含了各种系统的设计原理以及注意事项,并分享一些日常开发的功能小技巧
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
👍《Spring Security》专栏中我们将逐步深入Spring Security的各个技术细节,带你从入门到精通,全面掌握这一安全技术
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~
视频续播功能实现 - 断点续看从前端到 Spring Boot 后端
1. 前言
在视频网站或在线学习平台中,用户观看长视频(如课程、电影)时常会中途退出。若再次进入时不得不从头开始,体验大打折扣。视频续播(Resume Playback)
功能可以帮助用户保存上次观看位置,下次打开时自动跳转到该时间点继续观看,大幅提升用户体验。
比如我们常见的B站,当你播放中途退出,继续访问这个视频的时候,会提示 已为您定位至XXXX 的提示,如下图:
本文博主将从为什么要做续播、续播原理、前端实现、后端实现到测试与优化,逐步拆解整个流程,并给出完整代码示例,帮助小伙伴快速在项目中落地该功能。
2. 为什么要做视频续播
在如今的流媒体时代,用户平均每天观看视频时长超过 2.5 小时,但其中可能会出现观看会话会被中断(临时退出、电话、通知、设备切换等)。能否记住播放位置并提供无缝续播体验,已成为衡量视频平台专业度的重要指标!
提升用户体验
用户无需手动记忆上次进度,打开即看
长视频更易于分段观看,提高学习/观影效率
增加平台粘性
优质体验能让用户更愿意再次回访,延长平台使用时长
数据价值挖掘
记录观看进度,可分析用户活跃度、观看习惯,用于个性化推荐
3. 续播功能原理
前端监听
视频播放进度,将当前时间点(currentTime
)在用户退出或定时时保存。
存储进度
简易方案:localStorage
(针对单设备、单浏览器)
复杂方案:通过 REST
接口将进度保存到后端数据库(支持多设备、多浏览器)
恢复进度
页面加载时,读取存储的进度,将 <video>
的 currentTime
设置为该值
3.1 常见的续播记录系统架构
如上述所说,如果你仅针对单设备、单浏览器,可以直接使用本地存储,但如果需要多设备支持那么就需要有如下规划:
3.2 常见的触发记录时机
前端在出发播放进度记录,常见的有以下几种
事件类型 | 记录策略 | 用户行为 |
---|---|---|
暂停播放 | 立即记录 | 主动暂停 |
离开页面 | 最后位置记录 | 关闭标签/切换应用 |
播放结束 | 重置位置 | 完整观看 |
进度拖拽 | 延迟记录(防抖动) | 快速跳转 |
4. 纯前端实现方案
下面给小伙伴们演示基于原生 HTML5 Video
+ JavaScript
的示例,使用 localStorage
做本地保存
4.1 基础实现代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>视频续播示例</title>
<style>
video { width: 100%; max-width: 600px; margin: 20px auto; display: block; }
</style>
</head>
<body>
<h2>视频续播示例</h2>
<!-- 视频播放地址 -->
<video id="myVideo" controls>
<source src="https://你的视频地址.mp4" type="video/mp4">
</video>
<!-- 视频播放监听 -->
<script>
const video = document.getElementById('myVideo');
const VIDEO_ID = 'movie-123'; //视频标识
const STORAGE_KEY = `video-progress-${VIDEO_ID}`;
// 初始化播放位置
const savedTime = localStorage.getItem(STORAGE_KEY);
if (savedTime) video.currentTime = parseFloat(savedTime);
// 进度记录函数
function saveProgress() {
localStorage.setItem(STORAGE_KEY, video.currentTime.toString());
}
// 事件监听
//拖动播放条或进度条播放变化
video.addEventListener('timeupdate', throttle(saveProgress, 5000));
//暂停
video.addEventListener('pause', saveProgress);
//播放完成
video.addEventListener('ended', () => {
localStorage.removeItem(STORAGE_KEY);
});
// 离开或刷新页面时立即保存
window.addEventListener('beforeunload', saveProgress);
// 节流函数
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
func.apply(this, args);
lastCall = now;
}
};
}
</script>
</body>
</html>
STORAGE_KEY
基于视频 URL 唯一标识,每个视频分开保存
4.2 增强版本地存储
聪明的小伙伴们上述代码案例就能看出,仅仅只能记录并续播最后一次观看的视频,那么如果我希望记录5个之前观看中断的视频,那么就可以参考以下代码:
// 存储完整观看记录
function savePlaybackState() {
const state = {
timestamp: Date.now(),
progress: video.currentTime,
duration: video.duration,
videoId: VIDEO_ID,
percentage: (video.currentTime / video.duration * 100).toFixed(1)
};
// 保存最近5条记录
const history = JSON.parse(localStorage.getItem('video-history') || '[]');
const newHistory = [
state,
...history.filter(item => item.videoId !== VIDEO_ID)
].slice(0, 5);
localStorage.setItem('video-history', JSON.stringify(newHistory));
localStorage.setItem(STORAGE_KEY, video.currentTime.toString());
}
5. 后端(Spring Boot)实现
当需要跨设备同步或用户登录状态下保存进度时,可通过后端接口存储。下面示例用 Spring Boot
+MyBatis
+MySQL
做简易实现,供小伙伴们参考:
5.1 数据库表
对应的实体模型小伙伴们可以自己生成
CREATE TABLE video_progress (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
video_id VARCHAR(255) NOT NULL,
watched_time DOUBLE NOT NULL,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY idx_user_video (user_id, video_id)
);
5.2 后端接口
这里仅仅演示后端记录视频进度的功能,相关用户鉴权等小伙伴们自行实现,可以参考博主的 《Spring Security》专栏进一步学习
@RestController
@RequestMapping("/api/video")
public class VideoProgressController {
@Autowired
private VideoProgressService progressService;
// 保存或更新进度
@PostMapping("/progress")
public ResponseEntity<?> saveProgress(
@RequestBody ProgressRequest req,
@RequestHeader("X-User-Id") Long userId) {
progressService.saveOrUpdate(userId, req.getVideoId(), req.getCurrentTime());
return ResponseEntity.ok().build();
}
// 获取进度
@GetMapping("/progress")
public ResponseEntity<Double> getProgress(
@RequestParam String videoId,
@RequestHeader("X-User-Id") Long userId) {
Double time = progressService.getProgress(userId, videoId);
return ResponseEntity.ok(time != null ? time : 0.0);
}
// 请求 DTO
public static class ProgressRequest {
private String videoId;
private Double currentTime;
// getters/setters...
}
}
5.3 Service服务及Mapper
Mapper代码
@Mapper
public interface VideoProgressMapper {
@Select("SELECT * FROM video_progress WHERE user_id=#{userId} AND video_id=#{videoId}")
VideoProgress findByUserAndVideo(@Param("userId") Long userId, @Param("videoId") String videoId);
@Insert("INSERT INTO video_progress(user_id,video_id,watched_time) VALUES(#{userId},#{videoId},#{watchedTime}) " +
"ON DUPLICATE KEY UPDATE watched_time=#{watchedTime}, update_time=NOW()")
void upsert(VideoProgress record);
}
Service代码
@Service
public class VideoProgressService {
@Autowired
private VideoProgressMapper mapper;
public void saveOrUpdate(Long userId, String videoId, Double time) {
VideoProgress record = new VideoProgress(userId, videoId, time);
mapper.upsert(record);
}
public Double getProgress(Long userId, String videoId) {
VideoProgress rec = mapper.findByUserAndVideo(userId, videoId);
return rec != null ? rec.getWatchedTime() : null;
}
}
5.4. 前端调用示例
<video id="myVideo" controls>
<source src="movie.mp4" type="video/mp4">
</video>
<script>
const API_BASE = '/api/video';
const video = document.getElementById('myVideo');
const VIDEO_ID = 'movie-123';
const userId = 42; // 假设已登录并拿到 userId
const STORAGE_KEY = `video-progress-${VIDEO_ID}`;
// 恢复进度 初始化播放位置
async function loadProgress() {
const res = await fetch(`${API_BASE}/progress?videoId=${videoId}`, {
headers: { 'X-User-Id': userId }
});
const time = await res.json();
if (time > 0 && time < video.duration) {
video.currentTime = time;
}
}
// 进度记录函数
function saveProgress() {
fetch(`${API_BASE}/progress`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-User-Id': userId
},
body: JSON.stringify({ videoId, currentTime: video.currentTime })
});
}
}
// 事件监听
video.addEventListener('timeupdate', throttle(saveProgress, 5000));
video.addEventListener('pause', saveProgress);
video.addEventListener('ended', () => {
//TODO 后端删除API小伙伴们可自行实现
});
// 页面关闭前保存
window.addEventListener('beforeunload', saveProgress);
// 节流函数
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
func.apply(this, args);
lastCall = now;
}
};
}
</script>
6. 测试与优化
测试
模拟网络抖动、断网重连,确保进度及时更新
跨设备登录测试:在不同设备/浏览器登录同一账号,验证进度同步
优化建议
数据校验:后端对 currentTime 做合法性校验(不超出视频总时长)
批量提交:可改为用户退出时一次性提交最后进度,减少请求次数
缓存 & 重试:前端调用失败时缓存到 IndexedDB,下次自动重试
并发合并:后端可结合消息队列异步写库,减小请求延迟
不同规模平台的实施建议:
7. 结语
通过本文示例,相信小伙伴已掌握了从本地存储到后端持久化的完整视频续播实现方案。无论是单设备场景下的 localStorage
,还是支持多端同步的 Spring Boot + 数据库
方案,都能灵活应用到你的项目中。
希望这篇文章能帮助你打造更友好的视频观看体验,如果你在实践过程中有任何疑问或更好的扩展思路,欢迎在评论区留言,最后希望大家 一键三连 给博主一点点鼓励!
前端技术专栏回顾:
01【前端技术】 ES6 介绍及常用语法说明
02【前端技术】标签页通讯localStorage、BroadcastChannel、SharedWorker的技术详解
03 前端请求乱序问题分析与AbortController、async/await、Promise.all等解决方案
04 前端开发中深拷贝的循环引用问题:从问题复现到完美解决
05 前端AJAX请求上传下载进度监控指南详解与完整代码示例
06 TypeScript 进阶指南 - 使用泛型与keyof约束参数
07 前端实现视频文件动画帧图片提取全攻略 - 附完整代码样例
08 前端函数防抖(Debounce)完整讲解 - 从原理、应用到完整实现
09 JavaScript异步编程 Async/Await 使用详解:从原理到最佳实践
10 前端图片裁剪上传全流程详解:从预览到上传的完整流程
11 前端大文件分片上传详解 - Spring Boot 后端接口实现
12 前端实现图片防盗链技术详解 - 原理分析与SpringBoot解决方案