Java视频分块上传
环境:springboot2.5.6+jdk1.8
1、在启动类中配置静态资源映射
springboot项目中配置了静态资源映射之后就启动项目之后就可以通过地址访问了
@SpringBootApplication
public class VideouploadApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(VideouploadApplication.class, args);
}
/**
* 配置静态资源映射
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
/*
http://localhost:8080/play/1.mp4
下面配置的大概意思就是,只有在url中是以play开头(如:http://localhost:8080/play)+ 文件名
就到文件夹中如(D:\private\javastudaykeshanchu\video\)找到对应的图片或视频,就可以在浏览器地址栏中显示了
*/
registry.addResourceHandler("/play/**").addResourceLocations("file:D:\\private\\javastudaykeshanchu\\video\\");
}
}
2、前端代码
这里使用了sparkmd5,对于相同文件名的视频,加密后的md5是一样的,所以上传会覆盖原来已经存在的md5加密后的文件名。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue2视频文件分片上传</title>
<!-- 生产环境版本,优化了尺寸和速度 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<el-row>
<el-button @click="dianji">默认按钮</el-button>
<el-button type="primary">主要按钮</el-button>
<el-button type="success">成功按钮</el-button>
<el-button type="info">信息按钮</el-button>
<el-button type="warning">警告按钮</el-button>
<el-button type="danger">危险按钮</el-button>
</el-row>
<el-row>
<!-- accept=".mp4,.png",这个填什么就会在文件夹中显示什么,如果只有.mp4的话,打开文件夹是没有其他文件的。:auto-upload="false"这个一定要有,否则会重复请求的 -->
<el-col :span="12">
<el-upload class="upload-demo" accept=".mp4,.png" :on-change="handleChange" :on-success="handleAvatarSuccess"
drag action="#" :auto-upload="false">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
</el-upload>
</el-col>
<el-col :span="12">
<video id="playVideos" width="320" height="240" controls muted>
<source id="playVideosss" src="" type="video/mp4">
<!-- <source src="movie.ogg" type="video/ogg"> -->
您的浏览器不支持 video 标签。
</video>
</el-col>
</el-row>
<el-row>
<el-progress style="width: 360px;" :text-inside="true" :stroke-width="26" :percentage="uploadprogress">
</el-progress>
</el-row>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
chunkSize: 1024 * 1024 * 8,
uploadprogress: 0
},
methods: {
handleAvatarSuccess(resp) {
console.log(resp)
},
dianji() {
console.log('vue和element ui样式测试')
console.log(this.message)
},
async handleChange(file, fileList) {
debugger
this.uploadprogress = 0
// 每个选集的 md5,Math.ceil向上取整,不会的话就百度
let chunks = Math.ceil(file.size / this.chunkSize);
let spark = new SparkMD5.ArrayBuffer();
for (let i = 0, start = 0; i < chunks; i++, start += this.chunkSize) {
// 文件分割是注意是file.raw.slice(start, end),这里是element ui中特有的。注意是特有的
let end = Math.min(file.size, start + this.chunkSize);
// console.log('read chunk nr', i + 1, 'of', chunks, start, end);
let data = await this.readFile(file.raw.slice(start, end));
spark.append(data);
}
// 这里spark.end()是获取视频对应的md5文件名
const md5 = spark.end() + ".mp4";
// 上传选集,分片循环上传。file.slice(start, end)
for (let i = 0, start = 0; i < chunks; i++, start += this.chunkSize) {
let end = Math.min(file.size, start + this.chunkSize);
let json = await this.upload(file.raw.slice(start, end), (i + 1), chunks, md5);
// 这里是计算百分比,因为这里循环是从0开始的,所以这里需要加1
this.uploadprogress = ((i + 1) * 100.0 / chunks).toFixed(2)
}
await this.finish(chunks, md5);
// let vdo = document.getElementById("playVideos")
// 这个是视频播放的地址路径,这里就可以根据后端的提示进行返回对应的路径了,这里需要看静态资源映射的方法。
// vdo.src = 'http://localhost:8080/play/2_1.mp4';
// vdo.play();
},
// 将文件上传成功时的回调
readFile(file) {
return new Promise((resolve, reject) => {
// 文件读取
let reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = reject;
reader.readAsArrayBuffer(file);
})
},
async upload(data, i, chunks, part) {
const form = new FormData();
// 这里的data是文件
form.append("data", data);
form.append("i", i);
form.append("chunks", chunks);
// 这里的url是文件名
form.append("filename", part);
await axios.post("http://localhost:8080/upload/uploadVideo", form)
},
// 合并分块上传的视频
async finish(chunks, part) {
const form = new FormData();
form.append("chunks", chunks);
form.append("filename", part);
const resp = await axios.post("http://localhost:8080/upload/finish", form)
}
}
})
</script>
</body>
</html>
3、后端java代码
application.yml配置
spring:
servlet:
multipart:
max-file-size: 10MB
否则会报如下错误,如果出现如下报错,记得看一下上面的配置是否合理
Controller
分块上传方法
因为这里我html使用vscode打开的,前后端分离。我并没有将html放在java的static目录下,所以这里有跨域,需要添加@CrossOrigin允许跨域配置
package com.example.controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/upload")
@CrossOrigin
public class UploadVideoController {
/**
这里注意了,上面前端使用的是FormDate的形式提交(表单),后端就不能用@RequestBody的形式接受,
应该用@RequestParam的形式接受,如这里的代码所示。这里可以通过参数接受,也可以通过一个Map集合
接受,在map前面(加不加@RequestParam都可以,这里只是说明一下应该用这种形式接收)。当然也可以用一个实体类接受,
同样(加不加@RequestParam都可以,这里只是说明一下应该用这种形式接收)
const form = new FormData();
// 这里的data是文件
form.append("data", data);
form.append("i", i);
form.append("chunks", chunks);
// 这里的url是文件名
form.append("filename", part);
*/
@RequestMapping("/uploadVideo")
public Map<String, Object> uploadVideo(int i, MultipartFile data, int chunks, String filename) throws IOException {
data.transferTo(Paths.get("D:\\private\\javastudaykeshanchu\\video\\" + filename + ".part" + i));
Map<String, Object> maps = new HashMap<>();
maps.put("filename", filename);
// 进度
double jindu = (i * 100.0 / chunks);
maps.put("jindu", jindu);
return maps;
}
}
调用uploadVideo() 方法分片上传之后得到的结果
合并删除分片上传的文件
package com.example.controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/upload")
@CrossOrigin
public class UploadVideoController {
/**
* 视频分块上传
* @param i
* @param data
* @param chunks
* @param filename
* @return
* @throws IOException
*/
@RequestMapping("/uploadVideo")
public Map<String, Object> uploadVideo(int i, MultipartFile data, int chunks, String filename) throws IOException {
data.transferTo(Paths.get("D:\\private\\javastudaykeshanchu\\video\\" + filename + ".part" + i));
Map<String, Object> maps = new HashMap<>();
maps.put("filename", filename);
// 进度
double jindu = (i * 100.0 / chunks);
maps.put("jindu", jindu);
return maps;
}
/**
* 视频合并
* @param chunks
* @param filename
* @return
* @throws Exception
*/
@RequestMapping("/finish")
public String finish(int chunks, String filename) throws Exception {
FileOutputStream os = null;
try {
os = new FileOutputStream("D:\\private\\javastudaykeshanchu\\video\\" + filename);
for (int i = 1; i <= chunks; i++) {
Path path = Paths.get("D:\\private\\javastudaykeshanchu\\video\\" + filename + ".part" + i);
// 将临时文件合并为一个文件
Files.copy(path,os);
// 删除上传分块的临时文件
Files.delete(path);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if(os != null) {
os.close();
}
}
return "http://localhost:8080/play/" + filename;
}
}
调用了合并的方法之后就可以会删除临时文件合并成完整文件了
通过地址访问就可以了
4、将文件路径配置到application.yml中
application.yml
spring:
servlet:
multipart:
max-file-size: 10MB
video-path:
D:\private\javastudaykeshanchu\video\
VideouploadApplication启动类
@SpringBootApplication
public class VideouploadApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(VideouploadApplication.class, args);
}
@Value("${video-path}")
private String videoPath;
/**
* 配置静态资源映射
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
/*
http://localhost:8080/play/1.mp4
下面配置的大概意思就是,只有在url中是以play开头(如:http://localhost:8080/play)+ 文件名
就到文件夹中如(D:\private\javastudaykeshanchu\video\)找到对应的图片或视频,就可以在浏览器地址栏中显示了
*/
// registry.addResourceHandler("/play/**").addResourceLocations("file:D:\\private\\javastudaykeshanchu\\video\\");
registry.addResourceHandler("/play/**").addResourceLocations("file:" + videoPath);
}
}
Controller
@RestController
@RequestMapping("/upload")
@CrossOrigin
public class UploadVideoController {
@Value("${video-path}")
private String videoPath;
/**
* 视频分块上传
* @param i
* @param data
* @param chunks
* @param filename
* @return
* @throws IOException
*/
@RequestMapping("/uploadVideo")
public Map<String, Object> uploadVideo(int i, MultipartFile data, int chunks, String filename) throws IOException {
data.transferTo(Paths.get(videoPath + filename + ".part" + i));
Map<String, Object> maps = new HashMap<>();
maps.put("filename", filename);
// 进度
double jindu = (i * 100.0 / chunks);
maps.put("jindu", jindu);
return maps;
}
/**
* 视频合并
* @param chunks
* @param filename
* @return
* @throws Exception
*/
@RequestMapping("/finish")
public String finish(int chunks, String filename) throws Exception {
FileOutputStream os = null;
try {
os = new FileOutputStream(videoPath + filename);
for (int i = 1; i <= chunks; i++) {
Path path = Paths.get(videoPath + filename + ".part" + i);
// 将临时文件合并为一个文件
Files.copy(path,os);
// 删除上传分块的临时文件
Files.delete(path);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if(os != null) {
os.close();
}
}
return "http://localhost:8080/play/" + filename;
}
}