视频播放进度记忆:基于videojs-player实现历史记录

视频播放进度记忆:基于videojs-player实现历史记录

🔥【免费下载链接】videojs-player @videojs player component for @vuejs(3) and React. 🔥【免费下载链接】videojs-player 项目地址: https://gitcode.com/gh_mirrors/vi/videojs-player

1. 痛点与解决方案概述

你是否遇到过这样的场景:观看教学视频时中途被打断,再次打开却需要重新拖动进度条寻找上次观看位置?或者在多设备间切换时,希望无缝接续视频播放进度?这些问题严重影响用户体验,尤其对于教育、培训类平台更为突出。

本文将详细介绍如何基于videojs-player组件实现视频播放进度的本地存储与恢复功能,核心解决以下问题:

  • 自动记录用户观看位置(精确到秒)
  • 页面刷新或重新打开时恢复上次进度
  • 支持多视频独立记忆管理
  • 提供手动清除记录功能

通过本文你将获得:

  • 完整的进度记忆实现方案(含核心代码)
  • 本地存储优化策略
  • videojs-player深度集成的实践经验
  • 可直接复用的组件代码与演示案例

2. 技术原理与实现思路

2.1 核心技术栈

技术版本作用
videojs-player^8.0.0视频播放核心组件
LocalStorageHTML5标准持久化存储播放进度
TypeScript^4.5.0类型安全开发
Vue.js/React3.x前端框架支持

2.2 实现流程图

mermaid

2.3 数据存储结构设计

interface PlaybackRecord {
  videoId: string;          // 视频唯一标识
  currentTime: number;      // 当前播放时间(秒)
  duration: number;         // 视频总时长(秒)
  lastWatched: string;      // 最后观看时间
  completed: boolean;       // 是否播放完成(>95%视为完成)
  positionPercent: number;  // 播放进度百分比
}

3. 核心功能实现

3.1 进度记录服务封装

首先实现一个通用的进度记录服务,负责与LocalStorage交互:

class PlaybackHistoryService {
  private STORAGE_KEY = 'videojs_playback_history';
  
  // 获取指定视频的播放记录
  getRecord(videoId: string): PlaybackRecord | null {
    const records = this.getAllRecords();
    return records.find(item => item.videoId === videoId) || null;
  }
  
  // 保存播放记录
  saveRecord(record: PlaybackRecord): void {
    const records = this.getAllRecords();
    // 移除旧记录
    const filtered = records.filter(item => item.videoId !== record.videoId);
    // 添加新记录
    filtered.push({
      ...record,
      lastWatched: new Date().toISOString()
    });
    // 限制最多存储100条记录
    if (filtered.length > 100) {
      filtered.sort((a, b) => new Date(b.lastWatched).getTime() - new Date(a.lastWatched).getTime());
      filtered.pop();
    }
    localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filtered));
  }
  
  // 获取所有记录
  getAllRecords(): PlaybackRecord[] {
    const data = localStorage.getItem(this.STORAGE_KEY);
    return data ? JSON.parse(data) : [];
  }
  
  // 删除指定记录
  deleteRecord(videoId: string): void {
    const records = this.getAllRecords();
    const filtered = records.filter(item => item.videoId !== videoId);
    localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filtered));
  }
  
  // 清空所有记录
  clearAllRecords(): void {
    localStorage.removeItem(this.STORAGE_KEY);
  }
}

3.2 与videojs-player集成

Vue3组件实现
<template>
  <div class="video-container">
    <video-player
      ref="videoPlayer"
      :options="playerOptions"
      @ready="onPlayerReady"
      @timeupdate="onTimeUpdate"
      @pause="onPause"
      @ended="onEnded"
    />
    <button @click="clearHistory" v-if="hasHistory">
      清除播放记录
    </button>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { VideoPlayer } from '@videojs-player/vue';
import { PlaybackHistoryService } from './services/PlaybackHistoryService';

const videoId = 'video_123456'; // 实际项目中应动态传入
const player = ref<VideoPlayer | null>(null);
const historyService = new PlaybackHistoryService();
const hasHistory = ref(false);
let lastSavedTime = 0;
const SAVE_INTERVAL = 30; // 30秒自动保存一次

