前言

在现代视频网站中,断点续播功能已经成为用户体验的基础标配。想象一下,当你在手机上看到一部电影的精彩片段时突然有事离开,几小时后在电脑上打开同一部电影,系统会贴心地询问”是否从上次观看的位置继续播放?”这看似简单的功能背后,实际上涉及了复杂的技术实现:实时的播放进度追踪、跨设备的数据同步、智能的恢复策略、以及海量用户数据的高效存储。就像一个贴心的书签系统,不仅要记住你读到了哪一页,还要在你换了不同的书、不同的阅读设备时,都能准确地帮你找到上次的阅读位置。本文将深入分析视频网站断点续播功能的技术实现原理,包括播放进度的实时记录、数据存储策略、跨设备同步机制等核心技术。

一、功能需求与技术挑战

(一)断点续播功能需求分析

核心功能需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
断点续播功能需求:

基础功能:
├── 播放进度记录:实时记录用户观看进度
├── 进度恢复:下次播放时从上次位置继续
├── 跨设备同步:不同设备间的进度同步
├── 多视频管理:同时记录多个视频的进度
└── 智能提醒:询问是否从断点继续播放

高级功能:
├── 智能跳过:自动跳过片头片尾
├── 章节记忆:记录章节观看状态
├── 观看历史:完整的观看历史记录
├── 推荐优化:基于观看进度的内容推荐
└── 数据分析:用户观看行为分析

技术要求:
├── 实时性:播放进度的实时更新和保存
├── 准确性:精确到秒级的进度记录
├── 可靠性:数据不丢失,故障恢复能力
├── 性能:高并发下的快速响应
└── 一致性:跨设备数据的强一致性

技术挑战分析:

  1. 海量数据:亿级用户×万级视频=万亿级进度记录
  2. 实时更新:每秒数百万次的进度更新请求
  3. 跨设备同步:多设备间的数据一致性保证
  4. 存储优化:海量小数据的高效存储和查询
  5. 用户体验:毫秒级的进度恢复响应时间

(二)系统架构设计

整体架构概览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
// 视频断点续播系统架构
class VideoProgressSystem {
constructor() {
// 核心组件
this.progressTracker = new ProgressTracker(); // 进度追踪器
this.storageManager = new StorageManager(); // 存储管理器
this.syncService = new SyncService(); // 同步服务
this.recoveryService = new RecoveryService(); // 恢复服务
this.analyticsService = new AnalyticsService(); // 分析服务

// 缓存和优化
this.cacheManager = new CacheManager(); // 缓存管理
this.batchProcessor = new BatchProcessor(); // 批处理器
this.compressionService = new CompressionService(); // 压缩服务

this.initializeSystem();
}

// 系统初始化
initializeSystem() {
this.setupEventHandlers(); // 设置事件处理器
this.startBackgroundTasks(); // 启动后台任务
this.initializeCache(); // 初始化缓存
}

// 核心功能:记录播放进度
async recordProgress(userId, videoId, currentTime, totalDuration, deviceInfo = {}) {
try {
// 1. 构建进度记录
const progressRecord = {
userId: userId,
videoId: videoId,
currentTime: Math.floor(currentTime), // 精确到秒
totalDuration: Math.floor(totalDuration),
percentage: (currentTime / totalDuration * 100).toFixed(2),
timestamp: Date.now(),
deviceId: deviceInfo.deviceId || 'unknown',
deviceType: deviceInfo.deviceType || 'web',
sessionId: deviceInfo.sessionId,
userAgent: deviceInfo.userAgent
};

// 2. 数据验证
if (!this.validateProgressRecord(progressRecord)) {
console.warn('无效的进度记录:', progressRecord);
return false;
}

// 3. 更新本地缓存(立即响应)
await this.cacheManager.updateProgressCache(userId, videoId, progressRecord);

// 4. 异步批量写入数据库
await this.batchProcessor.addProgressUpdate(progressRecord);

// 5. 触发跨设备同步
await this.syncService.notifyProgressUpdate(userId, videoId, progressRecord);

// 6. 更新用户观看统计
this.analyticsService.updateWatchingStats(userId, videoId, progressRecord);

return true;

} catch (error) {
console.error('记录播放进度失败:', error);
return false;
}
}

// 核心功能:获取播放进度
async getProgress(userId, videoId) {
try {
// 1. 优先从缓存获取
let progress = await this.cacheManager.getProgressFromCache(userId, videoId);

if (progress && this.isCacheValid(progress)) {
return this.formatProgressResponse(progress);
}

// 2. 从数据库获取
progress = await this.storageManager.getProgress(userId, videoId);

if (progress) {
// 更新缓存
await this.cacheManager.updateProgressCache(userId, videoId, progress);
return this.formatProgressResponse(progress);
}

// 3. 没有进度记录
return {
hasProgress: false,
currentTime: 0,
percentage: 0,
lastWatchTime: null
};

} catch (error) {
console.error('获取播放进度失败:', error);
return { hasProgress: false, currentTime: 0, percentage: 0 };
}
}

// 核心功能:恢复播放进度
async resumePlayback(userId, videoId, deviceInfo = {}) {
try {
// 1. 获取最新进度
const progress = await this.getProgress(userId, videoId);

if (!progress.hasProgress) {
return {
shouldResume: false,
resumeTime: 0,
message: '没有观看记录'
};
}

// 2. 智能恢复策略判断
const resumeDecision = await this.recoveryService.shouldResumePlayback(
progress,
deviceInfo
);

if (resumeDecision.shouldResume) {
// 3. 记录恢复事件
await this.analyticsService.recordResumeEvent(userId, videoId, {
resumeTime: progress.currentTime,
lastDevice: progress.deviceType,
currentDevice: deviceInfo.deviceType,
timeSinceLastWatch: Date.now() - progress.timestamp
});

return {
shouldResume: true,
resumeTime: progress.currentTime,
percentage: progress.percentage,
lastWatchTime: new Date(progress.timestamp),
message: `从 ${this.formatTime(progress.currentTime)} 继续播放`
};
} else {
return {
shouldResume: false,
resumeTime: 0,
reason: resumeDecision.reason,
message: resumeDecision.message
};
}

} catch (error) {
console.error('恢复播放进度失败:', error);
return { shouldResume: false, resumeTime: 0 };
}
}

// 批量获取用户的观看进度
async getUserProgressBatch(userId, videoIds) {
try {
// 1. 批量从缓存获取
const cacheResults = await this.cacheManager.batchGetProgress(userId, videoIds);
const cachedVideoIds = Object.keys(cacheResults);
const missedVideoIds = videoIds.filter(id => !cachedVideoIds.includes(id));

// 2. 从数据库获取缓存未命中的数据
let dbResults = {};
if (missedVideoIds.length > 0) {
dbResults = await this.storageManager.batchGetProgress(userId, missedVideoIds);

// 更新缓存
await this.cacheManager.batchUpdateProgressCache(userId, dbResults);
}

// 3. 合并结果
const allResults = { ...cacheResults, ...dbResults };

// 4. 格式化响应
const formattedResults = {};
videoIds.forEach(videoId => {
const progress = allResults[videoId];
formattedResults[videoId] = progress ?
this.formatProgressResponse(progress) :
{ hasProgress: false, currentTime: 0, percentage: 0 };
});

return formattedResults;

} catch (error) {
console.error('批量获取用户进度失败:', error);
return {};
}
}

// 清理过期进度记录
async cleanupExpiredProgress(userId, options = {}) {
try {
const expiredThreshold = options.expiredDays || 90; // 默认90天
const expiredTime = Date.now() - (expiredThreshold * 24 * 60 * 60 * 1000);

// 1. 获取过期的进度记录
const expiredRecords = await this.storageManager.getExpiredProgress(
userId,
expiredTime
);

if (expiredRecords.length === 0) {
return { cleaned: 0, message: '没有过期记录' };
}

// 2. 备份重要记录(观看完成的视频)
const importantRecords = expiredRecords.filter(record =>
record.percentage > 90 || record.isCompleted
);

if (importantRecords.length > 0) {
await this.storageManager.archiveProgressRecords(importantRecords);
}

// 3. 删除过期记录
const deletedCount = await this.storageManager.deleteExpiredProgress(
userId,
expiredTime
);

// 4. 清理相关缓存
await this.cacheManager.cleanupUserProgressCache(userId);

return {
cleaned: deletedCount,
archived: importantRecords.length,
message: `清理了${deletedCount}条过期记录,归档了${importantRecords.length}条重要记录`
};

} catch (error) {
console.error('清理过期进度记录失败:', error);
throw error;
}
}

// 数据验证
validateProgressRecord(record) {
// 基础字段验证
if (!record.userId || !record.videoId) {
return false;
}

// 时间验证
if (record.currentTime < 0 || record.totalDuration <= 0) {
return false;
}

if (record.currentTime > record.totalDuration) {
return false;
}

// 百分比验证
const calculatedPercentage = (record.currentTime / record.totalDuration * 100);
if (Math.abs(parseFloat(record.percentage) - calculatedPercentage) > 1) {
return false;
}

return true;
}

// 格式化进度响应
formatProgressResponse(progress) {
return {
hasProgress: true,
currentTime: progress.currentTime,
totalDuration: progress.totalDuration,
percentage: parseFloat(progress.percentage),
lastWatchTime: new Date(progress.timestamp),
deviceType: progress.deviceType,
isCompleted: progress.percentage > 95,
formattedTime: this.formatTime(progress.currentTime),
formattedDuration: this.formatTime(progress.totalDuration)
};
}

// 格式化时间显示
formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;

if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
}

