1前端
<inputtype="file"accept=".mp4"ref="videoInput"@change="handleVideoChange"style="display: none;">
2生成hash
// 根據整個文件的文件名和大小組合的字符串生成hash值,大概率確定文件的唯一性fhash(file) {// console.log("哈希字段: ", file.name+file.size.toString());return new Promise(resolve => {const spark = new SparkMD5();spark.append(file.name+file.size.toString());resolve(spark.end());})},
3生成切片
// 生成切片createChunks(file) {const result = [];for (let i = 0; i < file.size; i += this.chunkSize) {result.push(file.slice(i, i + this.chunkSize));}return result;},
4查詢未上傳切片的hash,斷點續傳
// 獲取當前還沒上傳的序號 斷點續傳async askCurrentChunk(hash) {return await this.$get("/video/ask-chunk", {params: { hash: hash },headers: { Authorization: "Bearer " + localStorage.getItem("teri_token") }});},
5上傳分片
// 上傳分片async uploadChunk(formData) {return await this.$post("/video/upload-chunk", formData, {headers: {'Content-Type': 'multipart/form-data',Authorization: "Bearer " + localStorage.getItem("teri_token"),}})},
6上傳文件
async upload() {const chunks = this.createChunks(this.selectedVideo);// 向服務器查詢還沒上傳的下一個分片序號const result = await this.askCurrentChunk(this.hash);this.current = result.data.data;// 逐個上傳分片for (this.current; this.current < chunks.length; this.current++) {const chunk = chunks[this.current];const formData = new FormData();formData.append('chunk', chunk); // 將當前分片作為單獨的文件上傳formData.append('hash', this.hash);formData.append('index', this.current); // 傳遞分片索引// 發送分片到服務器try {const res = await this.uploadChunk(formData);if (res.data.code !== 200) {// ElMessage.error("分片上傳失敗");this.isFailed = true;this.isPause = true;}} catch {// ElMessage.error("分片上傳失敗");this.isFailed = true;this.isPause = true;return;}// 暫停上傳if (this.isPause) {// 取消上傳徹底刪除已上傳分片if (this.isCancel) {await this.cancelUpload(this.hash);this.isCancel = false;}return;}this.progress = Math.round(((this.current + 1) / chunks.length) * 100); // 實時改進度條}this.progress = 100; // 上傳完成再次確認為100%},
后端
1查詢hash
/*** 獲取視頻下一個還沒上傳的分片序號* @param hash 視頻的hash值* @return CustomResponse對象*/public CustomResponse askCurrentChunk(String hash) {CustomResponse customResponse = new CustomResponse();// 查詢本地// 獲取分片文件的存儲目錄File chunkDir = new File(CHUNK_DIRECTORY);// 獲取存儲在目錄中的所有分片文件File[] chunkFiles = chunkDir.listFiles((dir, name) -> name.startsWith(hash + "-"));// 返回還沒上傳的分片序號if (chunkFiles == null) {customResponse.setData(0);} else {customResponse.setData(chunkFiles.length);}// 查詢OSS當前存在的分片數量,即前端要上傳的分片序號,建議分布式系統才使用OSS存儲分片,單體系統本地存儲分片效率更高
// int counts = ossUploadUtil.countFiles("chunk/", hash + "-");
// customResponse.setData(counts);return customResponse;}
2上傳分片
/*** 上傳單個視頻分片,當前上傳到阿里云對象存儲* @param chunk 分片文件* @param hash 視頻的hash值* @param index 當前分片的序號* @return CustomResponse對象* @throws IOException*/@Overridepublic CustomResponse uploadChunk(MultipartFile chunk, String hash, Integer index) throws IOException {CustomResponse customResponse = new CustomResponse();// 構建分片文件名String chunkFileName = hash + "-" + index;// 存儲到本地// 構建分片文件的完整路徑String chunkFilePath = Paths.get(CHUNK_DIRECTORY, chunkFileName).toString();// 檢查是否已經存在相同的分片文件File chunkFile = new File(chunkFilePath);if (chunkFile.exists()) {log.warn("分片 " + chunkFilePath + " 已存在");customResponse.setCode(500);customResponse.setMessage("已存在分片文件");return customResponse;}// 保存分片文件到指定目錄chunk.transferTo(chunkFile);// 存儲到OSS,建議分布式系統才使用OSS存儲分片,單體系統本地存儲分片效率更高
// try {
// boolean flag = ossUploadUtil.uploadChunk(chunk, chunkFileName);
// if (!flag) {
// log.warn("分片 " + chunkFileName + " 已存在");
// customResponse.setCode(500);
// customResponse.setMessage("已存在分片文件");
// }
// } catch (IOException ioe) {
// log.error("讀取分片文件數據流時出錯了");
// }// 返回成功響應return customResponse;}
3合并
/*** 合并分片并將投稿信息寫入數據庫* @param vui 存放投稿信息的 VideoUploadInfo 對象*/@Transactionalpublic void mergeChunks(VideoUploadInfoDTO vui) throws IOException {String url; // 視頻最終的URL// 合并到本地
// // 獲取分片文件的存儲目錄
// File chunkDir = new File(CHUNK_DIRECTORY);
// // 獲取當前時間戳
// long timestamp = System.currentTimeMillis();
// // 構建最終文件名,將時間戳加到文件名開頭
// String finalFileName = timestamp + vui.getHash() + ".mp4";
// // 構建最終文件的完整路徑
// String finalFilePath = Paths.get(VIDEO_DIRECTORY, finalFileName).toString();
// // 創建最終文件
// File finalFile = new File(finalFilePath);
// // 獲取所有對應分片文件
// File[] chunkFiles = chunkDir.listFiles((dir, name) -> name.startsWith(vui.getHash() + "-"));
// if (chunkFiles != null && chunkFiles.length > 0) {
// // 使用流操作對文件名進行排序,防止出現先合并 10 再合并 2
// List<File> sortedChunkFiles = Arrays.stream(chunkFiles)
// .sorted(Comparator.comparingInt(file -> Integer.parseInt(file.getName().split("-")[1])))
// .collect(Collectors.toList());
// try {
System.out.println("正在合并視頻");
// // 合并分片文件
// for (File chunkFile : sortedChunkFiles) {
// byte[] chunkBytes = FileUtils.readFileToByteArray(chunkFile);
// FileUtils.writeByteArrayToFile(finalFile, chunkBytes, true);
// chunkFile.delete(); // 刪除已合并的分片文件
// }
System.out.println("合并完成!");
// // 獲取絕對路徑,僅限本地服務器
// url = finalFile.getAbsolutePath();
System.out.println(url);
// } catch (IOException e) {
// // 處理合并失敗的情況 重新入隊等
// log.error("合并視頻失敗");
// throw e;
// }
// } else {
// // 沒有找到分片文件 發通知用戶投稿失敗
// log.error("未找到分片文件 " + vui.getHash());
// return;
// }// 合并到OSS,并返回URL地址url = ossUtil.appendUploadVideo(vui.getHash());if (url == null) {return;}// 存入數據庫Date now = new Date();Video video = new Video(null,vui.getUid(),vui.getTitle(),vui.getType(),vui.getAuth(),vui.getDuration(),vui.getMcId(),vui.getScId(),vui.getTags(),vui.getDescr(),vui.getCoverUrl(),url,0,now,null);videoMapper.insert(video);VideoStats videoStats = new VideoStats(video.getVid(),0,0,0,0,0,0,0,0);videoStatsMapper.insert(videoStats);esUtil.addVideo(video);CompletableFuture.runAsync(() -> redisUtil.setExObjectValue("video:" + video.getVid(), video), taskExecutor);CompletableFuture.runAsync(() -> redisUtil.addMember("video_status:0", video.getVid()), taskExecutor);CompletableFuture.runAsync(() -> redisUtil.setExObjectValue("videoStats:" + video.getVid(), videoStats), taskExecutor);// 其他邏輯 (發送消息通知寫庫成功)}
4oss追加上傳
/*** 將本地的視頻分片文件追加合并上傳到OSS* @param hash 視頻的hash值,用于檢索對應分片* @return 視頻在OSS的URL地址* @throws IOException*/public String appendUploadVideo(@NonNull String hash) throws IOException {// 生成文件名String uuid = System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "");String fileName = uuid + ".mp4";// 完整路徑名String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date()).replace("-", "");String filePathName = date + "/video/" + fileName;ObjectMetadata meta = new ObjectMetadata();// 設置內容類型為MP4視頻meta.setContentType("video/mp4");int chunkIndex = 0;long position = 0; // 追加位置while (true) {File chunkFile = new File(CHUNK_DIRECTORY + hash + "-" + chunkIndex);if (!chunkFile.exists()) {if (chunkIndex == 0) {log.error("沒找到任何相關分片文件");return null;}break;}// 讀取分片數據FileInputStream fis = new FileInputStream(chunkFile);byte[] buffer = new byte[(int) chunkFile.length()];fis.read(buffer);fis.close();// 追加上傳分片數據try {AppendObjectRequest appendObjectRequest = new AppendObjectRequest(OSS_BUCKET, filePathName, new ByteArrayInputStream(buffer), meta);appendObjectRequest.setPosition(position);AppendObjectResult appendObjectResult = ossClient.appendObject(appendObjectRequest);position = appendObjectResult.getNextPosition();} catch (OSSException oe) {log.error("OSS出錯了:" + oe.getErrorMessage());throw oe;} catch (ClientException ce) {log.error("OSS連接出錯了:" + ce.getMessage());throw ce;}chunkFile.delete(); // 上傳完后刪除分片chunkIndex++;}return OSS_BUCKET_URL + filePathName;}