const playerOptions = {
  autoplay: false,
  controls: true,
  responsive: true,
  fluid: true,
  sources: [{
    src: 'https://cdn.example.com/videos/sample.mp4',
    type: 'video/mp4'
  }]
};

const onPlayerReady = (player: any) => {
  // 检查是否有历史记录
  const record = historyService.getRecord(videoId);
  if (record && !record.completed) {
    hasHistory.value = true;
    // 恢复播放进度
    player.currentTime(record.currentTime);
    // 显示提示信息
    console.log(`已恢复上次播放进度: ${formatTime(record.currentTime)}`);
  }
};

const onTimeUpdate = (currentTime: number, duration: number) => {
  const now = Date.now() / 1000;
  // 每30秒或进度变化超过10秒时保存
  if (now - lastSavedTime > SAVE_INTERVAL || 
      Math.abs(currentTime - lastSavedTime) > 10) {
    saveProgress(currentTime, duration);
    lastSavedTime = now;
  }
};

const onPause = (currentTime: number, duration: number) => {
  saveProgress(currentTime, duration);
};

const onEnded = (currentTime: number, duration: number) => {
  // 播放完成(超过95%视为完成)
  const isCompleted = currentTime / duration > 0.95;
  saveProgress(currentTime, duration, isCompleted);
};

const saveProgress = (currentTime: number, duration: number, completed = false) => {
  historyService.saveRecord({
    videoId,
    currentTime,
    duration,
    completed,
    positionPercent: (currentTime / duration) * 100
  });
};

const clearHistory = () => {
  historyService.deleteRecord(videoId);
  hasHistory.value = false;
  player.value?.currentTime(0);
};

// 格式化时间为分:秒格式
const formatTime = (seconds: number): string => {
  const mins = Math.floor(seconds / 60);
  const secs = Math.floor(seconds % 60);
  return `${mins}:${secs < 10 ? '0' + secs : secs}`;
};
</script>

3.3 React组件实现

import React, { useRef, useState, useEffect } from 'react';
import { VideoPlayer } from '@videojs-player/react';
import { PlaybackHistoryService } from './services/PlaybackHistoryService';

interface VideoPlayerWithHistoryProps {
  videoId: string;
  videoSrc: string;
}

export const VideoPlayerWithHistory: React.FC<VideoPlayerWithHistoryProps> = ({
  videoId,
  videoSrc
}) => {
  const playerRef = useRef<VideoPlayer | null>(null);
  const historyService = new PlaybackHistoryService();
  const [hasHistory, setHasHistory] = useState(false);
  let lastSavedTime = 0;
  const SAVE_INTERVAL = 30;

  useEffect(() => {
    // 组件卸载时保存进度
    return () => {
      if (playerRef.current) {
        const currentTime = playerRef.current.currentTime();
        const duration = playerRef.current.duration();
        saveProgress(currentTime, duration);
      }
    };
  }, []);

  const onPlayerReady = (player: any) => {
    const record = historyService.getRecord(videoId);
    if (record && !record.completed) {
      setHasHistory(true);
      player.currentTime(record.currentTime);
      console.log(`已恢复上次播放进度: ${formatTime(record.currentTime)}`);
    }
  };

  const onTimeUpdate = () => {
    if (!playerRef.current) return;
    
    const currentTime = playerRef.current.currentTime();
    const duration = playerRef.current.duration();
    const now = Date.now() / 1000;

    if (now - lastSavedTime > SAVE_INTERVAL ||
        Math.abs(currentTime - lastSavedTime) > 10) {
      saveProgress(currentTime, duration);
      lastSavedTime = now;
    }
  };

  const saveProgress = (currentTime: number, duration: number, completed = false) => {
    historyService.saveRecord({
      videoId,
      currentTime,
      duration,
      completed,
      positionPercent: (currentTime / duration) * 100
    });
  };

  const clearHistory = () => {
    historyService.deleteRecord(videoId);
    setHasHistory(false);
    playerRef.current?.currentTime(0);
  };

  const formatTime = (seconds: number): string => {
    const mins = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return `${mins}:${secs < 10 ? '0' + secs : secs}`;
  };

  return (
    <div className="video-player-container">
      <VideoPlayer
        ref={playerRef}
        options={{
          autoplay: false,
          controls: true,
          responsive: true,
          fluid: true,
          sources: [{ src: videoSrc, type: 'video/mp4' }]
        }}
        onReady={onPlayerReady}
        onTimeUpdate={onTimeUpdate}
        onPause={() => {
          if (playerRef.current) {
            saveProgress(
              playerRef.current.currentTime(),
              playerRef.current.duration()
            );
          }
        }}
        onEnded={() => {
          if (playerRef.current) {
            const currentTime = playerRef.current.currentTime();
            const duration = playerRef.current.duration();
            saveProgress(currentTime, duration, currentTime / duration > 0.95);
          }
        }}
      />
      {hasHistory && (
        <button onClick={clearHistory} className="clear-history-btn">
          清除播放记录
        </button>
      )}
    </div>
  );
};