// 检查缓存有效性
isCacheValid(cachedProgress) {
const cacheTimeout = 5 * 60 * 1000; // 5分钟缓存超时
return (Date.now() - cachedProgress.cacheTime) < cacheTimeout;
}

// 设置事件处理器
setupEventHandlers() {
// 用户登录事件
this.on('user.login', async (event) => {
await this.handleUserLogin(event.userId, event.deviceInfo);
});

// 用户登出事件
this.on('user.logout', async (event) => {
await this.handleUserLogout(event.userId, event.deviceInfo);
});

// 视频播放结束事件
this.on('video.completed', async (event) => {
await this.handleVideoCompleted(event.userId, event.videoId);
});
}

// 启动后台任务
startBackgroundTasks() {
// 定期批量写入数据库
setInterval(async () => {
await this.batchProcessor.flushPendingUpdates();
}, 10000); // 每10秒执行一次

// 定期清理过期缓存
setInterval(async () => {
await this.cacheManager.cleanupExpiredCache();
}, 300000); // 每5分钟执行一次

// 定期同步跨设备数据
setInterval(async () => {
await this.syncService.syncPendingUpdates();
}, 30000); // 每30秒执行一次
}

// 初始化缓存
async initializeCache() {
await this.cacheManager.initialize();
console.log('播放进度缓存系统初始化完成');
}
}

二、数据存储与实时追踪

(一)播放进度存储设计

数据库表结构设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
-- 用户播放进度主表
CREATE TABLE user_video_progress (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL, -- 用户ID
video_id BIGINT NOT NULL, -- 视频ID
current_time INT NOT NULL DEFAULT 0, -- 当前播放时间(秒)
total_duration INT NOT NULL DEFAULT 0, -- 视频总时长(秒)
progress_percentage DECIMAL(5,2) NOT NULL DEFAULT 0.00, -- 播放进度百分比
device_id VARCHAR(64), -- 设备ID
device_type ENUM('web', 'mobile', 'tablet', 'tv', 'desktop') DEFAULT 'web',
session_id VARCHAR(64), -- 会话ID
user_agent TEXT, -- 用户代理信息
is_completed BOOLEAN DEFAULT FALSE, -- 是否播放完成
last_update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

-- 唯一约束:每个用户对每个视频只有一条最新记录
UNIQUE KEY uk_user_video (user_id, video_id),

-- 核心查询索引
INDEX idx_user_update_time (user_id, last_update_time DESC),
INDEX idx_video_progress (video_id, progress_percentage),
INDEX idx_device_user (device_id, user_id),

-- 分区策略:按用户ID哈希分区,便于水平扩展
PARTITION BY HASH(user_id) PARTITIONS 32
) ENGINE=InnoDB;

