做在线教育、点播平台或者内容付费网站时,统计用户实际观看了多长时间几乎是绕不开的需求。它直接影响课程完成度、会员权益判定和推荐算法的数据来源。但视频播放发生在浏览器端,PHP作为服务端语言本身无法直接感知播放器的一举一动,所以所谓的PHP获取观看时长,本质上是前端采集加服务端接收存储的组合。下面介绍几种常见的实现方案,并附上可直接参考的代码。

方案一:前端定时上报加服务端累加
这是最通用的做法。前端每隔一段时间(比如10秒或30秒)向PHP接口上报一次当前累计观看秒数,PHP端负责累加并存库。它的好处是即使用户中途刷新页面或者意外断网,之前上报的数据已经落库,误差最多就是最后一个上报周期。
前端部分可以用setInterval配合播放器的timeupdate事件来实现,下面是一个基于HTML5 Video的简单示例:
<video id="myVideo" src="lesson.mp4" controls></video>
<script>
var video = document.getElementById('myVideo');
var watchedSeconds = 0; // 本次页面内的观看秒数
var lastTime = 0;
var REPORT_INTERVAL = 15; // 每15秒上报一次
video.addEventListener('timeupdate', function() {
var diff = video.currentTime - lastTime;
// 只累计正常前进的播放,过滤拖动进度条带来的跳变
if (diff > 0 && diff < 2) {
watchedSeconds += diff;
}
lastTime = video.currentTime;
});
setInterval(function() {
if (!video.paused && watchedSeconds > 0) {
fetch('/report_watch_time.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'video_id=1024&seconds=' + Math.floor(watchedSeconds)
}).then(function() { watchedSeconds = 0; });
}
}, REPORT_INTERVAL * 1000);
</script>注意上面代码里对diff的判断很关键。用户如果快进,currentTime会瞬间跳增几十秒,这部分不应计入观看时长;而暂停、缓冲导致的微小回退也需要过滤,否则统计出来的时长会失真。
PHP接收端要做的事情包括身份校验、时长合法性和累加入库,示例代码如下:
<?php
session_start();
if (empty($_SESSION['user_id'])) {
http_response_code(401);
exit(json_encode(['code' => 401, 'msg' => '未登录']));
}
$userId = (int)$_SESSION['user_id'];
$videoId = (int)$_POST['video_id'];
$seconds = (int)$_POST['seconds'];
// 合法性校验:单次上报不应超过上报周期本身,防止恶意刷时长
if ($seconds < 0 || $seconds > 20) {
exit(json_encode(['code' => 400, 'msg' => '时长不合法']));
}
$pdo = new PDO('mysql:host=127.0.0.1;dbname=app', 'root', 'password');
$sql = 'INSERT INTO watch_record (user_id, video_id, total_seconds)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE total_seconds = total_seconds + VALUES(total_seconds)';
$stmt = $pdo->prepare($sql);
$stmt->execute([$userId, $videoId, $seconds]);
echo json_encode(['code' => 200, 'msg' => 'ok']);这里用了MySQL的ON DUPLICATE KEY UPDATE,前提是watch_record表对user_id加video_id建了唯一索引。这样累加逻辑交给数据库一条语句完成,不需要先查后写,并发场景下也不会丢失数据。
方案二:页面关闭时利用sendBeacon上报
定时上报虽然稳,但用户关闭页面的最后几秒到十几秒数据会丢。浏览器提供了一个专门为这类场景设计的接口:navigator.sendBeacon。它在页面卸载时也能异步发出请求,不阻塞页面关闭,PHP端照常接收即可。
window.addEventListener('pagehide', function() {
if (watchedSeconds > 0) {
var formData = new FormData();
formData.append('video_id', '1024');
formData.append('seconds', Math.floor(watchedSeconds));
navigator.sendBeacon('/report_watch_time.php', formData);
}
});sendBeacon发送的是POST请求,PHP端用$_POST接收没有问题。需要注意的是它不能设置自定义Header,所以身份认证要依赖Cookie或Session,而不能用Header里的token。如果你的项目是前后端分离且用Header传token,可以在用户打开页面时先换取一个短期的一次性令牌,随beacon一起带上,PHP端校验后即焚。
另外要提醒一句,不要把这个事件绑定在beforeunload上,部分移动端浏览器不触发它,而pagehide的兼容性更可靠。对于移动端App内的WebView场景,建议前端再配合原生日志做兜底。
方案三:基于播放日志的服务端估算
如果你的视频走的是自建流媒体服务或者用了HLS切片,还有第三条路:直接分析服务端的访问日志。每个切片被请求一次,就说明用户播放到了对应的位置,通过切片的时间戳可以推算出观看进度和时长。这种方案完全在服务端完成,不怕用户端篡改上报数据,防刷能力最强。
思路大致是:日志里按用户会话维度把切片请求按时间排序,相邻两次请求的间隔如果小于某个阈值(比如切片时长乘以2),就算作连续观看;出现大间隔说明用户离开过,中间的空档不计入。用PHP解析nginx日志的实现框架如下:
<?php
// 模拟解析nginx访问日志,统计某用户的连续观看时长
$logs = [
['ts' => 1700000100, 'seg' => 'seg_0.ts'],
['ts' => 1700000106, 'seg' => 'seg_1.ts'],
['ts' => 1700000112, 'seg' => 'seg_2.ts'],
['ts' => 1700000300, 'seg' => 'seg_3.ts'], // 中间离开了两分多钟
];
$total = 0;
$SEG_DURATION = 6; // 每个切片6秒
for ($i = 1; $i < count($logs); $i++) {
$gap = $logs[$i]['ts'] - $logs[$i - 1]['ts'];
if ($gap <= $SEG_DURATION * 2) {
$total += min($gap, $SEG_DURATION);
}
}
$total += $SEG_DURATION; // 补上最后一个切片
echo '估算观看时长:' . $total . '秒'; // 输出 24 秒左右这个方案的缺点也明显:日志量大的情况下解析成本高,通常需要配合定时任务离线计算;而且客户端有本地缓存或者预加载时,日志会缺失,估算值可能偏小。实际项目中一般把它作为辅助手段,用来交叉验证前端上报数据的真实性。
防刷与数据校验的几个细节
观看时长直接关系到会员权益、奖励发放时,一定会有人想办法刷。光靠前端上报是不可信的,服务端必须做兜底。最基本的做法是记录会话开始时间,每次上报时对比服务器当前时间与上次上报时间的差值,如果客户端声称的观看秒数明显大于真实流逝时间,就判定为异常并拒绝累加。
其次是限制观看时长的上限:不管视频多长,单个用户单条视频的累计观看时长不可能无限大,可以设置为视频时长的1.2倍封顶。倍速播放也要考虑,1.5倍速下真实观看的墙钟时间会小于视频时长,校验逻辑要留出这个弹性。
最后建议把明细和汇总分开存:一张watch_log表记录每次上报的原始明细(用户、视频、本次秒数、上报时间、IP),一张watch_record表存汇总时长。明细表既是审计依据,也方便日后发现统计口径问题后重新计算汇总,不必推翻重来。查询展示时直接读汇总表,性能压力也小得多。
综合来看,大多数项目的落地组合是方案一加方案二:定时上报保证容错,sendBeacon补上页面关闭的尾巴,再配合服务端的时间差校验防刷。方案三适合对数据真实性要求极高的场景作为补充校验。根据你的业务对精度的要求选择合适的组合即可。