当前位置: 首页 > news >正文

完整教程:视频上传以及在线播放

完整教程:视频上传以及在线播放

大文件分片上传:

<!-- 视频上传按钮 -->
<button @click="triggerFileInput">视频上传</button><!-- 隐藏的文件输入 --><inputtype="file"ref="fileInputRef"style="display: none"@change="handleFileSelect"accept="video/*"/><script>// 触发文件选择const triggerFileInput = () =>{fileInputRef.value?.click();};// 处理文件选择const handleFileSelect = (e) =>{const file = e.target?.files?.[0];if (!file) return;preRequest(file);// 开始上传};const chunkSize = 1 * 1024 * 1024;const number = ref(null);//预请求const preRequest = async (file) =>{try {// Step 1: 计算文件基本信息const fileName = file.name;const fileSize = file.size;const fileType = file.type;const totalChunks = Math.ceil(fileSize / chunkSize);// ✅ 使用 FormData 替代 JSON.stringifyconst formData = new FormData();formData.append('fileName', fileName);formData.append('fileSize', fileSize);formData.append('fileType', fileType);formData.append('chunkSize', chunkSize);formData.append('totalChunks', totalChunks);// Step 2: 发送预请求,服务端返回 uploadId 和已上传的切片列表(用于断点续传)const preRes = await fetch('/api/api/v1/indexRatingPollution/pre', {method: 'POST',headers: {// 'Content-Type': 'application/json','Authorization': 'Bearer ' + getToken(),},body: formData});const preData = await preRes.json();console.log(preData)const { uploadId,number} = preData.data;console.log('预请求成功:', { uploadId, number});// Step 3: 开始上传切片(跳过已上传的,实现断点续传)await uploadChunks(file, uploadId, number,totalChunks);} catch (err) {console.error('预请求失败:', err);alert('上传准备失败: ' + err.message);}};const uploadChunks = async (file, uploadId, currentChunkIndex, totalChunks) =>{currentChunkIndex = Number(currentChunkIndex);//  递归终止条件:所有切片已上传完成if (currentChunkIndex >=totalChunks) {console.log('✅ 所有切片上传完成,开始合并文件...');mergeFile(uploadId);reload()// await mergeFile(uploadId, file.name);return;// 递归结束}// 计算当前切片范围const start = currentChunkIndex * chunkSize;const end = Math.min(start + chunkSize, file.size);const chunk = file.slice(start, end);// 构造 FormDataconst formData = new FormData();formData.append('file', chunk);formData.append('uploadId', uploadId);formData.append('number', currentChunkIndex);// 当前是第几片formData.append('fileName', file.name);// 建议也传,便于后端记录try {console.log(` 正在上传第 ${currentChunkIndex + 1} / ${totalChunks} 片...`);const res = await fetch('/api/api/v1/indexRatingPollution/uploadFile', {method: 'POST',headers: {'Authorization': 'Bearer ' + getToken(),// 不要手动设置 Content-Type},body: formData});const result = await res.json();if (result.success) {console.log(`✅ 第 ${currentChunkIndex + 1} 片上传成功`);// ✅ 递归:上传下一片await uploadChunks(file, uploadId, currentChunkIndex + 1, totalChunks);} else {throw new Error(result.message || '上传失败');}} catch (err) {alert(`${currentChunkIndex + 1} 片上传失败: ${err.message}`);// 可在此加入重试机制(见下方优化)}};const mergeFile = async (uploadId) =>{const formData = new FormData();formData.append('uploadId', uploadId);const res = await fetch('/api/api/v1/indexRatingPollution/mergeFile', {method: 'POST',headers: {'Authorization': 'Bearer ' + getToken()},body: formData});const result = await res.json();if (result.success) {console.log('合并成功,文件地址:', result.data);}};</script>
@PostMapping("/pre")
public Result pre(@RequestParam("fileName") String fileName,@RequestParam("fileSize") String fileSize,
@RequestParam("fileType") String fileType,@RequestParam("chunkSize") String chunkSize,
@RequestParam("totalChunks") String totalChunks){
HashMap<
String, String> map = new HashMap<
>();
map.put("fileName", fileName);
map.put("fileSize", fileSize);
map.put("fileType", fileType);
map.put("chunkSize", chunkSize);
map.put("totalChunks", totalChunks);
map.put("number", "0");
double random = Math.random();
map.put("uploadId", String.valueOf(random));
redisTemplate.opsForValue().set( String.valueOf(random), map);
return Result.success(map);
}
@PostMapping("/uploadFile")
public Result uploadFile(@RequestParam("file") MultipartFile file,
@RequestParam("uploadId") String uploadId,
@RequestParam("number") String number
){
HashMap<
String,String> hashMap = (HashMap<
String,String>) redisTemplate.opsForValue().get(uploadId);
String chunkNumber = (String) hashMap.get("number");
if(Integer.parseInt(chunkNumber) != Integer.parseInt(number)){
ExceptionTool.throwException("xx");
}else{
hashMap.put("number", String.valueOf(Integer.parseInt(number) + 1));
redisTemplate.opsForValue().set(uploadId, hashMap);
}
try {
File file1 = new File(Paths.get("C:\\code\\environment-backend\\src\\main\\resources", uploadId).toString());
if(!file1.exists()){
file1.mkdirs();
}
file.transferTo(new File(file1.getPath() +"/" +number));
} catch (IOException e) {
throw new RuntimeException(e);
}
return Result.success(hashMap);
}
@PostMapping("/mergeFile")
public Result mergeFile(@RequestParam("uploadId") String uploadId) {
// 1. 从 Redis 获取上传元信息
Object obj = redisTemplate.opsForValue().get(uploadId);
if (!(obj instanceof HashMap)) {
return Result.fail("上传会话不存在或已过期");
}
HashMap<
String, String> uploadInfo = (HashMap<
String, String>) obj;
String totalChunksStr = uploadInfo.get("totalChunks");
String originalFileName = uploadInfo.get("fileName");
if (totalChunksStr == null || originalFileName == null) {
return Result.fail("缺少必要上传信息");
}
int totalChunks;
try {
totalChunks = Integer.parseInt(totalChunksStr);
} catch (NumberFormatException e) {
return Result.fail("分片总数格式错误");
}
// 2. 检查所有分片是否存在
String chunkDirPath = "C:\\code\\environment-backend\\src\\main\\resources\\" + uploadId;
File chunkDir = new File(chunkDirPath);
if (!chunkDir.exists()) {
return Result.fail("上传目录不存在");
}
for (int i = 0; i < totalChunks; i++) {
File chunkFile = new File(chunkDir, String.valueOf(i));
if (!chunkFile.exists()) {
return Result.fail("第 " + i + " 个分片缺失,无法合并");
}
}
// 3. 准备合并输出目录和文件
String mergedOutputDir = "C:\\code\\environment-backend\\src\\main\\resources\\static\\upload\\" + uploadId + "\\";
File outDir = new File(mergedOutputDir);
if (!outDir.exists()) {
outDir.mkdirs();
}
File mergedFile = new File(outDir, originalFileName);
// 4. 开始合并分片
try (FileOutputStream fos = new FileOutputStream(mergedFile);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
byte[] buffer = new byte[8192];
// 8KB 缓冲区
for (int i = 0; i < totalChunks; i++) {
File chunkFile = new File(chunkDir, String.valueOf(i));
try (FileInputStream fis = new FileInputStream(chunkFile);
BufferedInputStream bis = new BufferedInputStream(fis)) {
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
}
}
bos.flush();
// 确保写入磁盘
} catch (IOException e) {
return Result.fail("文件合并失败: " + e.getMessage());
}
// 5. 生成 HLS 流媒体(.m3u8 + .ts)
String hlsOutputDir = "C:\\code\\environment-backend\\src\\main\\resources\\static\\hls\\"+uploadId+"\\";
String m3u8Url;
try {
m3u8Url = generateHLS(mergedFile.getAbsolutePath(), hlsOutputDir, originalFileName);
} catch (Exception e) {
return Result.fail("HLS 转换失败: " + e.getMessage());
}
// 6. 清理临时文件
for (int i = 0; i < totalChunks; i++) {
File chunkFile = new File(chunkDir, String.valueOf(i));
if (chunkFile.exists()) chunkFile.delete();
}
if (chunkDir.exists()) chunkDir.delete();
// 7. 清理 Redis 记录
redisTemplate.delete(uploadId);
// 8. 返回 HLS 播放地址(前端可直接用 hls.js 播放)
return Result.success("合并并转码成功","hls/"+uploadId+"/" +m3u8Url);
} //返回前端预览
private String generateHLS(String inputFilePath, String outputDir, String originalFileName)
throws IOException, InterruptedException {
File outDir = new File(outputDir);
if (!outDir.exists() &&
!outDir.mkdirs()) {
throw new IOException("无法创建 HLS 输出目录: " + outputDir);
}
// 去扩展名
String baseName = originalFileName.replaceFirst("\\.[^.]+$", "");
String randomSuffix = UUID.randomUUID().toString().substring(0, 8);
// 更唯一
String m3u8FileName = baseName + "_" + randomSuffix + ".m3u8";
String m3u8OutputPath = outputDir + m3u8FileName;
String segmentPattern = outputDir + baseName + "_" + randomSuffix + "_%03d.ts";
ProcessBuilder pb = new ProcessBuilder(
"ffmpeg",
"-i", inputFilePath,
"-c:v", "libx264",
"-c:a", "aac",
"-strict", "experimental",
"-preset", "medium",
"-hls_time", "10",
"-hls_list_size", "0",
"-hls_segment_filename", segmentPattern,
"-f", "hls",
m3u8OutputPath
);
Process process = pb.start();
// 读取 stdout 和 stderr //不读取的话会有问题
new Thread(() ->
{
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println( line);
}
} catch (IOException e) {
System.out.println("读取 FFmpeg stdout 流失败");
}
}).start();
new Thread(() ->
{
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println( line);
}
} catch (IOException e) {
System.out.println("读取 FFmpeg stderr 流失败");
}
}).start();
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("HLS 转换失败,FFmpeg 错误码: " + exitCode);
}
return m3u8FileName;
}
@Configuration
public class WebConfig
implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/hls/**")
.allowedOriginPatterns("*") // ✅ 允许所有源(包括 credentials)
.allowedMethods("GET", "HEAD")
.allowedHeaders("*")
.exposedHeaders("*")
.allowCredentials(true) // ✅ 支持携带 Cookie 等凭证
.maxAge(3600);
}
}
server:
port: 8520
tomcat:
mime-types:
m3u8: application/vnd.apple.mpegurl
ts: video/MP2T

返回的ts文件需要以这种形式才能播放

http://www.wxhsa.cn/company.asp?id=7614

相关文章:

  • C++ STL 常用算法
  • Gitee:中国开发者生态的成长引擎与数字化转型的加速器
  • 【IEEE出版|五邑大学主办|连续四年EI检索】第五届电子信息工程与计算机技术国际学术会议(EIECT 2025)
  • tightvnc使用记录
  • 高科战神全家软件怎么设置
  • 简单数论函数求和题目的一些技巧
  • 3519DV500 BT.1120 无法输出 59.94帧率
  • 独立做产品,做一个,还是做多个找爆款?
  • 第六届计算机工程与智能控制学术会议(ICCEIC 2025)
  • ARL(灯塔)安装步骤
  • c# grpc
  • win10任务栏频繁卡死、转圈
  • Typora Markdown 编辑快捷键大全(优化补充版)
  • 第二届数字经济与计算机科学国际学术会议(DECS 2025)
  • 文件摆渡系统案例分享:医院如何构建高效内外网文件交换通道
  • 淘天一面
  • 利用小波变换对跳频信号进行参数估计
  • 【Qt】Window环境下搭建Qt6、MSVC2022开发环境(无需提前安装Visual Studio) - 实践
  • 编写测试用例技巧
  • 牛客刷题-Day1
  • TENGJUN防水TYPE-C 16PIN连接器技术解析:从结构设计到认证标准的全面解读 - 实践
  • 第三届人工智能与自动化控制国际学术会议(AIAC 2025)
  • 图纸安全外发平台全解析
  • webshell流量 - voasem
  • 软件测试分类
  • Linux下显卡驱动简单测试
  • 大模型三阶段训练方法(LLaMa Factory)
  • 算法与数据结构 8 - 线性筛求一般积性函数
  • SpringMVC使用jasypt加密配置文件 - Commissar
  • 三行Python代码实现深度学习推理:Infery全面解析