4. 高级优化策略

4.1 性能优化

优化点实现方法效果
减少存储次数设置30秒自动保存间隔降低LocalStorage操作频率
防抖处理使用时间戳判断是否需要保存避免频繁触发保存
批量存储多视频记录合并存储减少存储键值对数量
惰性加载视频播放时才初始化记录提高页面加载速度

4.2 数据安全与容量控制

// 改进的存储方法,增加容量控制
saveRecord(record: PlaybackRecord): void {
  const records = this.getAllRecords();
  const filtered = records.filter(item => item.videoId !== record.videoId);
  
  // 只保留最近100条记录
  if (filtered.length >= 100) {
    // 按最后观看时间排序,保留最新的99条
    filtered.sort((a, b) => 
      new Date(b.lastWatched).getTime() - new Date(a.lastWatched).getTime()
    );
    filtered.splice(99);
  }
  
  filtered.push({
    ...record,
    lastWatched: new Date().toISOString()
  });
  
  try {
    localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filtered));
  } catch (e) {
    console.error('存储播放记录失败:', e);
    // 处理存储满的情况,删除最旧的记录后重试
    if (filtered.length > 0) {
      filtered.shift();
      localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filtered));
    }
  }
}

5. 浏览器兼容性处理

5.1 LocalStorage兼容性

浏览器支持版本特殊处理
Chrome4+无需处理
Firefox3.5+无需处理
Safari4+隐私模式下可能不可用
IE8+需要polyfill

5.2 降级处理方案

// 兼容性处理增强版
class PlaybackHistoryService {
  private STORAGE_KEY = 'videojs_playback_history';
  private isStorageAvailable: boolean;
  
  constructor() {
    this.isStorageAvailable = this.checkStorageAvailability();
  }
  
  // 检查LocalStorage是否可用
  private checkStorageAvailability(): boolean {
    try {
      const key = '__storage_test__';
      localStorage.setItem(key, key);
      localStorage.removeItem(key);
      return true;
    } catch (e) {
      return false;
    }
  }
  
  // 保存记录时的兼容性处理
  saveRecord(record: PlaybackRecord): void {
    if (!this.isStorageAvailable) {
      console.warn('LocalStorage不可用,无法保存播放记录');
      return;
    }
    
    // ... 正常存储逻辑 ...
  }
  
  // ... 其他方法 ...
}

6. 完整使用示例

6.1 Vue3完整页面

<template>
  <div class="course-video-page">
    <h1>Vue.js实战教程 - 第1章</h1>
    <video-player-with-history 
      :video-id="videoId"
      :video-src="videoSource"
    />
    <div class="video-chapters">
      <h2>课程章节</h2>
      <ul>
        <li v-for="chapter in chapters" :key="chapter.id">
          <button @click="switchVideo(chapter.id, chapter.src)">
            {{ chapter.title }}
            <span v-if="chapter.id === currentVideoId && hasHistory">
              (上次看到: {{ formatTime(lastPosition) }})
            </span>
          </button>
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue';
import VideoPlayerWithHistory from './components/VideoPlayerWithHistory.vue';
import { PlaybackHistoryService } from './services/PlaybackHistoryService';

