导读:本期聚焦于湖南程序员创作的《CodeIgniter框架怎么实现视频播放记录保存?断点续播功能实战教程》,敬请观看详情。为什么用户退出视频页面后,再次打开还能从上次的位置继续播放?这背后靠的是播放记录的持久化存储。本文基于CodeIgniter框架,完整讲解如何实现视频播放记录的保存与断点续播功能,包括数据库表结构设计、播放进度的定时上报接口、后端控制器与模型的编写、以及前端播放器如何结合localStorage实现双保险缓存。文中还会分析上报频率的控制策略、跨设备进度同步的处理思路,以及常见的时间格式转换与边界问题排查方法。整体方案代码完整可直接落地,适合正在开发在线教育、影视站点或课程系统的PHP开发者参考。

做视频类站点时,断点续播几乎是必备功能。用户看到第18分钟关掉了页面,第二天打开应该自动定位到18分钟继续看,而不是从头再来。这个看似简单的功能,背后涉及播放进度的采集、上报时机的选择、服务端的存储结构以及再次进入时的进度回填。本文以CodeIgniter框架为例,把整套流程拆开讲透,代码可以直接拿去用。

CodeIgniter框架怎么实现视频播放记录保存?断点续播功能实战教程

一、整体思路与数据库设计

断点续播的核心就是一张播放记录表。每次用户播放视频时,前端定时把当前播放时间上报给后端,后端把这条记录存起来;下次用户打开同一个视频时,先查一次记录,把上次的时间点返回给前端,前端调用播放器的seek方法直接跳转过去。

先把数据库表建好,这里用MySQL,表结构如下:

CREATE TABLE `video_play_record` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户ID,游客为0',
  `video_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '视频ID',
  `progress_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已播放秒数',
  `duration` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '视频总时长(秒)',
  `device` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '设备标识',
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_video` (`user_id`, `video_id`, `device`),
  KEY `idx_video` (`video_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='视频播放记录表';

这里有个关键设计点:唯一索引uk_user_video把用户、视频、设备三个维度绑定在一起。为什么要把device加进唯一索引?因为同一个用户可能在手机上看到第10分钟,又换到电脑上从头看。如果不想做跨设备同步,按设备各存一条是最合理的选择;如果产品要求全端同步,去掉device字段或者统一device的值即可,非常灵活。

存储秒数而不是"01:23:45"这种时间字符串,是为了计算方便。前端上报秒数,后端回填时也返回秒数,格式转换交给前端展示层处理,数据层保持纯粹。

二、后端接口:模型与控制器的实现

数据库层面采用"存在则更新,不存在则插入"的写法。CodeIgniter 3.x提供了ON DUPLICATE KEY UPDATE的原生支持,可以直接拼一条SQL搞定,避免先查询再判断的两次交互。

先写模型,新建application/models/Play_record_model.php

<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class Play_record_model extends CI_Model {

    public function save($user_id, $video_id, $progress, $duration, $device)
    {
        $data = array(
            'user_id'       => (int)$user_id,
            'video_id'      => (int)$video_id,
            'progress_time' => (int)$progress,
            'duration'      => (int)$duration,
            'device'        => substr($device, 0, 32),
        );

        $sql = $this->db->insert_string('video_play_record', $data)
                . " ON DUPLICATE KEY UPDATE progress_time = VALUES(progress_time),
                    duration = VALUES(duration), updated_at = NOW()";

        return $this->db->query($sql);
    }

    // 获取某用户某视频的播放进度
    public function get_progress($user_id, $video_id, $device)
    {
        $this->db->where(array(
            'user_id'  => (int)$user_id,
            'video_id' => (int)$video_id,
            'device'   => $device
        ));
        $row = $this->db->get('video_play_record')->row_array();
        return $row ? $row['progress_time'] : 0;
    }
}

注意insert_string生成的SQL需要手动拼上ON DUPLICATE KEY UPDATE部分,CodeIgniter的查询构造器本身没有这个语法的封装。另外对device做了substr截断,防止超长字符串导致SQL报错。

接下来是控制器部分,新建application/controllers/Api.php

<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class Api extends CI_Controller {

    public function __construct()
    {
        parent::__construct();
        $this->load->model('Play_record_model');
        $this->load->helper('url');
    }

    // 保存播放进度接口
    public function save_progress()
    {
        $user_id  = $this->session->userdata('uid') ? (int)$this->session->userdata('uid') : 0;
        $video_id = (int)$this->input->post('video_id');
        $progress = (int)$this->input->post('progress');
        $duration = (int)$this->input->post('duration');
        $device   = $this->input->post('device', TRUE);

        if ($video_id <= 0 || $progress < 0)
        {
            $this->output
                 ->set_content_type('application/json')
                 ->set_output(json_encode(array('code' => 400, 'msg' => '参数错误')));
            return;
        }

        // 已看完的记录归零,避免下次从头秒跳
        if ($duration > 0 && $progress >= $duration - 5)
        {
            $progress = 0;
        }

        $this->Play_record_model->save($user_id, $video_id, $progress, $duration, $device);

        $this->output
             ->set_content_type('application/json')
             ->set_output(json_encode(array('code' => 0, 'msg' => 'ok')));
    }

    // 查询播放进度接口
    public function get_progress()
    {
        $user_id  = $this->session->userdata('uid') ? (int)$this->session->userdata('uid') : 0;
        $video_id = (int)$this->input->get('video_id');
        $device   = $this->input->get('device', TRUE);

        $time = $this->Play_record_model->get_progress($user_id, $video_id, $device);

        $this->output
             ->set_content_type('application/json')
             ->set_output(json_encode(array(
                 'code' => 0,
                 'data' => array('progress' => $time)
             )));
    }
}