-- 播放进度历史表(用于数据分析和恢复)
CREATE TABLE user_video_progress_history (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
video_id BIGINT NOT NULL,
current_time INT NOT NULL,
total_duration INT NOT NULL,
progress_percentage DECIMAL(5,2) NOT NULL,
device_id VARCHAR(64),
device_type VARCHAR(20),
session_id VARCHAR(64),
action_type ENUM('play', 'pause', 'seek', 'complete') DEFAULT 'play',
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

-- 查询索引
INDEX idx_user_video_time (user_id, video_id, timestamp DESC),
INDEX idx_timestamp (timestamp),

-- 按时间分区,便于历史数据清理
PARTITION BY RANGE (UNIX_TIMESTAMP(timestamp)) (
PARTITION p202501 VALUES LESS THAN (UNIX_TIMESTAMP('2025-02-01')),
PARTITION p202502 VALUES LESS THAN (UNIX_TIMESTAMP('2025-03-01')),
-- 自动分区管理
PARTITION p_future VALUES LESS THAN MAXVALUE
)
) ENGINE=InnoDB;

-- 用户观看统计表
CREATE TABLE user_watching_stats (
user_id BIGINT PRIMARY KEY,
total_watch_time BIGINT DEFAULT 0, -- 总观看时长(秒)
total_videos_watched INT DEFAULT 0, -- 观看视频总数
total_videos_completed INT DEFAULT 0, -- 完整观看视频数
avg_completion_rate DECIMAL(5,2) DEFAULT 0.00, -- 平均完成率
favorite_device_type VARCHAR(20), -- 最常用设备类型
last_watch_time TIMESTAMP, -- 最后观看时间
updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;

存储管理器实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
// 存储管理器:处理播放进度的数据库操作
class StorageManager {
constructor() {
this.dbPool = this.initializeDBPool();
this.redisClient = this.initializeRedis();
this.compressionEnabled = true;
}

// 初始化数据库连接池
initializeDBPool() {
const mysql = require('mysql2/promise');

return mysql.createPool({
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'video_platform',
connectionLimit: 100,
acquireTimeout: 60000,
timeout: 60000,
reconnect: true
});
}

// 保存播放进度
async saveProgress(progressRecord) {
const connection = await this.dbPool.getConnection();

try {
await connection.beginTransaction();

// 1. 更新主表记录
const updateMainSql = `
INSERT INTO user_video_progress
(user_id, video_id, current_time, total_duration, progress_percentage,
device_id, device_type, session_id, user_agent, is_completed)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
current_time = VALUES(current_time),
total_duration = VALUES(total_duration),
progress_percentage = VALUES(progress_percentage),
device_id = VALUES(device_id),
device_type = VALUES(device_type),
session_id = VALUES(session_id),
user_agent = VALUES(user_agent),
is_completed = VALUES(is_completed),
last_update_time = CURRENT_TIMESTAMP
`;

await connection.execute(updateMainSql, [
progressRecord.userId,
progressRecord.videoId,
progressRecord.currentTime,
progressRecord.totalDuration,
progressRecord.percentage,
progressRecord.deviceId,
progressRecord.deviceType,
progressRecord.sessionId,
progressRecord.userAgent,
progressRecord.percentage > 95 ? 1 : 0
]);

// 2. 插入历史记录
const insertHistorySql = `
INSERT INTO user_video_progress_history
(user_id, video_id, current_time, total_duration, progress_percentage,
device_id, device_type, session_id, action_type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`;

await connection.execute(insertHistorySql, [
progressRecord.userId,
progressRecord.videoId,
progressRecord.currentTime,
progressRecord.totalDuration,
progressRecord.percentage,
progressRecord.deviceId,
progressRecord.deviceType,
progressRecord.sessionId,
progressRecord.actionType || 'play'
]);

// 3. 更新用户统计
await this.updateUserStats(connection, progressRecord);

await connection.commit();
return true;

} catch (error) {
await connection.rollback();
console.error('保存播放进度失败:', error);
throw error;
} finally {
connection.release();
}
}

// 获取播放进度
async getProgress(userId, videoId) {
try {
const sql = `
SELECT user_id, video_id, current_time, total_duration, progress_percentage,
device_id, device_type, session_id, is_completed, last_update_time
FROM user_video_progress
WHERE user_id = ? AND video_id = ?
`;

const [rows] = await this.dbPool.execute(sql, [userId, videoId]);

if (rows.length > 0) {
const progress = rows[0];
return {
userId: progress.user_id,
videoId: progress.video_id,
currentTime: progress.current_time,
totalDuration: progress.total_duration,
percentage: parseFloat(progress.progress_percentage),
deviceId: progress.device_id,
deviceType: progress.device_type,
sessionId: progress.session_id,
isCompleted: progress.is_completed,
timestamp: progress.last_update_time.getTime()
};
}

return null;

} catch (error) {
console.error('获取播放进度失败:', error);
throw error;
}
}

// 批量获取播放进度
async batchGetProgress(userId, videoIds) {
try {
if (videoIds.length === 0) return {};

const placeholders = videoIds.map(() => '?').join(',');
const sql = `
SELECT user_id, video_id, current_time, total_duration, progress_percentage,
device_id, device_type, session_id, is_completed, last_update_time
FROM user_video_progress
WHERE user_id = ? AND video_id IN (${placeholders})
`;

const [rows] = await this.dbPool.execute(sql, [userId, ...videoIds]);

const results = {};
rows.forEach(row => {
results[row.video_id] = {
userId: row.user_id,
videoId: row.video_id,
currentTime: row.current_time,
totalDuration: row.total_duration,
percentage: parseFloat(row.progress_percentage),
deviceId: row.device_id,
deviceType: row.device_type,
sessionId: row.session_id,
isCompleted: row.is_completed,
timestamp: row.last_update_time.getTime()
};
});

return results;

} catch (error) {
console.error('批量获取播放进度失败:', error);
throw error;
}
}

// 获取用户最近观看记录
async getUserRecentProgress(userId, limit = 50) {
try {
const sql = `
SELECT uvp.video_id, uvp.current_time, uvp.total_duration,
uvp.progress_percentage, uvp.device_type, uvp.is_completed,
uvp.last_update_time, v.title, v.thumbnail_url, v.duration
FROM user_video_progress uvp
LEFT JOIN videos v ON uvp.video_id = v.id
WHERE uvp.user_id = ?
ORDER BY uvp.last_update_time DESC
LIMIT ?
`;

const [rows] = await this.dbPool.execute(sql, [userId, limit]);

return rows.map(row => ({
videoId: row.video_id,
title: row.title,
thumbnailUrl: row.thumbnail_url,
currentTime: row.current_time,
totalDuration: row.total_duration || row.duration,
percentage: parseFloat(row.progress_percentage),
deviceType: row.device_type,
isCompleted: row.is_completed,
lastWatchTime: row.last_update_time
}));

} catch (error) {
console.error('获取用户最近观看记录失败:', error);
throw error;
}
}

// 更新用户统计信息
async updateUserStats(connection, progressRecord) {
try {
const sql = `
INSERT INTO user_watching_stats
(user_id, total_watch_time, total_videos_watched, total_videos_completed, last_watch_time)
VALUES (?, ?, 1, ?, NOW())
ON DUPLICATE KEY UPDATE
total_watch_time = total_watch_time + ?,
total_videos_watched = total_videos_watched + 1,
total_videos_completed = total_videos_completed + ?,
last_watch_time = NOW()
`;

const watchTimeIncrement = 10; // 假设每次更新增加10秒观看时长
const completedIncrement = progressRecord.percentage > 95 ? 1 : 0;

await connection.execute(sql, [
progressRecord.userId,
watchTimeIncrement,
completedIncrement,
watchTimeIncrement,
completedIncrement
]);

} catch (error) {
console.error('更新用户统计失败:', error);
// 统计更新失败不影响主流程
}
}

// 删除过期进度记录
async deleteExpiredProgress(userId, expiredTime) {
try {
const sql = `
DELETE FROM user_video_progress
WHERE user_id = ? AND last_update_time < FROM_UNIXTIME(?)
AND progress_percentage < 90
`;

const [result] = await this.dbPool.execute(sql, [userId, expiredTime / 1000]);
return result.affectedRows;

} catch (error) {
console.error('删除过期进度记录失败:', error);
throw error;
}
}

// 归档重要记录
async archiveProgressRecords(records) {
try {
// 将重要记录移动到归档表
const archiveSql = `
INSERT INTO user_video_progress_archive
(user_id, video_id, current_time, total_duration, progress_percentage,
device_type, is_completed, archived_time)
VALUES ?
`;

const values = records.map(record => [
record.userId,
record.videoId,
record.currentTime,
record.totalDuration,
record.percentage,
record.deviceType,
record.isCompleted,
new Date()
]);

await this.dbPool.query(archiveSql, [values]);

} catch (error) {
console.error('归档进度记录失败:', error);
throw error;
}
}

// 获取过期记录
async getExpiredProgress(userId, expiredTime) {
try {
const sql = `
SELECT user_id, video_id, current_time, total_duration, progress_percentage,
device_type, is_completed, last_update_time
FROM user_video_progress
WHERE user_id = ? AND last_update_time < FROM_UNIXTIME(?)
`;

const [rows] = await this.dbPool.execute(sql, [userId, expiredTime / 1000]);

return rows.map(row => ({
userId: row.user_id,
videoId: row.video_id,
currentTime: row.current_time,
totalDuration: row.total_duration,
percentage: parseFloat(row.progress_percentage),
deviceType: row.device_type,
isCompleted: row.is_completed,
timestamp: row.last_update_time.getTime()
}));

} catch (error) {
console.error('获取过期记录失败:', error);
throw error;
}
}
}

(二)实时进度追踪器

ProgressTracker核心实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
// 播放进度实时追踪器
class ProgressTracker {
constructor() {
this.trackingIntervals = new Map(); // 存储追踪定时器
this.progressBuffer = new Map(); // 进度缓冲区
this.trackingConfig = {
updateInterval: 10000, // 10秒更新一次
bufferSize: 100, // 缓冲区大小
minProgressChange: 5, // 最小进度变化(秒)
maxBatchSize: 50 // 最大批处理大小
};

this.eventEmitter = new EventEmitter();
this.isTracking = false;
}

// 开始追踪播放进度
startTracking(userId, videoId, playerInstance, options = {}) {
try {
const trackingKey = `${userId}:${videoId}`;

// 如果已经在追踪,先停止
if (this.trackingIntervals.has(trackingKey)) {
this.stopTracking(userId, videoId);
}

// 初始化追踪状态
const trackingState = {
userId: userId,
videoId: videoId,
player: playerInstance,
lastRecordedTime: 0,
lastUpdateTime: Date.now(),
sessionId: options.sessionId || this.generateSessionId(),
deviceInfo: options.deviceInfo || {},
isPlaying: false,
totalDuration: 0
};

// 设置定时追踪
const intervalId = setInterval(async () => {
await this.trackProgress(trackingState);
}, this.trackingConfig.updateInterval);

this.trackingIntervals.set(trackingKey, {
intervalId: intervalId,
state: trackingState
});

// 监听播放器事件
this.setupPlayerEventListeners(trackingState);

console.log(`开始追踪播放进度: ${trackingKey}`);
return true;

} catch (error) {
console.error('开始追踪播放进度失败:', error);
return false;
}
}

// 停止追踪播放进度
stopTracking(userId, videoId) {
const trackingKey = `${userId}:${videoId}`;
const tracking = this.trackingIntervals.get(trackingKey);

if (tracking) {
// 清除定时器
clearInterval(tracking.intervalId);

// 最后一次记录进度
this.trackProgress(tracking.state, true);

// 清理资源
this.trackingIntervals.delete(trackingKey);
this.progressBuffer.delete(trackingKey);

console.log(`停止追踪播放进度: ${trackingKey}`);
return true;
}

return false;
}

// 追踪进度的核心方法
async trackProgress(trackingState, isFinalUpdate = false) {
try {
const player = trackingState.player;

// 获取当前播放状态
const currentTime = Math.floor(player.getCurrentTime() || 0);
const totalDuration = Math.floor(player.getDuration() || 0);
const isPlaying = player.isPlaying();
const isPaused = player.isPaused();

// 更新追踪状态
trackingState.isPlaying = isPlaying;
trackingState.totalDuration = totalDuration;

// 检查是否需要记录进度
const shouldRecord = this.shouldRecordProgress(
trackingState,
currentTime,
isFinalUpdate
);

if (shouldRecord) {
// 构建进度记录
const progressRecord = {
userId: trackingState.userId,
videoId: trackingState.videoId,
currentTime: currentTime,
totalDuration: totalDuration,
percentage: totalDuration > 0 ? (currentTime / totalDuration * 100).toFixed(2) : 0,
timestamp: Date.now(),
sessionId: trackingState.sessionId,
deviceInfo: trackingState.deviceInfo,
actionType: this.determineActionType(trackingState, isPlaying, isPaused),
isPlaying: isPlaying
};

// 记录进度
await this.recordProgress(progressRecord);

// 更新最后记录时间
trackingState.lastRecordedTime = currentTime;
trackingState.lastUpdateTime = Date.now();

// 触发进度更新事件
this.eventEmitter.emit('progress.updated', progressRecord);
}

} catch (error) {
console.error('追踪播放进度失败:', error);
}
}

// 判断是否应该记录进度
shouldRecordProgress(trackingState, currentTime, isFinalUpdate) {
// 强制更新(如暂停、结束等)
if (isFinalUpdate) {
return true;
}

// 检查时间变化
const timeChange = Math.abs(currentTime - trackingState.lastRecordedTime);
if (timeChange < this.trackingConfig.minProgressChange) {
return false;
}

// 检查更新间隔
const timeSinceLastUpdate = Date.now() - trackingState.lastUpdateTime;
if (timeSinceLastUpdate < this.trackingConfig.updateInterval) {
return false;
}

// 检查是否在播放
if (!trackingState.isPlaying && !isFinalUpdate) {
return false;
}

return true;
}

// 确定动作类型
determineActionType(trackingState, isPlaying, isPaused) {
if (isPaused) {
return 'pause';
} else if (isPlaying) {
return 'play';
} else if (trackingState.currentTime >= trackingState.totalDuration * 0.95) {
return 'complete';
} else {
return 'seek';
}
}

// 设置播放器事件监听
setupPlayerEventListeners(trackingState) {
const player = trackingState.player;

// 播放事件
player.on('play', () => {
trackingState.isPlaying = true;
this.trackProgress(trackingState);
});

// 暂停事件
player.on('pause', () => {
trackingState.isPlaying = false;
this.trackProgress(trackingState, true);
});

// 跳转事件
player.on('seeked', () => {
this.trackProgress(trackingState, true);
});

// 播放结束事件
player.on('ended', () => {
trackingState.isPlaying = false;
this.trackProgress(trackingState, true);

// 触发完成事件
this.eventEmitter.emit('video.completed', {
userId: trackingState.userId,
videoId: trackingState.videoId,
totalDuration: trackingState.totalDuration
});
});

// 错误事件
player.on('error', (error) => {
console.error('播放器错误:', error);
this.trackProgress(trackingState, true);
});
}

// 记录进度(异步处理)
async recordProgress(progressRecord) {
try {
// 添加到缓冲区
const bufferKey = `${progressRecord.userId}:${progressRecord.videoId}`;

if (!this.progressBuffer.has(bufferKey)) {
this.progressBuffer.set(bufferKey, []);
}

const buffer = this.progressBuffer.get(bufferKey);
buffer.push(progressRecord);

// 如果缓冲区满了,触发批量处理
if (buffer.length >= this.trackingConfig.bufferSize) {
await this.flushProgressBuffer(bufferKey);
}

} catch (error) {
console.error('记录进度失败:', error);
}
}

// 刷新进度缓冲区
async flushProgressBuffer(bufferKey) {
try {
const buffer = this.progressBuffer.get(bufferKey);

if (!buffer || buffer.length === 0) {
return;
}

// 获取最新的进度记录
const latestProgress = buffer[buffer.length - 1];

// 发送到主系统进行处理
await this.sendProgressUpdate(latestProgress);

// 清空缓冲区
this.progressBuffer.set(bufferKey, []);

} catch (error) {
console.error('刷新进度缓冲区失败:', error);
}
}

// 发送进度更新
async sendProgressUpdate(progressRecord) {
try {
// 这里应该调用主系统的recordProgress方法
// 为了演示,我们使用事件发射
this.eventEmitter.emit('progress.record', progressRecord);

} catch (error) {
console.error('发送进度更新失败:', error);
}
}

// 批量刷新所有缓冲区
async flushAllBuffers() {
try {
const flushPromises = [];

for (const bufferKey of this.progressBuffer.keys()) {
flushPromises.push(this.flushProgressBuffer(bufferKey));
}

await Promise.all(flushPromises);

} catch (error) {
console.error('批量刷新缓冲区失败:', error);
}
}

// 获取当前追踪状态
getTrackingStatus() {
const status = {
activeTrackings: this.trackingIntervals.size,
bufferSizes: {},
totalBufferedRecords: 0
};

for (const [key, buffer] of this.progressBuffer.entries()) {
status.bufferSizes[key] = buffer.length;
status.totalBufferedRecords += buffer.length;
}

return status;
}

// 生成会话ID
generateSessionId() {
return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}

// 清理资源
async cleanup() {
try {
// 停止所有追踪
for (const [key] of this.trackingIntervals.entries()) {
const [userId, videoId] = key.split(':');
this.stopTracking(userId, videoId);
}

// 刷新所有缓冲区
await this.flushAllBuffers();

// 清理数据
this.trackingIntervals.clear();
this.progressBuffer.clear();

console.log('播放进度追踪器清理完成');

} catch (error) {
console.error('清理播放进度追踪器失败:', error);
}
}
}

三、跨设备同步与智能恢复

(一)跨设备同步服务

SyncService实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
// 跨设备同步服务
class SyncService {
constructor() {
this.syncQueue = new Map(); // 同步队列
this.deviceConnections = new Map(); // 设备连接映射
this.conflictResolver = new ConflictResolver();
this.syncConfig = {
syncInterval: 30000, // 30秒同步间隔
maxRetries: 3, // 最大重试次数
conflictResolution: 'latest_wins' // 冲突解决策略
};

this.initializeSync();
}

// 初始化同步服务
initializeSync() {
// 启动定期同步任务
setInterval(async () => {
await this.processSyncQueue();
}, this.syncConfig.syncInterval);

// 设置WebSocket服务器用于实时同步
this.setupWebSocketServer();
}

// 通知进度更新
async notifyProgressUpdate(userId, videoId, progressRecord) {
try {
// 1. 添加到同步队列
const syncKey = `${userId}:${videoId}`;

if (!this.syncQueue.has(syncKey)) {
this.syncQueue.set(syncKey, []);
}

this.syncQueue.get(syncKey).push({
...progressRecord,
syncTimestamp: Date.now(),
retryCount: 0
});

// 2. 实时推送给用户的其他设备
await this.pushToUserDevices(userId, videoId, progressRecord);

} catch (error) {
console.error('通知进度更新失败:', error);
}
}

// 推送到用户的其他设备
async pushToUserDevices(userId, videoId, progressRecord) {
try {
const userDevices = this.deviceConnections.get(userId) || new Map();
const currentDeviceId = progressRecord.deviceId;

// 推送给除当前设备外的所有设备
for (const [deviceId, connection] of userDevices.entries()) {
if (deviceId !== currentDeviceId && connection.isConnected) {
try {
await this.sendProgressUpdate(connection, {
type: 'progress_update',
videoId: videoId,
progress: {
currentTime: progressRecord.currentTime,
percentage: progressRecord.percentage,
timestamp: progressRecord.timestamp,
sourceDevice: progressRecord.deviceType
}
});
} catch (error) {
console.error(`推送到设备${deviceId}失败:`, error);
// 标记连接为断开
connection.isConnected = false;
}
}
}

} catch (error) {
console.error('推送到用户设备失败:', error);
}
}

// 处理同步队列
async processSyncQueue() {
try {
const processPromises = [];

for (const [syncKey, updates] of this.syncQueue.entries()) {
if (updates.length > 0) {
processPromises.push(this.processSyncUpdates(syncKey, updates));
}
}

await Promise.allSettled(processPromises);

} catch (error) {
console.error('处理同步队列失败:', error);
}
}

// 处理单个同步更新
async processSyncUpdates(syncKey, updates) {
try {
const [userId, videoId] = syncKey.split(':');

// 获取最新的更新记录
const latestUpdate = updates[updates.length - 1];

// 检查是否存在冲突
const hasConflict = await this.checkForConflicts(userId, videoId, latestUpdate);

if (hasConflict) {
// 解决冲突
const resolvedUpdate = await this.conflictResolver.resolve(
userId,
videoId,
latestUpdate
);

if (resolvedUpdate) {
await this.applySyncUpdate(userId, videoId, resolvedUpdate);
}
} else {
// 直接应用更新
await this.applySyncUpdate(userId, videoId, latestUpdate);
}

// 清理已处理的更新
this.syncQueue.set(syncKey, []);

} catch (error) {
console.error('处理同步更新失败:', error);

// 重试逻辑
updates.forEach(update => {
update.retryCount = (update.retryCount || 0) + 1;
});

// 过滤掉超过最大重试次数的更新
const retryableUpdates = updates.filter(
update => update.retryCount < this.syncConfig.maxRetries
);

this.syncQueue.set(syncKey, retryableUpdates);
}
}

// 检查冲突
async checkForConflicts(userId, videoId, update) {
try {
// 获取数据库中的最新记录
const dbProgress = await this.getLatestProgress(userId, videoId);

if (!dbProgress) {
return false; // 没有现有记录,无冲突
}

// 检查时间戳冲突
const timeDiff = Math.abs(update.timestamp - dbProgress.timestamp);
const progressDiff = Math.abs(update.currentTime - dbProgress.currentTime);

// 如果时间差小于30秒但进度差大于30秒,可能存在冲突
return timeDiff < 30000 && progressDiff > 30;

} catch (error) {
console.error('检查冲突失败:', error);
return false;
}
}

// 应用同步更新
async applySyncUpdate(userId, videoId, update) {
try {
// 这里应该调用存储管理器保存进度
// 为了演示,我们发出事件
this.emit('sync.update', {
userId: userId,
videoId: videoId,
progress: update
});

} catch (error) {
console.error('应用同步更新失败:', error);
throw error;
}
}

// 设置WebSocket服务器
setupWebSocketServer() {
const WebSocket = require('ws');

this.wss = new WebSocket.Server({
port: 8081,
path: '/sync'
});

this.wss.on('connection', (ws, request) => {
this.handleDeviceConnection(ws, request);
});
}

// 处理设备连接
handleDeviceConnection(ws, request) {
try {
// 从请求中提取用户和设备信息
const url = new URL(request.url, 'http://localhost');
const userId = url.searchParams.get('userId');
const deviceId = url.searchParams.get('deviceId');

if (!userId || !deviceId) {
ws.close(1008, 'Missing userId or deviceId');
return;
}

// 注册设备连接
if (!this.deviceConnections.has(userId)) {
this.deviceConnections.set(userId, new Map());
}

const userDevices = this.deviceConnections.get(userId);
userDevices.set(deviceId, {
ws: ws,
isConnected: true,
lastHeartbeat: Date.now(),
deviceInfo: {
userAgent: request.headers['user-agent'],
ip: request.connection.remoteAddress
}
});

// 设置消息处理
ws.on('message', (data) => {
this.handleDeviceMessage(userId, deviceId, data);
});

// 设置连接关闭处理
ws.on('close', () => {
this.handleDeviceDisconnection(userId, deviceId);
});

// 发送连接确认
ws.send(JSON.stringify({
type: 'connection_established',
userId: userId,
deviceId: deviceId
}));

console.log(`设备连接建立: ${userId}:${deviceId}`);

} catch (error) {
console.error('处理设备连接失败:', error);
ws.close(1011, 'Server error');
}
}

// 处理设备消息
handleDeviceMessage(userId, deviceId, data) {
try {
const message = JSON.parse(data.toString());

switch (message.type) {
case 'heartbeat':
this.handleHeartbeat(userId, deviceId);
break;

case 'progress_sync':
this.handleProgressSync(userId, deviceId, message.data);
break;

case 'request_sync':
this.handleSyncRequest(userId, deviceId, message.data);
break;

default:
console.warn('未知消息类型:', message.type);
}

} catch (error) {
console.error('处理设备消息失败:', error);
}
}

// 处理心跳
handleHeartbeat(userId, deviceId) {
const userDevices = this.deviceConnections.get(userId);
if (userDevices && userDevices.has(deviceId)) {
const connection = userDevices.get(deviceId);
connection.lastHeartbeat = Date.now();
connection.isConnected = true;
}
}

// 发送进度更新到设备
async sendProgressUpdate(connection, data) {
return new Promise((resolve, reject) => {
if (connection.ws.readyState === WebSocket.OPEN) {
connection.ws.send(JSON.stringify(data), (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
} else {
reject(new Error('WebSocket连接未打开'));
}
});
}

// 处理设备断开连接
handleDeviceDisconnection(userId, deviceId) {
const userDevices = this.deviceConnections.get(userId);
if (userDevices) {
userDevices.delete(deviceId);

// 如果用户没有其他设备连接,清理用户记录
if (userDevices.size === 0) {
this.deviceConnections.delete(userId);
}
}

console.log(`设备连接断开: ${userId}:${deviceId}`);
}

// 获取最新进度(需要实现)
async getLatestProgress(userId, videoId) {
// 这里应该调用存储管理器获取最新进度
// 返回格式应该包含timestamp等字段
return null;
}
}

(二)智能恢复策略

RecoveryService智能恢复实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
// 智能恢复服务
class RecoveryService {
constructor() {
this.recoveryRules = this.initializeRecoveryRules();
this.userBehaviorAnalyzer = new UserBehaviorAnalyzer();
}

// 初始化恢复规则
initializeRecoveryRules() {
return {
// 时间相关规则
timeRules: {
maxResumeAge: 7 * 24 * 60 * 60 * 1000, // 7天内的记录才提示恢复
minWatchTime: 60, // 至少观看60秒才记录
skipEndingThreshold: 0.95, // 超过95%不提示恢复
skipBeginningThreshold: 0.05 // 少于5%不提示恢复
},

// 设备相关规则
deviceRules: {
crossDeviceEnabled: true, // 启用跨设备恢复
sameDevicePreference: true, // 同设备优先
mobileToDesktopBonus: 0.1, // 手机到桌面端加分
desktopToMobileBonus: 0.05 // 桌面到手机端加分
},

// 内容相关规则
contentRules: {
seriesEpisodeContinuity: true, // 连续剧集连续性
movieResumePreference: 0.8, // 电影恢复偏好
shortVideoThreshold: 300, // 短视频阈值(5分钟)
liveStreamNoResume: true // 直播不支持恢复
}
};
}

// 判断是否应该恢复播放
async shouldResumePlayback(progress, deviceInfo) {
try {
// 1. 基础验证
if (!progress || !progress.hasProgress) {
return {
shouldResume: false,
reason: 'no_progress',
message: '没有播放记录'
};
}

// 2. 时间规则检查
const timeCheck = this.checkTimeRules(progress);
if (!timeCheck.passed) {
return {
shouldResume: false,
reason: timeCheck.reason,
message: timeCheck.message
};
}

// 3. 进度位置检查
const progressCheck = this.checkProgressPosition(progress);
if (!progressCheck.passed) {
return {
shouldResume: false,
reason: progressCheck.reason,
message: progressCheck.message
};
}

// 4. 设备兼容性检查
const deviceCheck = this.checkDeviceCompatibility(progress, deviceInfo);

// 5. 用户行为分析
const behaviorScore = await this.userBehaviorAnalyzer.calculateResumeScore(
progress,
deviceInfo
);

// 6. 综合决策
const finalScore = this.calculateFinalScore(
timeCheck.score,
progressCheck.score,
deviceCheck.score,
behaviorScore
);

const shouldResume = finalScore > 0.6; // 阈值可调

return {
shouldResume: shouldResume,
confidence: finalScore,
reason: shouldResume ? 'intelligent_decision' : 'low_confidence',
message: shouldResume ?
this.generateResumeMessage(progress, deviceInfo) :
'根据观看习惯,建议重新开始观看',
details: {
timeScore: timeCheck.score,
progressScore: progressCheck.score,
deviceScore: deviceCheck.score,
behaviorScore: behaviorScore,
finalScore: finalScore
}
};

} catch (error) {
console.error('判断是否恢复播放失败:', error);
return {
shouldResume: false,
reason: 'error',
message: '无法确定恢复策略'
};
}
}

// 检查时间规则
checkTimeRules(progress) {
const now = Date.now();
const timeSinceLastWatch = now - progress.lastWatchTime.getTime();
const rules = this.recoveryRules.timeRules;

// 检查记录是否过期
if (timeSinceLastWatch > rules.maxResumeAge) {
return {
passed: false,
reason: 'too_old',
message: '观看记录过于久远',
score: 0
};
}

// 检查观看时长是否足够
if (progress.currentTime < rules.minWatchTime) {
return {
passed: false,
reason: 'too_short',
message: '观看时间太短',
score: 0
};
}

// 计算时间分数(越近分数越高)
const ageScore = Math.max(0, 1 - (timeSinceLastWatch / rules.maxResumeAge));

return {
passed: true,
score: ageScore,
timeSinceLastWatch: timeSinceLastWatch
};
}

// 检查进度位置
checkProgressPosition(progress) {
const percentage = progress.percentage / 100;
const rules = this.recoveryRules.timeRules;

// 检查是否在开头
if (percentage < rules.skipBeginningThreshold) {
return {
passed: false,
reason: 'at_beginning',
message: '还在开头部分',
score: 0
};
}

// 检查是否接近结尾
if (percentage > rules.skipEndingThreshold) {
return {
passed: false,
reason: 'near_ending',
message: '已接近结尾',
score: 0
};
}

// 计算进度分数(中间部分分数更高)
let progressScore;
if (percentage < 0.5) {
// 前半部分:线性增长
progressScore = percentage * 2;
} else {
// 后半部分:线性下降
progressScore = 2 - (percentage * 2);
}

return {
passed: true,
score: Math.max(0.3, progressScore), // 最低0.3分
percentage: percentage
};
}

// 检查设备兼容性
checkDeviceCompatibility(progress, deviceInfo) {
const rules = this.recoveryRules.deviceRules;
let score = 0.5; // 基础分数

// 跨设备检查
if (!rules.crossDeviceEnabled && progress.deviceType !== deviceInfo.deviceType) {
return {
passed: false,
reason: 'cross_device_disabled',
score: 0
};
}

// 同设备加分
if (progress.deviceType === deviceInfo.deviceType) {
score += rules.sameDevicePreference ? 0.3 : 0.1;
}

// 设备类型转换加分
if (progress.deviceType === 'mobile' && deviceInfo.deviceType === 'desktop') {
score += rules.mobileToDesktopBonus;
} else if (progress.deviceType === 'desktop' && deviceInfo.deviceType === 'mobile') {
score += rules.desktopToMobileBonus;
}

return {
passed: true,
score: Math.min(1.0, score),
deviceTransition: progress.deviceType !== deviceInfo.deviceType
};
}

// 计算最终分数
calculateFinalScore(timeScore, progressScore, deviceScore, behaviorScore) {
// 加权平均
const weights = {
time: 0.3,
progress: 0.25,
device: 0.2,
behavior: 0.25
};

return (
timeScore * weights.time +
progressScore * weights.progress +
deviceScore * weights.device +
behaviorScore * weights.behavior
);
}

// 生成恢复消息
generateResumeMessage(progress, deviceInfo) {
const timeSince = Date.now() - progress.lastWatchTime.getTime();
const timeText = this.formatTimeSince(timeSince);
const progressText = this.formatTime(progress.currentTime);

if (progress.deviceType !== deviceInfo.deviceType) {
return `在${progress.deviceType}上观看到 ${progressText}${timeText},是否继续观看?`;
} else {
return `上次观看到 ${progressText}${timeText},是否从此处继续?`;
}
}

// 格式化时间间隔
formatTimeSince(milliseconds) {
const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);

if (days > 0) {
return `${days}天前`;
} else if (hours > 0) {
return `${hours}小时前`;
} else if (minutes > 0) {
return `${minutes}分钟前`;
} else {
return '刚刚';
}
}

// 格式化时间
formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;

if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
}
}

// 用户行为分析器
class UserBehaviorAnalyzer {
constructor() {
this.behaviorCache = new Map();
}

// 计算恢复分数
async calculateResumeScore(progress, deviceInfo) {
try {
// 获取用户历史行为
const userBehavior = await this.getUserBehavior(progress.userId);

// 分析恢复偏好
const resumePreference = this.analyzeResumePreference(userBehavior);

// 分析设备使用习惯
const devicePreference = this.analyzeDevicePreference(userBehavior, deviceInfo);

// 分析观看模式
const watchingPattern = this.analyzeWatchingPattern(userBehavior, progress);

// 综合计算分数
return (resumePreference * 0.4 + devicePreference * 0.3 + watchingPattern * 0.3);

} catch (error) {
console.error('计算用户行为分数失败:', error);
return 0.5; // 默认中等分数
}
}

// 分析恢复偏好
analyzeResumePreference(userBehavior) {
if (!userBehavior || !userBehavior.resumeHistory) {
return 0.5;
}

const resumeHistory = userBehavior.resumeHistory;
const totalPrompts = resumeHistory.totalPrompts || 1;
const acceptedResumes = resumeHistory.acceptedResumes || 0;

return Math.min(1.0, acceptedResumes / totalPrompts);
}

// 分析设备偏好
analyzeDevicePreference(userBehavior, deviceInfo) {
if (!userBehavior || !userBehavior.deviceUsage) {
return 0.5;
}

const deviceUsage = userBehavior.deviceUsage;
const currentDeviceUsage = deviceUsage[deviceInfo.deviceType] || 0;
const totalUsage = Object.values(deviceUsage).reduce((sum, usage) => sum + usage, 1);

return currentDeviceUsage / totalUsage;
}

// 分析观看模式
analyzeWatchingPattern(userBehavior, progress) {
if (!userBehavior || !userBehavior.watchingPatterns) {
return 0.5;
}

const patterns = userBehavior.watchingPatterns;

// 分析完成率偏好
const avgCompletionRate = patterns.avgCompletionRate || 0.5;
const currentProgress = progress.percentage / 100;

// 如果用户通常完成度高,且当前进度适中,给高分
if (avgCompletionRate > 0.7 && currentProgress > 0.1 && currentProgress < 0.8) {
return 0.8;
}

return 0.5;
}

// 获取用户行为数据
async getUserBehavior(userId) {
// 从缓存获取
if (this.behaviorCache.has(userId)) {
const cached = this.behaviorCache.get(userId);
if (Date.now() - cached.timestamp < 3600000) { // 1小时缓存
return cached.data;
}
}

// 从数据库获取(这里简化处理)
const behaviorData = {
resumeHistory: {
totalPrompts: 100,
acceptedResumes: 75
},
deviceUsage: {
mobile: 60,
desktop: 30,
tablet: 10
},
watchingPatterns: {
avgCompletionRate: 0.75,
preferredWatchTime: 'evening',
avgSessionLength: 3600
}
};

// 更新缓存
this.behaviorCache.set(userId, {
data: behaviorData,
timestamp: Date.now()
});

return behaviorData;
}
}

四、技术总结与优化策略

(一)断点续播技术架构总结

通过深入分析视频网站断点续播功能的实现原理,我们可以看到这是一个集成了多项技术的复杂系统:

核心技术架构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
视频断点续播技术栈:

数据层:
├── MySQL集群:播放进度的持久化存储
├── Redis缓存:高频访问数据的快速缓存
├── 分库分表:海量用户数据的水平扩展
└── 数据压缩:进度数据的高效存储

业务逻辑层:
├── 进度追踪:实时监控播放状态变化
├── 智能恢复:基于多维度分析的恢复决策
├── 跨设备同步:多设备间的数据一致性
├── 冲突解决:并发更新的智能处理
└── 用户行为分析:个性化的恢复策略

传输层:
├── WebSocket:实时双向通信
├── HTTP API:标准的进度查询接口
├── 消息队列:异步任务处理
└── CDN加速:全球化的服务部署

优化策略:
├── 批量处理:减少数据库写入频率
├── 缓存分层:多级缓存提升响应速度
├── 智能预测:基于用户行为的预加载
└── 性能监控:实时系统健康状态监控

技术创新点:

  1. 智能恢复策略:多维度分析的个性化恢复决策
  2. 实时同步机制:跨设备的毫秒级数据同步
  3. 用户行为学习:基于历史数据的智能优化
  4. 冲突解决算法:并发场景下的数据一致性保证
  5. 性能优化策略:海量数据的高效处理方案

(二)性能优化与最佳实践

关键性能指标:

  • 响应时间:进度查询<50ms,恢复决策<100ms
  • 数据一致性:跨设备同步延迟<1秒
  • 存储效率:单用户进度数据<1KB
  • 并发处理:支持百万级并发进度更新
  • 可用性:99.9%的服务可用性保证

断点续播功能看似简单,实际上是现代视频平台用户体验优化的重要体现。它不仅解决了用户的实际需求,更展示了如何通过技术手段提升产品的易用性和用户满意度。随着5G、边缘计算等技术的发展,未来的断点续播功能将在实时性、智能化、个性化等方面有更大的突破。

参考资料