ITPub博客

首页 > 应用开发 > IT综合 > 视频续播功能实现:从前端交互到后端处理

视频续播功能实现:从前端交互到后端处理

投稿 IT综合 作者:bhluo 时间:2025-12-11 17:59:41 0 删除 编辑

# 视频续播功能实现:从前端交互到后端处理


## 一、用户场景与功能价值


在观看长视频内容时,用户经常因各种原因中断播放。视频续播功能能够记录用户观看进度,在下次打开时自动定位到上次中断的时间点,显著提升观看体验。这项技术不仅适用于在线教育平台,也广泛应用于影视网站、知识分享平台等场景。


## 二、技术架构概览


完整的视频续播功能需要前后端协同工作:前端负责进度采集与请求发送,后端负责数据存储与响应处理。整体架构如下图所示:


```

┌─────────────┐    ┌─────────────┐    ┌─────────────┐

│   前端播放器   │    │  Spring Boot │    │   数据存储   │

│             │    │    后端      │    │             │

│ 1.监听播放进度 │───▶│ 2.接收进度数据 │───▶│ 3.持久化存储 │

│             │    │             │    │             │

│ 6.跳转播放位置 │◀───│ 5.返回进度信息 │◀───│ 4.查询进度   │

└─────────────┘    └─────────────┘    └─────────────┘

```


## 三、前端实现策略


### 3.1 进度采集机制


前端播放器需要定期采集播放进度,但过于频繁的请求会增加服务器负担。合理的策略是在特定事件触发时上报进度:


```javascript

class VideoProgressTracker {

  constructor(videoElement, options = {}) {

    this.video = videoElement;

    this.videoId = options.videoId;

    this.userId = options.userId;

    this.reportInterval = options.interval || 10000; // 10秒上报一次

    this.lastReportedTime = 0;

    

    this.initializeListeners();

  }

  

  initializeListeners() {

    // 定期上报进度

    this.video.addEventListener('timeupdate', () => {

      const currentTime = Math.floor(this.video.currentTime);

      const timeDiff = Math.abs(currentTime - this.lastReportedTime);

      

      // 满足时间间隔或视频暂停时上报

      if (timeDiff >= this.reportInterval / 1000 || this.video.paused) {

        this.reportProgress(currentTime);

      }

    });

    

    // 页面关闭前上报

    window.addEventListener('beforeunload', () => {

      this.reportProgress(Math.floor(this.video.currentTime));

    });

    

    // 暂停时上报

    this.video.addEventListener('pause', () => {

      this.reportProgress(Math.floor(this.video.currentTime));

    });

  }

  

  async reportProgress(currentTime) {

    if (this.lastReportedTime === currentTime) return;

    

    try {

      const response = await fetch('/api/video/progress', {

        method: 'POST',

        headers: { 'Content-Type': 'application/json' },

        body: JSON.stringify({

          videoId: this.videoId,

          userId: this.userId,

          progress: currentTime,

          duration: Math.floor(this.video.duration)

        })

      });

      

      if (response.ok) {

        this.lastReportedTime = currentTime;

      }

    } catch (error) {

      console.warn('进度上报失败:', error);

    }

  }

  

  // 获取续播位置

  async getResumePosition() {

    try {

      const response = await fetch(

        `/api/video/progress?videoId=${this.videoId}&userId=${this.userId}`

      );

      

      if (response.ok) {

        const data = await response.json();

        return data.progress || 0;

      }

    } catch (error) {

      console.warn('获取续播位置失败:', error);

      return 0;

    }

  }

}

```


### 3.2 播放器集成示例


```html

<video id="videoPlayer" controls>

  <source src="/api/video/stream/123" type="video/mp4">

</video>


<script>

document.addEventListener('DOMContentLoaded', async () => {

  const video = document.getElementById('videoPlayer');

  const userId = getCurrentUserId(); // 获取用户标识

  const videoId = '123'; // 视频唯一标识

  

  // 创建进度跟踪器

  const tracker = new VideoProgressTracker(video, {

    videoId,

    userId,

    interval: 15000 // 15秒上报一次

  });

  

  // 获取续播位置并设置

  const resumePosition = await tracker.getResumePosition();

  if (resumePosition > 0) {

    // 询问用户是否续播

    if (confirm(`检测到您上次观看至 ${formatTime(resumePosition)},是否从此处继续播放?`)) {

      video.currentTime = resumePosition;

    }

  }

  

  video.play();

});


function formatTime(seconds) {

  const hours = Math.floor(seconds / 3600);

  const minutes = Math.floor((seconds % 3600) / 60);

  const secs = Math.floor(seconds % 60);

  

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

}

</script>

```


## 四、Spring Boot后端实现


### 4.1 数据模型设计