控制器里有个容易被忽略的细节:当进度接近视频结尾时(这里留了5秒容差),把进度归零。如果不做这个处理,用户看完整个视频后再进来,播放器会seek到最后几秒然后直接触发ended事件,体验非常奇怪。归零之后相当于"看完即重置",符合主流视频站的交互习惯。

三、前端播放器对接:上报时机与本地双缓存

后端接口有了,前端什么时候上报就成了关键。实时上报肯定不行,每秒一次请求会把服务器打崩;只在关闭页面时上报也不靠谱,用户直接杀掉浏览器进程时beforeunload事件根本来不及发请求。比较稳妥的做法是定时上报加页面卸载时兜底上报。

下面以原生HTML5 video元素为例,演示完整的前端逻辑:

<video id="player" src="/videos/demo.mp4" controls></video>

<script>
var player = document.getElementById('player');
var videoId = 123;
var deviceId = localStorage.getItem('device_id') || createDeviceId();
var timer = null;
var lastProgress = 0;

function createDeviceId() {
    var id = 'dev_' + Date.now() + '_' + Math.random().toString(36).substr(2, 8);
    localStorage.setItem('device_id', id);
    return id;
}

function report() {
    var progress = Math.floor(player.currentTime);
    if (progress === lastProgress) return; // 进度没变不重复上报
    lastProgress = progress;

    var fd = new FormData();
    fd.append('video_id', videoId);
    fd.append('progress', progress);
    fd.append('duration', Math.floor(player.duration || 0));
    fd.append('device', deviceId);

    navigator.sendBeacon('/api/save_progress', fd);

    // 本地双保险:网络异常时至少localStorage里有记录
    localStorage.setItem('vp_' + videoId, progress);
}

// 每10秒上报一次
player.addEventListener('playing', function() {
    if (!timer) timer = setInterval(report, 10000);
});
player.addEventListener('pause', function() {
    clearInterval(timer); timer = null; report();
});

// 页面卸载兜底,sendBeacon不阻塞页面关闭
window.addEventListener('pagehide', function() {
    if (timer) { clearInterval(timer); timer = null; }
    report();
});

// 初始回填进度:优先服务端,降级读本地
fetch('/api/get_progress?video_id=' + videoId + '&device=' + encodeURIComponent(deviceId))
    .then(function(r) { return r.json(); })
    .then(function(res) {
        var t = res.data && res.data.progress;
        if (!t) t = parseInt(localStorage.getItem('vp_' + videoId) || 0, 10);
        if (t > 0) {
            player.addEventListener('loadedmetadata', function() {
                player.currentTime = t;
            });
        }
    });
</script>

这里有几个点值得展开。第一,上报用navigator.sendBeacon而不是普通的XMLHttpRequest,因为sendBeacon是异步不阻塞的,即使页面正在关闭也能保证请求发出去,这正好解决卸载时机的问题,而且对旧浏览器可以降级成同步XHR。第二,回填进度必须等loadedmetadata事件触发后再设置currentTime,此时播放器已经拿到视频元数据,知道总时长了,seek才会生效,太早设置会被浏览器忽略。第三,localStorage做了一层本地缓存,服务端查询失败或游客状态下也能实现续播,两者互为补充。

四、进阶优化与常见问题排查

基础版跑通之后,还有一些可以打磨的地方。上报频率方面,10秒是通用值,如果是长视频(一小时以上)可以放宽到30秒,毕竟丢10秒进度用户基本无感,但请求量能降三分之二。高并发场景下,可以考虑在CodeIgniter前端加一层Redis缓存,播放进度先写Redis,每分钟批量刷回MySQL,进一步降低数据库压力。

游客续播是个常见需求。没有登录态时user_id统一存0,配合device字段就能实现"设备级续播",也就是不登录也能记住进度。但要注意清理策略:user_id为0的记录会随设备数量增长,建议写个定时任务,把updated_at超过30天的游客记录清掉,防止表无限膨胀。

排查问题时可以从这几个现象入手:

  • 进度回填不生效:九成是seek时机太早,检查是否监听了loadedmetadata;另外部分移动端浏览器要求用户先交互才能seek,可以先播放再暂停再跳转。
  • 上报偶尔丢失:确认用的是sendBeacon,普通异步请求在页面关闭时会中断;另外检查上报条件里是否有进度去重逻辑导致的漏报。
  • 多端进度互相覆盖:检查唯一索引里是否包含了device字段,或者前端deviceId是否被意外重置(比如隐身模式下localStorage不可用)。
  • 看完无法重新看:这就是控制器里归零逻辑的作用范围,确认容差秒数设置是否合理,太小的容差会因为时长估算误差导致误归零。

整体方案的代码量不大,但把前后端的协作时机理清楚才是重点。定时上报加卸载兜底保证数据不丢,服务端唯一索引保证记录不重复,归零逻辑保证看完能重看,localStorage保证极端情况下依然可用。这套结构同样适用于音频课程、有声书等场景,稍作改造就能复用。

CodeIgniter视频播放记录断点续播PHP视频进度保存修改时间:2026-09-07 06:36:44

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260907/52038.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。