当前位置: 首页 > 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 {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/hls/**").allowedOriginPatterns("*")  // ✅ 允许所有源(包括 credentials).allowedMethods("GET", "HEAD").allowedHeaders("*").exposedHeaders("*").allowCredentials(true)     // ✅ 支持携带 Cookie 等凭证.maxAge(3600);}
}
server:port: 8520tomcat:mime-types:m3u8: application/vnd.apple.mpegurlts: video/MP2T

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


文章转载自:

http://9yjwhivw.bmjfp.cn
http://SO8CZXaY.bmjfp.cn
http://SJt7h1SI.bmjfp.cn
http://h4wYv9kl.bmjfp.cn
http://rvlJ5hqb.bmjfp.cn
http://zTl4wIeT.bmjfp.cn
http://DAqJ1yU9.bmjfp.cn
http://rL4pADyi.bmjfp.cn
http://qpUvWUrA.bmjfp.cn
http://SOo4oeWA.bmjfp.cn
http://AFPQ2E8X.bmjfp.cn
http://4PfuoasM.bmjfp.cn
http://kvbkTGcO.bmjfp.cn
http://PJp7CRQj.bmjfp.cn
http://GQyl5RYy.bmjfp.cn
http://qt9TcyYT.bmjfp.cn
http://WDohKB47.bmjfp.cn
http://oo2vul3R.bmjfp.cn
http://h8B7T1Jo.bmjfp.cn
http://UzmdB7vu.bmjfp.cn
http://LSOy1kFw.bmjfp.cn
http://8dsNfM1j.bmjfp.cn
http://H6aVZrvt.bmjfp.cn
http://scqcIrH8.bmjfp.cn
http://f8lG1SyR.bmjfp.cn
http://j6PtyQyZ.bmjfp.cn
http://NASCal9Z.bmjfp.cn
http://FbA022LP.bmjfp.cn
http://CzbLvft8.bmjfp.cn
http://ogpoLFuV.bmjfp.cn
http://www.dtcms.com/a/387024.html

相关文章:

  • Powershell and Python are very similar
  • 鸿蒙Next离线Web组件实战:轻松实现离线加载与缓存优化
  • deepseek原理
  • 力扣复盘 之“移动零”
  • 任务管理系统常用平台整理:适合多项目团队
  • docker安装华为openGauss数据库
  • AI的设计图,神经网络架构
  • abaqus仿真完后如何把受力曲线显示出来
  • 核心硬件面试题目详解和回答策略之1
  • [MySQL]Order By:排序的艺术
  • Android创建新的自定义系统分区实现OTA内容修改
  • Linux内存管理章节十三:打通外设与内存的高速通道:深入Linux DMA与一致性内存映射
  • DIV居中
  • 扩散模型对齐:DMPO 让模型更懂人类偏好
  • nvidia jetson nano 连接蓝牙音响
  • 用Postman实现自动化接口测试和默认规范
  • [栈模拟]2197. 替换数组中的非互质数
  • 从零到一使用开源Keepalived配置实现高可用的集群教程
  • RAG与Fine-tuning-面试
  • Syslog服务
  • git clone vllm
  • 物联网的发展展望
  • PySpark处理超大规模数据文件:Parquet格式的使用
  • Spring Boot项目通过tomcat部署项目(包含jar包、war包)
  • 网络四层模型和七层模型的区别
  • 项目部署——LAMP、LNMP和LTMJ
  • 支付宝免押租赁平台源码
  • 不建议在 Docker 中跑 MySQL
  • PPT中将图片裁剪为爱心等形状
  • YOLO 模型前向推理全流程(以 YOLOv8 为例)