1. 前言
前端使用DPlayer作为播放器,需要提供一个flv视频下载功能。cdn太贵且没必要,自己写一个简单地用着,同时有公网ip,可以做一个小型视频站(当然是个人用)。
本人的下载的视频全部是存在nas中的。通过nfs挂载nas中的视频到本机,通过自定义的规则拼接出在本机的映射地址,然后读取文件并输出文件流。以此方式可以部署多个做集群(nas的传输速度有限,受限于硬盘速度以及网速,集群也只能部署个3-4台,更多的是为了保证服务高可用)。企业用还是买cdn靠谱。
- 提供视频播放接口(实际就是文件下载服务)
- 支持
range
请求头(断点续传/跳跃看视频/部分缓存视频) - 须校验
referer
(暂时未做,因为还没确定前端的域名) - 涉及跨域,前端需要请求头:
"Access-Control-Allow-Origin", "*"
将*
更换为前端的域名 - 涉及大文件跨域传输,浏览器会自动发送option请求,需要拦截器处理
- 视频服务器只做读取文件并下载,保证职责单一,可部署多个做集群。
- 无须身份校验(可以让其他模块做校验,本模块通过安全通信与其他模块交互)
2. Controller代码
/**
* 简单的根据视频编号返回文件流,实际上线需求做中间跳转保证数据安全
* @param cid 视频编号
* @param request
* @param response
* @return
*/
@RequestMapping("/play/cid/{cid}")
public void playWithCid(@PathVariable String cid, HttpServletRequest request, HttpServletResponse response) {
if (StringUtils.isEmpty(cid)) {
return;
}
//根据视频编号,查出视频在本机的路径
String fullPath = videoPathService.getPathWithCid(cid);
if (Objects.isNull(fullPath)) {
return;
}
// log.info(fullPath);
flushFile(request, response, fullPath);
}
/**
* 写出文件流
* @param request
* @param response
* @param fullPath 文件在本机的路径
*/
private void flushFile(HttpServletRequest request, HttpServletResponse response, String fullPath) {
log.info("下载路径:" + fullPath);
File downloadFile = new File(fullPath);
if (!downloadFile.exists() || !downloadFile.isFile()) {
log.warn("路径错误:{}", fullPath);
return;
}
ServletContext context = request.getServletContext();
String mimeType = context.getMimeType(fullPath);
if (mimeType == null) {
mimeType = "application/octet-stream";
}
response.setContentType(mimeType);
// set headers for the response
String headerKey = "Content-Disposition";
String headerValue = String.format("attachment; filename=\"%s\"", downloadFile.getName());
response.setHeader(headerKey, headerValue);
// 解析断点续传相关信息
response.setHeader("Accept-Ranges", "bytes");
long downloadSize = downloadFile.length();
long fromPos = 0, toPos = 0;
if (request.getHeader("Range") == null) {
response.setHeader("Content-Length", downloadSize + "");
} else {
// 若客户端传来Range,从断点处下载文件,设置206状态(SC_PARTIAL_CONTENT)
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
String range = request.getHeader("Range");
String bytes = range.replaceAll("bytes=", "");
String[] ary = bytes.split("-");
fromPos = Long.parseLong(ary[0]);
if (ary.length == 2) {
toPos = Long.parseLong(ary[1]);
}
int size;
if (toPos > fromPos) {
size = (int) (toPos - fromPos);
} else {
size = (int) (downloadSize - fromPos);
}
response.setHeader("Content-Length", size + "");
downloadSize = size;
}
try (
RandomAccessFile in = new RandomAccessFile(downloadFile, "rw");
OutputStream out = response.getOutputStream()
) {
// 设置下载起始位置
if (fromPos > 0) {
in.seek(fromPos);
}
// 缓冲区大小
int bufLen = (int) (downloadSize < 2048 ? downloadSize : 2048);
byte[] buffer = new byte[bufLen];
int num;
int count = 0; // 当前写到客户端的大小
while ((num = in.read(buffer)) != -1) {
out.write(buffer, 0, num);
count += num;
//处理最后一段,计算不满缓冲区的大小
if (downloadSize - count < bufLen) {
bufLen = (int) (downloadSize - count);
if (bufLen == 0) {
break;
}
buffer = new byte[bufLen];
}
}
response.flushBuffer();
} catch (IOException e) {
log.info("数据被暂停或中断。");
}
}
3. 拦截器
拦截器:
- 前端请求跨域,需要
Access-Control-Allow-Origin
请求头,这里最好把*
改成前端的域名,保证不被盗用视频链接 - 同时拦截
OPTIONS
的请求方式,一律直接返回请求头。
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class CorsInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
response.setHeader("Access-Control-Max-Age", "86400");
response.setHeader("Access-Control-Allow-Headers", "*");
// 如果是OPTIONS则结束请求
if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
response.setStatus(HttpStatus.NO_CONTENT.value());
return false;
}
return true;
}
}
添加拦截器:
import cn.bfmiaodi.videoserver.handle.CorsInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private CorsInterceptor corsInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 跨域拦截器需放在最上面
registry.addInterceptor(corsInterceptor).addPathPatterns("/**");
}
}