1. 项目概述

这个基于SpringBoot的视频点播系统是一个典型的B/S架构应用,我最近刚用它完成了一个在线教育平台的视频模块开发。系统采用前后端分离设计,后端使用SpringBoot 2.7.18构建RESTful API,前端可以选择Vue或uniapp框架。核心功能包括视频上传转码、分级权限管理、多终端播放适配等,特别适合需要搭建私有化视频服务的中小型企业。

在实际部署中,我发现这个架构对硬件要求不高——2核4G的云服务器就能流畅运行基础功能。系统自带的HLS切片功能很实用,实测支持同时50人观看720P视频时CPU占用率不到30%。源码包里已经集成了七牛云SDK,如果要对接其他云存储,修改配置文件就能快速切换。

2. 技术架构解析

2.1 核心组件选型

后端框架选择SpringBoot 2.7.18是经过实际验证的决策。这个长期支持版本在稳定性与功能完备性上达到了最佳平衡,我对比过2.5.x和3.x版本:

  • 2.5.x缺少对JDK17的完整支持
  • 3.x版本需要Jakarta EE 9+环境
  • 2.7.x完美兼容企业现有JDK8环境

数据库采用MySQL 8.0+InnoDB集群方案,视频元数据表设计时特别注意了以下几点:

CREATE TABLE `video_meta` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '使用雪花算法生成ID',
  `title` varchar(255) COLLATE utf8mb4_bin NOT NULL,
  `cover_url` varchar(512) COLLATE utf8mb4_bin DEFAULT NULL,
  `duration` int DEFAULT '0' COMMENT '秒为单位',
  `width` smallint DEFAULT NULL,
  `height` smallint DEFAULT NULL,
  `status` tinyint NOT NULL DEFAULT '0' COMMENT '0-转码中 1-正常 2-下架',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_status` (`status`),
  KEY `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

2.2 视频处理流水线

文件上传采用分片上传策略,前端通过WebUploader实现断点续传。后端处理流程值得注意的几个技术点:

  1. 转码服务使用FFmpeg 5.x版本,关键参数配置:
ffmpeg -i input.mp4 \
  -c:v libx264 -profile:v high -preset faster -crf 23 \
  -c:a aac -b:a 128k \
  -movflags faststart \
  -f hls -hls_time 10 -hls_list_size 0 output.m3u8
  1. 水印添加采用GPU加速方案,NVIDIA显卡需要安装对应驱动:
// 在SpringBoot中调用FFmpeg的滤镜参数
String watermarkCmd = "-vf \"movie=logo.png [wm];[in][wm] overlay=10:10 [out]\"";
  1. 关键帧提取使用OpenCV Java库,示例代码:
Mat frame = new Mat();
VideoCapture capture = new VideoCapture(filePath);
capture.read(frame);
Imgcodecs.imwrite("thumbnail.jpg", frame);

3. 关键功能实现

3.1 权限控制系统

采用RBAC模型结合JWT认证,特别注意了视频权限的细粒度控制。在权限拦截器中实现了以下逻辑:

@Aspect
@Component
public class VideoAuthAspect {
    @Before("@annotation(videoAccess)")
    public void checkAccess(VideoAccess videoAccess) {
        String videoId = RequestContextHolder.getRequestAttributes()
                           .getParameter("videoId");
        // 检查用户是否有权访问该视频
        if(!videoService.checkAccess(SecurityUtils.getUserId(), videoId)){
            throw new BusinessException(403, "无观看权限");
        }
    }
}

权限表设计采用位运算存储复合权限:

public class VideoPermission {
    public static final int VIEW = 1 << 0;    // 1
    public static final int DOWNLOAD = 1 << 1; // 2
    public static final int EDIT = 1 << 2;    // 4
    public static final int DELETE = 1 << 3;  // 8
}

3.2 多终端适配方案

针对不同终端做了如下适配处理:

  1. Web端:
  • 使用video.js播放器
  • 自适应码率切换通过HLS.js实现
  • 关键配置:
const player = videojs('video-player', {
  html5: {
    vhs: {
      overrideNative: true,
      enableLowInitialPlaylist: true
    }
  }
});
  1. 移动端:
  • uniapp封装原生播放组件
  • 缓存策略优化:
uni.downloadFile({
  url: videoUrl,
  success: (res) => {
    uni.saveVideoToPhotosAlbum({
      filePath: res.tempFilePath
    });
  }
});
  1. 微信小程序:
  • 使用 组件
  • 特别注意域名白名单配置
  • 需要在小程序后台添加业务域名

4. 性能优化实践

4.1 缓存策略设计

采用三级缓存架构:

  1. 客户端缓存:通过HTTP Cache-Control头控制
@GetMapping("/video/{id}")
public ResponseEntity<Resource> getVideo(
    @PathVariable String id,
    @RequestHeader String range) {
    
    HttpHeaders headers = new HttpHeaders();
    headers.setCacheControl("public, max-age=31536000");
    // ...其他处理逻辑
}
  1. 服务端缓存:
  • 热点视频元数据用Redis缓存
  • 使用Spring Cache抽象层:
@Cacheable(value = "videoMeta", key = "#id")
public VideoMeta getVideoMeta(String id) {
    return videoMapper.selectById(id);
}
  1. CDN加速:
  • 与阿里云OSS联动配置
  • 自动预热脚本示例:
#!/bin/bash
# 预热最近3天上传的视频
videoIds=$(mysql -uroot -p$PWD -e "SELECT id FROM video WHERE created_at > DATE_SUB(NOW(), INTERVAL 3 DAY)")
for id in $videoIds; do
    curl -X POST "https://cdn.aliyuncs.com/?Action=PushObjectCache" \
         -d "ObjectPath=video/${id}/index.m3u8"