const historyService = new PlaybackHistoryService();
const currentVideoId = ref('vue_intro_01');
const videoSource = ref('https://cdn.example.com/videos/vue-intro.mp4');
const hasHistory = ref(false);
const lastPosition = ref(0);

const chapters = reactive([
  { id: 'vue_intro_01', title: 'Vue3简介', src: 'https://cdn.example.com/videos/vue-intro.mp4' },
  { id: 'vue_intro_02', title: '响应式原理', src: 'https://cdn.example.com/videos/vue-reactivity.mp4' },
  { id: 'vue_intro_03', title: '组件基础', src: 'https://cdn.example.com/videos/vue-components.mp4' }
]);

// 切换视频时检查是否有历史记录
const switchVideo = (id: string, src: string) => {
  currentVideoId.value = id;
  videoSource.value = src;
  
  const record = historyService.getRecord(id);
  hasHistory.value = !!record;
  if (record) {
    lastPosition.value = record.currentTime;
  }
};

// 格式化时间显示
const formatTime = (seconds: number): string => {
  const mins = Math.floor(seconds / 60);
  const secs = Math.floor(seconds % 60);
  return `${mins}:${secs < 10 ? '0' + secs : secs}`;
};

// 初始化时检查当前视频的历史记录
switchVideo(currentVideoId.value, videoSource.value);
</script>

7. 总结与扩展

7.1 功能回顾

本文实现了一个功能完善的视频播放进度记忆组件,主要特点包括:

  1. 自动记录与恢复播放进度
  2. 高效的LocalStorage数据管理
  3. 完善的错误处理与兼容性考虑
  4. 支持Vue.js和React两种主流框架

7.2 未来扩展方向

  1. 云同步功能:结合用户账号系统,实现多设备间进度同步
  2. 智能预加载:根据历史记录预测用户可能观看的下一视频
  3. 观看行为分析:统计用户观看习惯,优化视频内容
  4. 离线播放支持:结合Service Worker实现离线进度记录

7.3 最佳实践建议

  1. 始终为视频提供唯一标识(videoId)
  2. 设置合理的自动保存间隔(建议20-60秒)
  3. 提供清晰的"清除记录"功能入口
  4. 对长视频考虑分段记录,提高恢复精度
  5. 在隐私模式下提供明确的功能不可用提示

8. 常见问题解答

Q1: 为什么有时进度记录不精确?

A1: 由于我们采用定时保存机制(默认30秒),极端情况下可能会丢失最后30秒的播放进度。可以通过减小保存间隔提高精度,但会增加存储操作频率。

Q2: 如何在多标签页同时播放时保持进度同步?

A2: 可以通过监听storage事件实现多标签页间的数据同步:

// 监听LocalStorage变化
window.addEventListener('storage', (e) => {
  if (e.key === 'videojs_playback_history' && playerRef.current) {
    const records = JSON.parse(e.newValue || '[]');
    const currentVideoRecord = records.find(r => r.videoId === currentVideoId);
    if (currentVideoRecord) {
      playerRef.current.currentTime(currentVideoRecord.currentTime);
    }
  }
});

Q3: 如何迁移到IndexedDB以支持更大数据量?

A3: 可以实现一个抽象存储层,无缝切换LocalStorage和IndexedDB:

interface StorageAdapter {
  getRecords(): Promise<PlaybackRecord[]>;
  saveRecord(record: PlaybackRecord): Promise<void>;
  deleteRecord(videoId: string): Promise<void>;
}

// 然后分别实现LocalStorageAdapter和IndexedDBAdapter

希望本文提供的方案能帮助你为用户打造更优质的视频观看体验!如果觉得有用,请点赞收藏,关注获取更多前端视频播放技术分享。

🔥【免费下载链接】videojs-player @videojs player component for @vuejs(3) and React. 🔥【免费下载链接】videojs-player 项目地址: https://gitcode.com/gh_mirrors/vi/videojs-player

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值