🌷 古之立大事者,不惟有超世之才,亦必有堅忍不拔之志
🎐 個人CSND主頁——Micro麥可樂的博客
🐥《Docker實操教程》專欄以最新的Centos版本為基礎進行Docker實操教程,入門到實戰
🌺《RabbitMQ》專欄19年編寫主要介紹使用JAVA開發RabbitMQ的系列教程,從基礎知識到項目實戰
🌸《設計模式》專欄以實際的生活場景為案例進行講解,讓大家對設計模式有一個更清晰的理解
🌛《開源項目》本專欄主要介紹目前熱門的開源項目,帶大家快速了解并輕松上手使用
🍎 《前端技術》專欄以實戰為主介紹日常開發中前端應用的一些功能以及技巧,均附有完整的代碼示例
?《開發技巧》本專欄包含了各種系統的設計原理以及注意事項,并分享一些日常開發的功能小技巧
💕《Jenkins實戰》專欄主要介紹Jenkins+Docker的實戰教程,讓你快速掌握項目CI/CD,是2024年最新的實戰教程
🌞《Spring Boot》專欄主要介紹我們日常工作項目中經常應用到的功能以及技巧,代碼樣例完整
👍《Spring Security》專欄中我們將逐步深入Spring Security的各個技術細節,帶你從入門到精通,全面掌握這一安全技術
如果文章能夠給大家帶來一定的幫助!歡迎關注、評論互動~
視頻續播功能實現 - 斷點續看從前端到 Spring Boot 后端
- 1. 前言
- 2. 為什么要做視頻續播
- 3. 續播功能原理
- 3.1 常見的續播記錄系統架構
- 3.2 常見的觸發記錄時機
- 4. 純前端實現方案
- 4.1 基礎實現代碼
- 4.2 增強版本地存儲
- 5. 后端(Spring Boot)實現
- 5.1 數據庫表
- 5.2 后端接口
- 5.3 Service服務及Mapper
- 5.4. 前端調用示例
- 6. 測試與優化
- 7. 結語
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 {@Autowiredprivate 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);}// 請求 DTOpublic 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 {@Autowiredprivate 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解決方案