done

4.2 数据库优化

针对视频点播场景特别优化的几个点:

  1. 分表策略:
  • 按视频ID哈希分16个表
  • 使用Sharding-JDBC配置:
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
    sharding:
      tables:
        video_play_log:
          actual-data-nodes: ds$->{0..1}.video_play_log_$->{0..15}
          table-strategy:
            inline:
              sharding-column: video_id
              algorithm-expression: video_play_log_$->{video_id % 16}
  1. 索引优化:
  • 为播放记录表添加复合索引:
ALTER TABLE video_play_log ADD INDEX idx_user_video (user_id, video_id);
  1. 慢查询监控:
  • 配置MyBatis拦截器:
@Intercepts(@Signature(type= StatementHandler.class, 
        method="query", 
        args={Statement.class, ResultHandler.class}))
public class SlowQueryInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = invocation.proceed();
        long time = System.currentTimeMillis() - start;
        if(time > 1000) {
            log.warn("Slow query detected: {}ms - {}", 
                    time, 
                    invocation.getArgs()[0]);
        }
        return result;
    }
}

5. 部署与监控

5.1 容器化部署

使用Docker Compose编排服务,关键配置要点:

version: '3.8'
services:
  app:
    image: openjdk:11-jre
    ports:
      - "8080:8080"
    volumes:
      - ./logs:/app/logs
    environment:
      - SPRING_PROFILES_ACTIVE=prod
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  ffmpeg:
    image: jrottenberg/ffmpeg:5.1-alpine
    volumes:
      - ./videos:/data
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G

5.2 监控体系搭建

  1. Prometheus监控指标暴露:
@Bean
public MeterRegistryCustomizer<PrometheusMeterRegistry> configurer(
    @Value("${spring.application.name}") String appName) {
    return registry -> registry.config().commonTags("application", appName);
}
  1. 关键业务指标埋点:
@GetMapping("/play/{id}")
@Timed(value = "video.play.time", description = "Time taken to process play request")
@Counted(value = "video.play.count", description = "Total play requests")
public ResponseEntity<VideoPlayResponse> playVideo(
    @PathVariable String id) {
    // 业务逻辑
}
  1. 日志收集方案:
  • 使用ELK Stack
  • Logback配置示例:
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
    <destination>logstash:5044</destination>
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
        <customFields>{"app":"video-service","env":"${spring.profiles.active}"}</customFields>
    </encoder>
</appender>

6. 常见问题排查

6.1 转码失败处理

典型错误场景及解决方案:

  1. 编码器不支持:
Error while opening encoder for output stream #0:0 - maybe incorrect parameters

解决方法:

# 查看支持的编码器
ffmpeg -encoders | grep h264
# 安装额外编码器
apt install libx264-dev
  1. 内存不足:
malloc: can't allocate region

解决方法:

  • 增加JVM堆内存: -Xmx2g
  • 限制FFmpeg进程内存: ulimit -v 1000000
  1. 文件权限问题:
Permission denied when trying to write to output file

解决方法:

chmod 777 /data/transcode
chown -R app:app /data

6.2 播放卡顿优化

通过以下几个步骤定位问题:

  1. 检查网络带宽:
# 服务器端检测
iftop -i eth0 -n -P
  1. 分析HLS切片:
ffprobe -i playlist.m3u8 -show_frames -select_streams v -print_format json
  1. 客户端调试:
videojs.getPlayer('my-player').on('error', (err) => {
    console.error('Player error:', err);
});

优化建议:

  • 降低初始分辨率
  • 调整关键帧间隔
  • 启用QUIC协议传输

7. 扩展开发建议

7.1 弹幕功能实现

基于WebSocket的实时弹幕方案:

  1. 服务端配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/danmaku")
                .setAllowedOrigins("*")
                .withSockJS();
    }
}
  1. 前端集成:
const socket = new SockJS('/danmaku');
const stompClient = Stomp.over(socket);
stompClient.connect({}, () => {
    stompClient.subscribe('/topic/comments', (message) => {
        showDanmaku(JSON.parse(message.body));
    });
});

7.2 AI功能扩展

  1. 内容审核集成:
// 阿里云内容安全API调用示例
public boolean checkVideoContent(File videoFile) {
    DefaultProfile profile = DefaultProfile.getProfile(
        "cn-shanghai", 
        accessKeyId, 
        accessKeySecret);
    IAcsClient client = new DefaultAcsClient(profile);
    
    SubmitAsyncScanRequest request = new SubmitAsyncScanRequest();
    request.setVideoUrl(uploadToOSS(videoFile));
    // ...其他参数设置
    
    try {
        SubmitAsyncScanResponse response = client.getAcsResponse(request);
        return "pass".equals(response.getData().getResult());
    } catch (Exception e) {
        throw new RuntimeException("审核失败", e);
    }
}
  1. 智能推荐算法:
# 协同过滤推荐示例
from surprise import Dataset, KNNBasic

def train_recommend_model():
    data = Dataset.load_builtin('ml-100k')
    trainset = data.build_full_trainset()
    sim_options = {'name': 'cosine', 'user_based': False}
    algo = KNNBasic(sim_options=sim_options)
    algo.fit(trainset)
    return algo

在实际项目中,我建议先用简单的基于热度的推荐策略,等用户行为数据积累到10万条以上再引入机器学习算法。初期可以这样实现:

SELECT v.* 
FROM video v
JOIN (
    SELECT video_id, COUNT(*) as play_count
    FROM video_play_log
    WHERE created_at > NOW() - INTERVAL 7 DAY
    GROUP BY video_id
    ORDER BY play_count DESC
    LIMIT 10
) hot ON v.id = hot.video_id
Logo

邀请您加入社区

更多推荐