SpringBoot视频点播系统生产级实现指南
简介:这是一套基于SpringBoot的Java视频点播系统完整课程设计源码,面向计算机专业本科生、Java初学者及Web全栈学习者,聚焦于后端开发、数据库交互与多媒体业务集成能力训练。资源包含312个文件,涵盖76个Java核心业务类(Controller/Service/Repository分层清晰)、59个Vue前端组件(实现视频浏览、搜索与播放界面)、40个JS与19个CSS文件支撑交互逻辑与样式,另有SQL建表脚本、YML配置、MP4演示视频及多尺寸图片资源,整体包体16.92MB,结构完整、开箱即用。已有45人学习下载,适合用于课程设计答辩、毕业设计参考或SpringBoot工程化实践。读者可直接获取含用户管理、视频上传存储、多清晰度转码适配、Redis缓存优化及Spring Security权限控制的全流程实现,代码注释充分,目录模块划分明确,配套SQL与部署说明便于快速本地运行与二次开发。
1. 为什么一个 SpringBoot 视频点播系统,不能只靠“能跑起来”就交差?
很多 Java 开发者在做毕业设计或内部 PoC 时,看到“SpringBoot-Java视频点播系统设计与实现.zip”这个标题,第一反应是解压、 mvn clean install 、 java -jar target/*.jar ,然后打开浏览器输入 http://localhost:8080 —— 页面出来了,上传个 MP4 能播,就以为完成了。但真实业务场景里,这连及格线都没摸到:用户上传 500MB 视频卡死?HLS 切片没生成,移动端直接黑屏?同一视频并发 200+ 请求,CPU 拉满、磁盘 IO 告警?后台管理页连视频封面都没自动提取?更别说权限控制漏掉管理员删除他人视频、HTTP 头缺失导致跨域失败、日志里全是 NullPointerException 却找不到源头……这个标题背后不是“做个能播的网页”,而是要落地一套 可监控、可伸缩、可运维、符合生产级媒体服务规范的 Java 后端服务 。它面向的是需要交付、上线、被 QA 和运维盯住的开发者,不是仅满足课程答辩的演示工程。核心矛盾从来不是“能不能播”,而是“播得稳不稳、查得清不清、扩得快不快、改得安不安”。接下来,我们就从 SpringBoot 的能力边界出发,一层层把视频点播的骨架搭实、血肉填满、神经接通。
2. 用 SpringBoot 构建点播服务的最小可行架构:为什么选 WebMvc + FFmpeg + LocalStorage 而非全栈云方案
2.1 架构选型必须回答三个现实问题:部署成本、调试确定性、技术栈收敛性
很多初学者一上来就想集成阿里云 OSS、腾讯云点播 API 或自建 SRS 流媒体服务器,结果卡在 AK/SK 配置、跨域策略、回调地址白名单、HTTPS 证书绑定上,两周过去连本地上传都跑不通。而本项目标题明确指向“设计与实现”,意味着 所有组件必须可本地复现、可单机验证、可断网调试 。SpringBoot 本身不处理音视频编解码,也不提供流式分发能力,但它作为胶水层,能精准调度外部工具、统一管理元数据、暴露 RESTful 接口。因此我们采用三层极简架构:
- 接入层 :SpringBoot WebMvc(非 WebFlux,因点播文件 I/O 密集,阻塞式更易控)
- 处理层 :FFmpeg CLI(版本 6.1,静态编译版,避免系统依赖冲突)
- 存储层 :本地文件系统(
/data/vod/),配合ResourceHandler直接映射 HTTP 路径,规避对象存储 SDK 版本兼容陷阱
提示:不要用
spring-boot-starter-webflux替代spring-boot-starter-web。WebFlux 在大文件上传时需额外配置DataBuffer大小和超时,且 FFmpeg 进程调用本质是阻塞操作,强行 Reactive 反而增加线程调度开销和内存泄漏风险。
2.2 初始化 SpringBoot 工程:避开 JDK 17+ 和 SpringBoot 3.x 的常见陷阱
当前热词中高频出现“springboot版本太高”,确为痛点。本项目需稳定运行 FFmpeg 进程并支持 multipart/form-data 大文件上传,推荐组合:
# 使用 Spring Initializr 创建时关键选项:
# Group: com.example.vod
# Artifact: vod-server
# Spring Boot: 2.7.18(LTS,兼容 JDK 8–17)
# Dependencies: Spring Web, Spring Data JPA, Lombok, Validation
若强制使用 SpringBoot 3.x(如面试题常考的 @ConfigurationProperties 新语法),则必须升级至 JDK 17+,且 FFmpeg 调用需显式指定 ProcessBuilder.inheritIO() 避免子进程日志丢失:
// SpringBoot 3.x 兼容写法(JDK 17+)
ProcessBuilder pb = new ProcessBuilder("ffmpeg", "-i", inputPath, "-c:v", "libx264", "-f", "mp4", outputPath);
pb.inheritIO(); // 关键!否则 FFmpeg 错误输出无法捕获
Process process = pb.start();
int exitCode = process.waitFor();
而 SpringBoot 2.7.x 下可直接用 Runtime.getRuntime().exec() ,代码更直白:
// SpringBoot 2.7.x 推荐写法(JDK 8+)
String[] cmd = {
"ffmpeg", "-y",
"-i", input.getAbsolutePath(),
"-vcodec", "libx264",
"-acodec", "aac",
"-f", "mp4",
output.getAbsolutePath()
};
Process proc = Runtime.getRuntime().exec(cmd);
BufferedReader errorReader = new BufferedReader(
new InputStreamReader(proc.getErrorStream(), StandardCharsets.UTF_8)
);
String line;
while ((line = errorReader.readLine()) != null) {
log.warn("FFmpeg error: {}", line); // 实时捕获错误,便于定位编码失败原因
}
int exitCode = proc.waitFor();
2.3 视频上传接口的健壮性设计:不只是 @RequestParam MultipartFile file
单纯接收 MultipartFile 会导致 OOM(上传 1GB 文件时 JVM 堆内存爆掉)。必须启用流式上传并限制单文件大小:
# application.yml
spring:
servlet:
context-path: /api
servlet:
multipart:
max-file-size: 2GB
max-request-size: 2GB
# 关键:禁用内存缓冲,强制写入临时磁盘
file-size-threshold: 0
server:
tomcat:
# Tomcat 默认 2MB,必须调大
max-swallow-size: -1
connection-timeout: 600000 # 10分钟超时,避免大文件上传中断
对应 Controller 层需手动处理输入流,而非依赖 MultipartFile.transferTo() :
@PostMapping("/upload")
public ResponseEntity<UploadResult> uploadVideo(@RequestParam String title,
@RequestParam String category,
HttpServletRequest request) throws IOException {
// 1. 从 request 获取原始 InputStream(绕过 MultipartFile 内存拷贝)
ServletInputStream inputStream = request.getInputStream();
String originalFilename = getOriginalFilename(request); // 从 Content-Disposition 解析
// 2. 生成唯一存储路径(避免中文路径乱码)
String safeName = FilenameUtils.normalize(originalFilename, true);
String videoId = UUID.randomUUID().toString().replace("-", "");
Path storagePath = Paths.get("/data/vod/raw/", videoId + "_" + safeName);
// 3. 流式写入磁盘(不加载全量到内存)
Files.createDirectories(storagePath.getParent());
try (FileOutputStream fos = new FileOutputStream(storagePath.toFile())) {
inputStream.transferTo(fos);
}
// 4. 异步触发转码(避免阻塞 HTTP 线程)
CompletableFuture.runAsync(() -> transcodeVideo(videoId, storagePath));
return ResponseEntity.ok(new UploadResult(videoId, "uploading"));
}
2.3.1 getOriginalFilename() 的安全实现:防御恶意文件名
private String getOriginalFilename(HttpServletRequest request) {
String contentDisposition = request.getHeader("Content-Disposition");
if (contentDisposition == null) return "unknown.mp4";
Pattern pattern = Pattern.compile("filename[^;]*=\\s*\"([^\"]*)\"|filename[^;]*=\\s*([^;]*)");
Matcher matcher = pattern.matcher(contentDisposition);
String filename = matcher.find() ? matcher.group(1) != null ? matcher.group(1) : matcher.group(2) : "unknown.mp4";
// 移除路径遍历字符(..、/、\)和控制字符
return filename.replaceAll("[\\\\/\\x00-\\x1f\\x7f]", "_").replaceAll("\\.{2,}", "_");
}
3. 视频转码与 HLS 切片:用 FFmpeg 命令链打通点播核心链路
3.1 为什么必须生成 HLS(.m3u8 + .ts)而非仅提供 MP4 直播链接?
热词中“springboot vue前后端分离”高频出现,意味着前端大概率用 video.js 或 hls.js 播放。而现代浏览器(Chrome/Firefox/Safari)对 MP4 的 range 请求支持虽好,但 无首帧预加载、无自适应码率、无 CDN 缓存友好性 。HLS 是事实标准: .m3u8 是播放清单, .ts 是分片视频,CDN 可缓存每个 .ts 文件,用户拖动进度时只需加载对应分片,而非整个 MP4。SpringBoot 不负责生成这些文件,但必须可靠调用 FFmpeg 完成。
3.2 生产可用的 FFmpeg 转码命令模板(含关键参数说明)
以下命令在 CentOS 7/Ubuntu 22.04 实测通过,适配主流硬件:
ffmpeg -y \
-i "/data/vod/raw/abc123_video.mp4" \
-c:v libx264 \
-preset medium \
-crf 23 \
-maxrate 1500k \
-bufsize 3000k \
-vf "scale='if(gt(iw,ih),min(1280,iw),-2)':'if(gt(iw,ih),-2,min(720,ih))'" \
-c:a aac \
-b:a 128k \
-ac 2 \
-ar 44100 \
-f hls \
-hls_time 10 \
-hls_list_size 0 \
-hls_segment_filename "/data/vod/hls/abc123_%05d.ts" \
"/data/vod/hls/abc123.m3u8"
| 参数 | 说明 | 为什么必须设 |
|---|---|---|
-preset medium | 编码速度/质量平衡点 | ultrafast 画质差、 slow 耗时长, medium 是生产默认值 |
-crf 23 | 恒定质量因子(0-51,越小越好) | 18 画质过优浪费带宽, 28 压缩过度, 23 是 YouTube 推荐值 |
-maxrate 1500k + -bufsize 3000k | VBR(可变码率)控制 | 防止瞬时码率飙升导致卡顿, bufsize 应为 maxrate 的 2 倍 |
-vf scale=... | 自适应分辨率缩放 | 保证宽高比不变,最大宽度 1280px/高度 720px,避免手机端拉伸 |
-hls_time 10 | 每个 .ts 分片时长(秒) | <5s 增加 HTTP 请求压力, >15s 拖动延迟高, 10s 是 CDN 友好值 |
-hls_list_size 0 | 保留全部 .ts 文件 | 避免 HLS 清理旧分片导致播放中断 |
3.3 SpringBoot 中安全执行 FFmpeg 并捕获进度
FFmpeg 执行耗时长(1GB 视频约 3–8 分钟),需异步 + 进度上报:
private void transcodeVideo(String videoId, Path rawPath) {
try {
Path hlsDir = Paths.get("/data/vod/hls/", videoId);
Files.createDirectories(hlsDir);
String m3u8Path = hlsDir.resolve(videoId + ".m3u8").toString();
String tsPattern = hlsDir.resolve(videoId + "_%05d.ts").toString();
String[] cmd = {
"ffmpeg", "-y",
"-i", rawPath.toString(),
"-c:v", "libx264", "-preset", "medium", "-crf", "23",
"-maxrate", "1500k", "-bufsize", "3000k",
"-vf", "scale='if(gt(iw,ih),min(1280,iw),-2)':'if(gt(iw,ih),-2,min(720,ih))'",
"-c:a", "aac", "-b:a", "128k", "-ac", "2", "-ar", "44100",
"-f", "hls", "-hls_time", "10", "-hls_list_size", "0",
"-hls_segment_filename", tsPattern, m3u8Path
};
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectErrorStream(true); // 合并 stdout/stderr,便于解析进度
Process proc = pb.start();
// 实时读取 FFmpeg 输出(含进度百分比)
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(proc.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("frame=")) {
// 解析如:frame= 2345 fps= 24.1 q=-1.0 size= 123456kB time=00:01:32.45 bitrate=1234.5kbits/s
double progress = parseProgress(line);
updateTranscodeProgress(videoId, progress); // 存入 Redis 或 DB
}
}
}
int exitCode = proc.waitFor();
if (exitCode == 0) {
markTranscodeSuccess(videoId, m3u8Path);
} else {
markTranscodeFailed(videoId, "FFmpeg exit code: " + exitCode);
}
} catch (Exception e) {
log.error("Transcode failed for {}", videoId, e);
markTranscodeFailed(videoId, e.getMessage());
}
}
private double parseProgress(String ffmpegOutput) {
// 匹配 time=00:01:32.45 提取总秒数
Pattern p = Pattern.compile("time=(\\d{2}):(\\d{2}):(\\d{2}\\.\\d{2})");
Matcher m = p.matcher(ffmpegOutput);
if (m.find()) {
int hours = Integer.parseInt(m.group(1));
int mins = Integer.parseInt(m.group(2));
double secs = Double.parseDouble(m.group(3));
return (hours * 3600 + mins * 60 + secs) / DURATION_ESTIMATE; // 需预估总时长
}
return 0.0;
}
注意:
DURATION_ESTIMATE不能硬编码。应在上传后先用ffprobe快速获取时长:ffprobe -v quiet -show_entries format=duration -of csv=p=0 input.mp4,再启动转码。否则进度条不准。
4. 视频元数据管理与播放地址生成:用 JPA + MySQL 实现可查询的点播目录
4.1 数据库表设计必须覆盖点播业务核心维度
仅存 video_id , file_path 远不够。用户要按分类筛选、按热度排序、看播放统计、查审核状态。MySQL 表结构如下(Spring Data JPA Entity):
@Entity
@Table(name = "vod_video")
@Data
public class VideoEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "video_id", unique = true, nullable = false, length = 32)
private String videoId; // UUID,非自增ID,防爬虫遍历
@Column(name = "title", nullable = false, length = 200)
private String title;
@Column(name = "category", nullable = false, length = 50)
private String category; // "education", "entertainment", "tech"
@Column(name = "status", nullable = false, length = 20)
@Enumerated(EnumType.STRING)
private VideoStatus status; // UPLOADED, TRANSCODING, READY, FAILED
@Column(name = "duration_sec", nullable = false)
private Integer durationSec; // 秒数,用于前端显示时长
@Column(name = "cover_url", length = 500)
private String coverUrl; // /api/cover/{videoId}.jpg
@Column(name = "hls_m3u8_url", length = 500)
private String hlsM3u8Url; // /api/hls/{videoId}.m3u8
@Column(name = "view_count", nullable = false)
private Long viewCount = 0L;
@Column(name = "created_at", nullable = false, updatable = false)
@CreationTimestamp
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
@UpdateTimestamp
private LocalDateTime updatedAt;
}
public enum VideoStatus {
UPLOADED, TRANSCODING, READY, FAILED
}
4.2 播放地址生成规则:拒绝硬编码路径,用 SpringBoot ResourceHandler 统一托管
前端请求 /api/hls/abc123.m3u8 ,不应由 Controller 返回文件流(性能差),而应交由 SpringBoot 静态资源处理器:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 映射 /api/hls/** 到 /data/vod/hls/ 目录
registry.addResourceHandler("/api/hls/**")
.addResourceLocations("file:/data/vod/hls/")
.setCachePeriod(3600); // 缓存 1 小时,减少磁盘读
// 映射封面图
registry.addResourceHandler("/api/cover/**")
.addResourceLocations("file:/data/vod/cover/")
.setCachePeriod(86400); // 封面图长期缓存
}
}
这样, VideoEntity.hlsM3u8Url 字段只需存相对路径 /api/hls/abc123.m3u8 ,前端直接 <video src="/api/hls/abc123.m3u8"> 即可。 避免 Controller 层做 ResponseEntity<Resource> 包装,那是反模式 。
4.3 查询接口的性能优化:分页 + 条件 + 缓存
用户列表页需支持分类筛选、按热度排序、分页:
@Repository
public interface VideoRepository extends JpaRepository<VideoEntity, Long> {
// JPQL 自定义查询,避免 N+1
@Query("SELECT v FROM VideoEntity v WHERE v.status = 'READY' " +
"AND (:category IS NULL OR v.category = :category) " +
"ORDER BY v.viewCount DESC")
Page<VideoEntity> findReadyVideos(@Param("category") String category, Pageable pageable);
// 精确查询单个视频(用于播放页)
Optional<VideoEntity> findByVideoId(String videoId);
}
Controller 层开启二级缓存(Ehcache 或 Caffeine):
@Service
public class VideoService {
@Cacheable(value = "videos", key = "#videoId", unless = "#result == null")
public VideoEntity getVideoById(String videoId) {
return videoRepository.findByVideoId(videoId)
.orElseThrow(() -> new RuntimeException("Video not found: " + videoId));
}
}
# application.yml 启用缓存
spring:
cache:
type: caffeine
cache-names: videos
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m
5. 播放统计与防盗链:用拦截器 + Referer 校验守住流量出口
5.1 为什么播放统计不能只靠前端埋点?
用户刷新页面、关闭标签页、网络中断都会导致统计不准。必须在服务端记录每次 .m3u8 和 .ts 请求:
@Component
public class VideoAccessInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String path = request.getRequestURI();
if (path.startsWith("/api/hls/")) {
String videoId = extractVideoIdFromPath(path);
if (videoId != null) {
// 记录访问(异步,避免阻塞)
CompletableFuture.runAsync(() -> {
videoStatsService.incrementViewCount(videoId);
// 可扩展:记录 IP、User-Agent、Referer
});
}
}
return true;
}
private String extractVideoIdFromPath(String path) {
// /api/hls/abc123.m3u8 → abc123
Pattern p = Pattern.compile("/api/hls/([a-zA-Z0-9]+)\\.m3u8");
Matcher m = p.matcher(path);
return m.find() ? m.group(1) : null;
}
}
注册拦截器:
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new VideoAccessInterceptor())
.excludePathPatterns("/api/upload", "/api/health");
}
}
5.2 防盗链:用 Referer 白名单堵住外站盗链
热词中“java知道了服务器的公网怎么访问”暗示了公网暴露风险。若不限制 Referer,别人只需在自己网站 <video src="https://your-domain.com/api/hls/xxx.m3u8"> 就能白嫖你的带宽:
@Component
public class RefererCheckFilter implements Filter {
private static final Set<String> ALLOWED_REFERERS = Set.of(
"https://your-frontend.com",
"http://localhost:3000",
"http://127.0.0.1:3000"
);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String referer = httpRequest.getHeader("Referer");
String path = httpRequest.getRequestURI();
if (path.startsWith("/api/hls/") && referer != null) {
boolean allowed = false;
for (String domain : ALLOWED_REFERERS) {
if (referer.startsWith(domain)) {
allowed = true;
break;
}
}
if (!allowed) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Referer not allowed");
return;
}
}
chain.doFilter(request, response);
}
}
提示:生产环境必须用 Nginx 做前置 Referer 校验(更高效),Java 层拦截器仅作兜底。Nginx 配置示例:
location /api/hls/ { valid_referers none blocked server_names ~\.your-frontend\.com; if ($invalid_referer) { return 403; } alias /data/vod/hls/; }
5.3 封面图自动生成:用 FFmpeg 抽帧 + Java 图像处理
用户上传视频后,系统应自动生成封面(第 1 秒关键帧),而非让用户手动上传:
public void generateCover(String videoId, Path videoPath) {
try {
Path coverDir = Paths.get("/data/vod/cover/");
Files.createDirectories(coverDir);
Path coverPath = coverDir.resolve(videoId + ".jpg");
// 抽取第 1 秒帧
String[] cmd = {
"ffmpeg", "-y",
"-ss", "00:00:01.000", // 精确到毫秒
"-i", videoPath.toString(),
"-vframes", "1",
"-q:v", "2", // 质量 2(1-31,越小越好)
coverPath.toString()
};
Process proc = Runtime.getRuntime().exec(cmd);
proc.waitFor();
// 可选:用 Thumbnailator 缩放为 320x180(适配移动端)
Thumbnails.of(coverPath.toFile())
.size(320, 180)
.outputQuality(0.9)
.toFile(coverPath.toFile());
// 更新数据库
videoRepository.findByVideoId(videoId)
.ifPresent(v -> {
v.setCoverUrl("/api/cover/" + videoId + ".jpg");
videoRepository.save(v);
});
} catch (Exception e) {
log.error("Cover generation failed for {}", videoId, e);
}
}
6. 生产部署 checklist:从开发机到 Linux 服务器的 7 个必调项
6.1 JVM 参数:针对 I/O 密集型服务的堆外内存与 GC 优化
SpringBoot 默认 JVM 参数不适合视频服务。 -Xmx 设太大反而触发 Full GC,太小则 FFmpeg 进程创建失败:
# 启动脚本 start.sh
#!/bin/bash
java \
-Xms512m -Xmx1024m \ # 堆内存 0.5–1G 足够,避免大堆 GC
-XX:MetaspaceSize=256m \ # 元空间初始值
-XX:+UseG1GC \ # G1 垃圾收集器,低延迟
-XX:MaxGCPauseMillis=200 \ # GC 暂停目标 200ms
-Dfile.encoding=UTF-8 \ # 防止中文路径乱码
-Dsun.net.client.defaultConnectTimeout=60000 \
-Dsun.net.client.defaultReadTimeout=60000 \
-jar vod-server.jar \
--spring.profiles.active=prod
6.2 Linux 系统级调优:突破文件句柄与进程限制
# /etc/security/limits.conf
* soft nofile 65536
* hard nofile 65536
* soft nproc 65536
* hard nproc 65536
# /etc/sysctl.conf
fs.file-max = 2097152
net.core.somaxconn = 65535
vm.swappiness = 1
6.3 FFmpeg 安装验证:确保静态编译版无 glibc 依赖
# 在目标服务器执行
ldd $(which ffmpeg) | grep "not found" # 若有输出,则需换静态版
# 推荐下载:https://johnvansickle.com/ffmpeg/ (x86_64 静态版)
wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
tar -xf ffmpeg-release-amd64-static.tar.xz
sudo cp ffmpeg-*/ffmpeg /usr/local/bin/
6.4 日志分级:区分业务日志与 FFmpeg 调试日志
# logback-spring.xml
<configuration>
<appender name="FFMPEG_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/vod/ffmpeg.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/vod/ffmpeg.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
</appender>
<logger name="com.example.vod.service.FFmpegService" level="WARN" additivity="false">
<appender-ref ref="FFMPEG_LOG"/>
</logger>
</configuration>
6.5 健康检查端点:让运维能快速判断服务状态
@RestController
@RequestMapping("/actuator")
public class HealthController {
@GetMapping("/health")
public Map<String, Object> health() {
Map<String, Object> result = new HashMap<>();
result.put("status", "UP");
result.put("timestamp", System.currentTimeMillis());
result.put("disk_free", FileStoreUtil.getFreeSpace("/data/vod")); // 自定义工具类
result.put("ffmpeg_version", getFFmpegVersion()); // 执行 ffmpeg -version
result.put("db_status", checkDatabaseConnection());
return result;
}
}
6.6 文件存储权限:避免 java.io.IOException: Permission denied
# 创建专用用户运行服务
sudo useradd -r -s /bin/false voduser
sudo chown -R voduser: /data/vod/
sudo chmod -R 755 /data/vod/
# 启动服务时指定用户
sudo -u voduser ./start.sh
6.7 HTTPS 强制跳转:Nginx 前置配置(非 SpringBoot 内置)
server {
listen 80;
server_name vod.your-domain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name vod.your-domain.com;
ssl_certificate /etc/letsencrypt/live/vod.your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vod.your-domain.com/privkey.pem;
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/hls/ {
alias /data/vod/hls/;
add_header Cache-Control "public, max-age=3600";
expires 1h;
}
}
更多推荐
所有评论(0)