```java

@Entity

@Table(name = "video_progress")

@Data

@NoArgsConstructor

@AllArgsConstructor

public class VideoProgress {

    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;

    

    @Column(nullable = false)

    private String userId;

    

    @Column(nullable = false)

    private String videoId;

    

    @Column(nullable = false)

    private Integer progress; // 观看进度(秒)

    

    @Column(nullable = false)

    private Integer duration; // 视频总时长(秒)

    

    @Column(nullable = false)

    private Double progressPercentage; // 进度百分比

    

    @Column(nullable = false)

    @UpdateTimestamp

    private LocalDateTime updatedAt;

    

    // 计算进度百分比

    public void calculatePercentage() {

        if (duration > 0) {

            this.progressPercentage = (double) progress / duration * 100;

        } else {

            this.progressPercentage = 0.0;

        }

    }

}


// 进度传输对象

@Data

@NoArgsConstructor

@AllArgsConstructor

public class ProgressDTO {

    @NotBlank(message = "用户标识不能为空")

    private String userId;

    

    @NotBlank(message = "视频标识不能为空")

    private String videoId;

    

    @Min(value = 0, message = "进度不能为负数")

    private Integer progress;

    

    @Min(value = 1, message = "视频时长无效")

    private Integer duration;

}

```


### 4.2 服务层实现


```java

@Service

@Slf4j

public class VideoProgressService {

    

    @Autowired

    private VideoProgressRepository progressRepository;

    

    /**

     * 保存或更新观看进度

     */

    @Transactional

    public VideoProgress saveProgress(ProgressDTO progressDTO) {

        // 查找现有记录

        Optional<VideoProgress> existingProgress = progressRepository

            .findByUserIdAndVideoId(progressDTO.getUserId(), progressDTO.getVideoId());

vb.a6v1.cn\gs.a6v1.cn\ij.a6v1.cn

        

        VideoProgress progress;

        

        if (existingProgress.isPresent()) {

            // 更新现有记录

            progress = existingProgress.get();

            

            // 避免频繁更新:仅当进度差异大于10秒或视频结束时更新

            int timeDiff = Math.abs(progressDTO.getProgress() - progress.getProgress());

            if (timeDiff < 10 && progressDTO.getProgress() < progressDTO.getDuration()) {

                return progress;

            }

            

            progress.setProgress(progressDTO.getProgress());

            progress.setDuration(progressDTO.getDuration());

        } else {

            // 创建新记录

            progress = new VideoProgress();

            progress.setUserId(progressDTO.getUserId());

            progress.setVideoId(progressDTO.getVideoId());

            progress.setProgress(progressDTO.getProgress());

            progress.setDuration(progressDTO.getDuration());

        }

        

        // 计算进度百分比

        progress.calculatePercentage();

        

        // 保存到数据库

        VideoProgress savedProgress = progressRepository.save(progress);

        log.info("进度保存成功:用户{},视频{},进度{}秒", 

                 progressDTO.getUserId(), progressDTO.getVideoId(), progressDTO.getProgress());

        

        return savedProgress;

    }

    

    /**

     * 获取观看进度

     */

    public Optional<VideoProgress> getProgress(String userId, String videoId) {

        Optional<VideoProgress> progress = progressRepository

            .findByUserIdAndVideoId(userId, videoId);

        

        // 如果进度接近视频末尾(95%),则返回0,重新开始

        progress.ifPresent(p -> {

            if (p.getProgressPercentage() > 95.0) {

                p.setProgress(0);

                p.calculatePercentage();

            }

        });

        

        return progress;

    }

    

    /**

     * 批量获取用户的所有视频进度

     */

    public Map<String, Integer> getUserProgresses(String userId, List<String> videoIds) {

        List<VideoProgress> progresses = progressRepository

            .findByUserIdAndVideoIdIn(userId, videoIds);

        

        return progresses.stream()

            .collect(Collectors.toMap(

                VideoProgress::getVideoId,

                VideoProgress::getProgress

            ));

    }

}

```


### 4.3 控制器层


```java

@RestController

@RequestMapping("/api/video")

@Validated

public class VideoProgressController {

    

    @Autowired

    private VideoProgressService progressService;

    

    /**

     * 上报观看进度

     */

    @PostMapping("/progress")

    public ResponseEntity<?> reportProgress(@Valid @RequestBody ProgressDTO progressDTO) {

        try {

            VideoProgress progress = progressService.saveProgress(progressDTO);

            

            Map<String, Object> response = new HashMap<>();

            response.put("code", 200);

            response.put("message", "进度保存成功");

            response.put("data", Map.of(

                "progress", progress.getProgress(),

                "percentage", progress.getProgressPercentage()

            ));

            

            return ResponseEntity.ok(response);

        } catch (Exception e) {

            log.error("进度保存失败", e);

            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)

                .body(Map.of("code", 500, "message", "进度保存失败"));

        }on.a6v1.cn\pk.a6v1.cn\yt.a6v1.cn

    }

    

    /**

     * 获取续播位置

     */

    @GetMapping("/progress")

    public ResponseEntity<?> getProgress(

            @RequestParam @NotBlank String userId,

            @RequestParam @NotBlank String videoId) {

        

        Optional<VideoProgress> progressOpt = progressService.getProgress(userId, videoId);

        

        Map<String, Object> response = new HashMap<>();

        response.put("code", 200);

        response.put("message", "获取成功");

        

        if (progressOpt.isPresent()) {

            VideoProgress progress = progressOpt.get();

            response.put("data", Map.of(

                "progress", progress.getProgress(),

                "duration", progress.getDuration(),

                "percentage", progress.getProgressPercentage(),

                "updatedAt", progress.getUpdatedAt()

            ));

        } else {

            response.put("data", Map.of("progress", 0));

        }

        

        return ResponseEntity.ok(response);

    }

    

    /**

     * 批量获取视频进度

     */

    @GetMapping("/progress/batch")

    public ResponseEntity<?> getBatchProgress(

            @RequestParam @NotBlank String userId,

            @RequestParam List<String> videoIds) {

        

        Map<String, Integer> progresses = progressService

            .getUserProgresses(userId, videoIds);

        

        return ResponseEntity.ok(Map.of(

            "code", 200,

            "message", "获取成功",

            "data", progresses

        ));

    }

}

```


