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

使用spring boot vue 上传mp4转码为dash并播放

1.前端实现

<template><div class="video-upload"><el-uploadclass="upload-demo"action="/api/upload":before-upload="beforeUpload":on-success="handleSuccess":on-error="handleError":show-file-list="false":data="uploadData":headers="headers"><i class="el-icon-upload"></i><div class="el-upload__text">将视频文件拖到此处,或<em>点击上传</em></div></el-upload><div class="video-preview"><video :src="videoUrl" id="video" ref="videoPlayer"  controls class="w-full"></video></div><div v-if="progress > 0" class="progress-container">转码进度:<el-progress :percentage="progress" :status="progressStatus"></el-progress><p>{{ progressMessage }}</p></div></div>
</template><script>
import axios from 'axios';
import * as dashjs from 'dashjs';
import '../../node_modules/dashjs/dist/modern/esm/dash.mss.min.js';export default {name: 'HelloWorld',data() {return {timerId: null,id: null,uploadData: {title: '',description: ''},headers: {},videoUrl: '',progress: 0,progressStatus: '',progressMessage: '',playerOptions: {autoplay: false,controls: true,sources: []}};},methods: {beforeUpload(file) {const isVideo = /\.(mp4|avi|mov|mkv|flv|wmv)$/i.test(file.name);if (!isVideo) {this.$message.error('只能上传视频文件!');return false;}// 初始化上传状态this.progress = 0;this.progressStatus = '';this.progressMessage = '准备上传...';return true;},async handleSuccess(response, file) {console.log("file",file);if (response.success) {this.progress = 100;this.progressStatus = 'success';this.progressMessage = '上传成功! 转码处理中...';// 开始轮询转码状态await this.pollTranscodingStatus(response.data.taskId);} else {this.handleError(response.message);}},handleError(err) {this.progressStatus = 'exception';this.progressMessage = `上传失败: ${err.message || err}`;console.error('上传错误:', err);},async pollTranscodingStatus(taskId) {try {const res = await axios.get(`/api/transcode/status/${taskId}`);if (res.data.data.status === 'COMPLETED') {this.progressMessage = '转码完成!';          this.id = res.data.data.fileName;this.playVideo(res.data.data.fileName)} else if (res.data.data.status === 'FAILED') {this.progressStatus = 'exception';this.progressMessage = `转码失败: ${res.data.data.message}`;} else {this.progressMessage = `转码进度: ${res.data.data.progress || 0}%`;this.timerId = setTimeout(() => this.pollTranscodingStatus(taskId), 1000);}} catch (err) {this.timerId = setTimeout(() => this.pollTranscodingStatus(taskId), 1000);console.error('获取转码状态失败:', err);}},async playVideo(fileName){const videoId = fileName.substring(0,fileName.lastIndexOf('.'));this.videoUrl = "http://localhost:3000/dashVideo/dash/"+videoId+"/manifest.mpd"const player = dashjs.MediaPlayer().create();player.initialize(document.querySelector('#video'), this.videoUrl, true);}}
};
</script><style scoped>
.video-upload {padding: 20px;
}
.upload-demo {margin-bottom: 20px;
}
.video-preview {margin-top: 20px;
}
.progress-container {margin-top: 20px;
}
</style>

2前端依赖

  "dependencies": {"core-js": "^3.8.3","axios": "^0.18.0","element-ui": "^2.15.14","dashjs": "^5.0.1","vue": "^2.5.2"},

3后端实现

3.1接收文件

    @PostMapping("/upload")public ResponseEntity<?> uploadVideo(@RequestParam("file") MultipartFile file) {try {// 生成唯一文件名String originalFilename = file.getOriginalFilename(); //客户端上传时的完整文件名String extension = originalFilename.substring(originalFilename.lastIndexOf('.'));String filename = UUID.randomUUID().toString() + extension;// 上传原始文件storageService.upload(file, filename);// 创建转码任务String taskId = UUID.randomUUID().toString();TranscodeTask task = new TranscodeTask();task.setOriginalFile(filename);task.setStatus("UPLOADED");transcodeTasks.put(taskId, task);// 异步开始转码transcodeService.transcodeToDash(filename, filename.substring(0, filename.lastIndexOf('.'))).thenAccept(result -> {task.setStatus(result.isSuccess() ? "COMPLETED" : "FAILED");task.setPlayUrl(result.getPlaylistUrl());task.setMessage(result.getMessage());});Map<String,String> taskIdMap = new HashMap<>();taskIdMap.put("taskId", taskId);return ResponseEntity.ok().body(new ApiResponse(true, "上传成功dfgdf", taskIdMap));} catch (Exception e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ApiResponse(false, "上传失败: " + e.getMessage(), null));}}

3.2文件转码

@Service
public class VideoTranscodeService {@Value("${video.transcode.ffmpeg-path}")private String ffmpegPath;@Value("${video.transcode.hls-time}")private int hlsTime;@Value("${video.storage.local.path}")private String uploadLocation;@Autowiredprivate StorageService storageService;private Map<String, Double> transcodeprogress = new ConcurrentHashMap<>();// 将本地视频转码为DASH分片(多码率)@Async("asyncTranscodeExecutor")public CompletableFuture<TranscodeResult> transcodeToDash(String filename, String outputBasePath) throws Exception {String outputDir = "../dash/"+outputBasePath + "_dash";Path outputPath = Paths.get(outputDir);Files.createDirectories(outputPath);FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(uploadLocation+"/"+filename);grabber.start();int totalFrames = grabber.getLengthInFrames();System.out.println("totalFrames:"+totalFrames);String outputInitPattern = "init_$RepresentationID$.m4s";String playsegmentPath = "segment_$RepresentationID$_$Number$.m4s";String playmanifestPath = outputDir + "/manifest.mpd";List<String> commands = new ArrayList<>();commands.add(ffmpegPath);commands.add("-i");commands.add(uploadLocation+"/"+filename);commands.add("-map");commands.add("0:v");commands.add("-map");commands.add("0:a");commands.add("-c:v");commands.add("libx264");commands.add("-crf");commands.add("22");commands.add("-profile:v");commands.add("high");commands.add("-level");commands.add("4.2");commands.add("-keyint_min");commands.add("60");commands.add("-g");commands.add("60");commands.add("-sc_threshold");commands.add("0");commands.add("-b:v:0");commands.add("1000k");commands.add("-s:v:0");commands.add("1280x720");commands.add("-b:v:1");commands.add("5000k");commands.add("-s:v:1");commands.add("1920x1080");commands.add("-c:a");commands.add("aac");commands.add("-b:a");commands.add("128k");commands.add("-f");commands.add("dash");commands.add("-seg_duration");commands.add("4");commands.add("-init_seg_name");commands.add(outputInitPattern);commands.add("-media_seg_name");commands.add(playsegmentPath);commands.add(playmanifestPath);ProcessBuilder builder = new ProcessBuilder(commands);builder.redirectErrorStream(true);Process process = builder.start();// 读取输出流try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {String line;while ((line = reader.readLine()) != null) {
//                System.out.println(line); // 可以记录日志或解析进度if (line.contains("frame=")) {// 提取当前帧数int currentFrame = extractFrame(line);System.out.println("currentFrame:"+currentFrame);double progress1 = ((double) currentFrame/totalFrames) * 100;System.out.println("adasdasdasd:"+progress1);transcodeprogress.put(filename, progress1);}}}process.waitFor(); // 等待转码完成int exitCode = process.waitFor();if (exitCode != 0) {throw new RuntimeException("FFmpeg转码失败,退出码: " + exitCode);}return CompletableFuture.completedFuture(new TranscodeResult(true, "转码成功"));}//转码进度计算public double getProgress(String filename) {Double progress = transcodeprogress.get(filename);System.out.println("progress:"+progress);return progress;}private int extractFrame(String logLine) {// 正则匹配 frame= 后的数字(兼容空格和不同分隔符)Pattern pattern = Pattern.compile("frame=\\s*(\\d+)"); // 匹配示例:frame= 123 或 frame=456Matcher matcher = pattern.matcher(logLine);if (matcher.find()) {try {return Integer.parseInt(matcher.group(1)); // 提取捕获组中的数字} catch (NumberFormatException e) {throw new IllegalStateException("帧数解析失败:" + logLine);}}return 0; // 未匹配时返回默认值或抛异常}@Data@AllArgsConstructorpublic static class TranscodeResult {private boolean success;private String message;}
}

3.3转码进度查询

@GetMapping("/transcode/status/{taskId}")public ResponseEntity<?> getTranscodeStatus(@PathVariable String taskId) {TranscodeTask task = transcodeTasks.get(taskId);if (task == null) {return ResponseEntity.notFound().build();}double progres = transcodeService.getProgress(task.getOriginalFile());Map<String, Object> data = new HashMap<>();data.put("status", task.getStatus());data.put("fileName", task.getOriginalFile());data.put("message", task.getMessage());data.put("progress", progres);return ResponseEntity.ok().body(new ApiResponse(true, "查询成功", data));}

3.4视频播放

@RestController
@RequestMapping("/dashVideo")
public class DashController {@Value("${video.storage.local.path}")private String storagePath;@GetMapping("/dash/{videoId}/manifest.mpd")public ResponseEntity<Resource> getDashManifest(@PathVariable String videoId) {String pathStr = "../dash/" + videoId+"_dash/manifest.mpd";Path mpdPath = Paths.get(pathStr);Resource resource = new FileSystemResource(mpdPath);return ResponseEntity.ok().header("Content-Type", "application/dash+xml").body(resource);}@GetMapping("/dash/{videoId}/{segment}")public ResponseEntity<Resource> getSegment(@PathVariable String videoId,@PathVariable String segment) {Path segmentPath = Paths.get("../dash/"+videoId+"_dash/"+segment);Resource resource = new FileSystemResource(segmentPath);return ResponseEntity.ok().header("Content-Type", "video/mp4").body(resource);}}

注:/dash/{videoId}/manifest.mpd中manifest.mpd是固定的不能删除
MPD文件中无时:
播放器会以MPD文件自身的URL路径为基准,拼接分片文件名。
示例:
MPD文件URL: http://example.com/videos/video1/manifest.mpd
分片文件名: segment_1.m4s
实际请求URL: http://example.com/videos/video1/segment_1.m4s

3.5上传完成后未转码文件位置

在这里插入图片描述

3.6转码后文件位置

在这里插入图片描述
播放的是转码分片后的文件


http://www.mrgr.cn/news/100257.html

相关文章:

  • 【深度强化学习 DRL 快速实践】深度确定性策略梯度算法 (DDPG)
  • 【深度强化学习 DRL 快速实践】近端策略优化 (PPO)
  • 【FreeRTOS】事件标志组
  • C语言实现对哈希表的操作:创建哈希表与扩容哈希表
  • Mysql日志undo redo binlog relaylog与更新一条数据的执行过程详解
  • 软考中级-软件设计师 知识点速过1(手写笔记)
  • 大模型应用开发之LLM入门
  • 计算机组成原理-408考点-数的表示
  • 正则表达式三剑客之——awk命令
  • 大内存生产环境tomcat-jvm配置实践
  • RocketMQ 主题与队列的协同作用解析(既然队列存储在不同的集群中,那要主题有什么用呢?)---管理命令、配置安装(主题、消息、队列与 Broker 的关系解析)
  • 张 LLM提示词拓展16中方式,提示词
  • 14-DevOps-快速部署Kubernetes
  • 【2025 最新前沿 MCP 教程 01】模型上下文协议:AI 领域的 USB-C
  • YOLO12架构优化——引入多维协作注意力机制(MCAM)抑制背景干扰,强化多尺度与小目标检测性能
  • 【数据可视化-25】时尚零售销售数据集的机器学习可视化分析
  • 【深度强化学习 DRL 快速实践】异步优势演员评论员算法 (A3C)
  • MySQL数据库(基础篇)
  • 【计算机视觉】CV实战项目 - 深入解析基于HOG+SVM的行人检测系统:Pedestrian Detection
  • VScode远程连接服务器(免密登录)