### 4.4 数据访问层


```java

@Repository

public interface VideoProgressRepository extends JpaRepository<VideoProgress, Long> {

    

    Optional<VideoProgress> findByUserIdAndVideoId(String userId, String videoId);

    

    List<VideoProgress> findByUserIdAndVideoIdIn(String userId, List<String> videoIds);

    

    @Modifying

    @Query("DELETE FROM VideoProgress vp WHERE vp.updatedAt < :threshold")

    int deleteOldRecords(@Param("threshold") LocalDateTime threshold);

    

    @Query("SELECT vp FROM VideoProgress vp WHERE vp.userId = :userId ORDER BY vp.updatedAt DESC")

    Page<VideoProgress> findRecentProgress(

        @Param("userId") String userId, 

        Pageable pageable

    );

}

```


## 五、进阶优化方案


### 5.1 性能优化策略


```java

@Service

@Slf4j

public class ProgressCacheService {

    

    @Autowired

    private RedisTemplate<String, Object> redisTemplate;

    

    @Autowired

    private VideoProgressService progressService;

    

    private static final String PROGRESS_KEY_PREFIX = "video:progress:";

    private static final long CACHE_TTL = 3600; // 1小时

    

    /**

     * 获取带缓存的进度

     */

    public Optional<Integer> getProgressWithCache(String userId, String videoId) {

        String cacheKey = buildCacheKey(userId, videoId);

        

        try {

            // 尝试从缓存读取

            Object cachedProgress = redisTemplate.opsForValue().get(cacheKey);

            if (cachedProgress != null) {

                return Optional.of((Integer) cachedProgress);

            }

            

            // 缓存未命中,查询数据库

            Optional<VideoProgress> progressOpt = progressService

                .getProgress(userId, videoId);

            

            if (progressOpt.isPresent()) {

                Integer progress = progressOpt.get().getProgress();

                

                // 写入缓存

                redisTemplate.opsForValue().set(

                    cacheKey, 

                    progress, 

                    CACHE_TTL, 

                    TimeUnit.SECONDS

                );

                

                return Optional.of(progress);

            }

        } catch (Exception e) {

            log.warn("缓存读取失败,降级到数据库查询", e);

            return progressService.getProgress(userId, videoId)

                .map(VideoProgress::getProgress);

        }

        

        return Optional.empty();

    }

    

    private String buildCacheKey(String userId, String videoId) {

        return PROGRESS_KEY_PREFIX + userId + ":" + videoId;

    }

}

```


### 5.2 数据清理任务


```java

@Component

@Slf4j

public class ProgressCleanupTask {

    

    @Autowired

    private VideoProgressRepository progressRepository;

    

    /**

     * 每天凌晨清理90天前的进度记录

     */

    @Scheduled(cron = "0 0 2 * * ?")

    @Transactional

    public void cleanupOldProgress() {

        LocalDateTime threshold = LocalDateTime.now().minusDays(90);

        

        try {

            int deletedCount = progressRepository.deleteOldRecords(threshold);

            log.info("清理进度记录完成,共删除{}条数据", deletedCount);

        } catch (Exception e) {

            log.error("清理进度记录失败", e);

        }

    }

}

```


## 六、总结与展望


视频续播功能通过合理的前后端协作,为用户提供流畅的观看体验。本文介绍了从进度采集、上报到存储、查询的完整实现方案。实际应用中还可以考虑以下扩展方向:


1. **多设备同步**:允许用户在不同设备间同步观看进度

2. **智能续播**:根据视频类型和用户习惯调整续播策略

3. **分布式存储**:对海量用户进度数据采用分布式存储方案

4. **进度分享**:允许用户分享特定时间点的视频内容


通过持续优化和改进,视频续播功能能够成为提升用户黏性的重要特性,为用户创造更加个性化、便捷的视频观看体验。


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/19545/viewspace-3105006/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2015-11-01

  • 博文量
    895
  • 访问量
    934067