09 視頻分片上傳Minio和播放

文章目錄

  • 一、流程設計
    • 1. 分片上傳實現思路
    • 2. 文件分片上傳流程
    • 3. 視頻播放流程
  • 二、代碼實現
    • 1. 后端代碼
    • 2. 文件上傳前端代碼
    • 3. 視頻播放前端代碼


一、流程設計

1. 分片上傳實現思路

在這里插入圖片描述

2. 文件分片上傳流程

在這里插入圖片描述

3. 視頻播放流程

在這里插入圖片描述

二、代碼實現

1. 后端代碼

  • pom.xml
<dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.5</version>
</dependency>
  • application.yml
spring:servlet:multipart:max-file-size: 300MBmax-request-size: 300MBminio:endpoint: http://127.0.0.1:9000 #MinIO服務所在地址accessKey: admin #訪問的keysecretKey: password #訪問的秘鑰bucketName: test #訪問的存儲桶名expiry: 86400 #過期時間
  • com.example.web.dto.file.FileResp
package com.example.web.dto.file;import com.example.web.dto.CommResp;
import java.io.Serializable;/*** @description 文件處理返回消息*/
@lombok.Setter
@lombok.Getter
public class FileResp extends CommResp implements Serializable {private static final long serialVersionUID = 1L;// 文件處理代碼private Integer code;// 文件名private String fileName;// 文件數量private Integer shardCount;// 文件MD5private String md5;// 文件訪問路徑private String fileUrl;public void setResp() {if (getCode()!=null && getCode()==200) setMsg("操作成功");if (getCode()!=null && getCode()==201) setMsg("分片上傳成功");if (getCode()!=null && getCode()==202) setMsg("所有的分片均上傳成功");if (getCode()!=null && getCode()==203) setMsg("系統異常");if (getCode()!=null && getCode()==204) setMsg("資源不存在");setPageNo(null);setPageSize(null);setTotals(null);}}
  • com.example.web.dto.file.MinioObject
package com.example.web.dto.file;import java.io.Serializable;
import java.util.Map;@lombok.Setter
@lombok.Getter
public class MinioObject implements Serializable {private static final long serialVersionUID = 1L;private String bucket;private String region;private String object;private String etag;private long size;private boolean deleteMarker;private Map<String, String> userMetadata;}
  • com.example.utils.FileMd5Util
package com.example.utils;import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;/*** @description 計算文件的Md5*/
public final class FileMd5Util {private static final int BUFFER_SIZE = 8 * 1024;private static final char[] HEX_CHARS ={'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};/*** 計算文件的輸入流*/public static String calculateMd5(InputStream inputStream) {try {MessageDigest md5MessageDigest = MessageDigest.getInstance("MD5");try (BufferedInputStream bis = new BufferedInputStream(inputStream);DigestInputStream digestInputStream = new DigestInputStream(bis, md5MessageDigest)) {final byte[] buffer = new byte[BUFFER_SIZE];while (digestInputStream.read(buffer) > 0) {md5MessageDigest = digestInputStream.getMessageDigest();}return encodeHex(md5MessageDigest.digest());} catch (IOException ioException) {throw new IllegalArgumentException(ioException.getMessage());}} catch (NoSuchAlgorithmException e) {throw new IllegalArgumentException("no md5 found");}}/*** 轉成的md5值為全小寫*/private static String encodeHex(byte[] bytes) {char[] chars = new char[32];for (int i = 0; i < chars.length; i = i + 2) {byte b = bytes[i / 2];chars[i] = HEX_CHARS[(b >>> 0x4) & 0xf];chars[i + 1] = HEX_CHARS[b & 0xf];}return new String(chars);}
}
  • com.example.utils.MinioFileUtil
package com.example.utils;import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.io.*;
import java.util.ArrayList;
import java.util.List;/*** @description minio文件操作*/
@lombok.Getter
@Component
public class MinioFileUtil {private static Log logger = LogFactory.getLog(MinioFileUtil.class);@Value("${minio.endpoint:1}")private String minioEndpoint;@Value("${minio.accessKey:1}")private String minioAccessKey;@Value("${minio.secretKey:1}")private String minioSecretKey;@Value("${minio.file-show-url:1}")private String showUrl;/*** @description 獲取minioClient*/public MinioClient getMinioClient() {return MinioClient.builder().endpoint(minioEndpoint).credentials(minioAccessKey, minioSecretKey).build();}/*** @description 將分鐘數轉換為秒數* @Param expiry 過期時間(分鐘)*/private int expiryHandle(Integer expiry) {expiry = expiry * 60;if (expiry > 604800) {return 604800;}return expiry;}/*** @description 文件上傳至指定桶容器,并返回對象文件的存儲路徑加文件名* @param inputStream    文件流* @param bucketName     桶名稱* @param directory      文件存儲目錄* @param objectName     文件名稱*/@SneakyThrowspublic String uploadObject(InputStream inputStream, String bucketName, String directory, String objectName) {if (StringUtils.isNotEmpty(directory)) {objectName = directory + "/" + objectName;}getMinioClient().putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(inputStream, inputStream.available(), -1).build());return objectName;}/*** @description 獲取訪問對象的url地址* @param bucketName     桶名稱* @param objectName     文件名稱(包含存儲目錄)* @param expiry         過期時間(分鐘) 最大為7天 超過7天則默認最大值*/@SneakyThrowspublic String getObjectUrl(String bucketName, String objectName, Integer expiry) {expiry = expiryHandle(expiry);String url = getMinioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(bucketName).object(objectName).expiry(expiry).build());if (!showUrl.equals("1") && showUrl.length()>2) {url = url.replace(minioEndpoint, showUrl);}return url;}/*** @description 獲取某個文件* @param bucketName     桶名稱* @param objectName     文件路徑*/@SneakyThrowspublic StatObjectResponse getObjectInfo(String bucketName, String objectName) {return getMinioClient().statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());}/*** @description 刪除一個對象文件* @param bucketName     桶名稱* @param objectName     文件名稱(包含存儲目錄)*/public boolean removeObject(String bucketName, String objectName) {try {getMinioClient().removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());return true;} catch (Exception e) {logger.error("removeObject error", e);return false;}}/*** @description 刪除多個對象文件* @param bucketName      桶名稱* @param objectNames     文件名稱(包含存儲目錄)*/@SneakyThrowspublic List<String> removeObjects(String bucketName, List<String> objectNames) {if (!bucketExists(bucketName)) {return new ArrayList<>();}List<String> deleteErrorNames = new ArrayList<>();List<DeleteObject> deleteObjects = new ArrayList<>(objectNames.size());for (String objectName : objectNames) {deleteObjects.add(new DeleteObject(objectName));}Iterable<Result<DeleteError>> results = getMinioClient().removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(deleteObjects).build());for (Result<DeleteError> result : results) {DeleteError error = result.get();deleteErrorNames.add(error.objectName());}return deleteErrorNames;}/*** @description 判斷bucket是否存在* @param bucketName     桶名稱*/@SneakyThrowspublic boolean bucketExists(String bucketName) {boolean exists = false;BucketExistsArgs.Builder builder = BucketExistsArgs.builder();BucketExistsArgs build = builder.bucket(bucketName).build();exists = getMinioClient().bucketExists(build);return exists;}/*** @description 創建存儲桶* minio 桶設置公共或私有,alioss統一設置成私有,可配置文件公共讀或私有讀* @param bucketName     桶名稱*/@SneakyThrowspublic void makeBucket(String bucketName) {if (bucketExists(bucketName)) {return;}getMinioClient().makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());}/*** @description 獲取文件* @param bucketName     桶名稱* @param objectName     文件路徑* @param offset         截取流的開始位置* @param length         截取長度*/@SneakyThrowspublic InputStream getObject(String bucketName, String objectName, Long offset, Long length) {return getMinioClient().getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).offset(offset).length(length).build());}/*** @description 獲取文件* @param bucketName     桶名稱* @param objectName     文件路徑*/@SneakyThrowspublic InputStream getObject(String bucketName, String objectName) {return getMinioClient().getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());}/*** @description 上傳分片文件* @param inputStream    輸入流* @param objectName     文件路徑* @param bucketName     桶名稱*/@SneakyThrowspublic void putChunkObject(InputStream inputStream, String bucketName, String objectName) {try {getMinioClient().putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(inputStream, inputStream.available(), -1).build());} finally {if (inputStream != null) {inputStream.close();}}}/*** @description 刪除空桶* @param bucketName     桶名稱*/@SneakyThrowspublic void removeBucket(String bucketName) {removeObjects(bucketName, listObjectNames(bucketName));getMinioClient().removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());}/*** @description 查詢桶中所有的文件* @param bucketName     桶名稱*/@SneakyThrowspublic List<String> listObjectNames(String bucketName) {List<String> objectNameList = new ArrayList<>();if (bucketExists(bucketName)) {Iterable<Result<Item>> objects = getMinioClient().listObjects(ListObjectsArgs.builder().bucket(bucketName).recursive(true).build());for (Result<Item> result : objects) {Item item = result.get();objectNameList.add(item.objectName());}}return objectNameList;}/*** @description 文件合并* @param originBucketName      分塊文件所在的桶* @param targetBucketName      合并文件生成文件所在的桶* @param objectName            存儲于桶中的對象名*/@SneakyThrowspublic String composeObject(String originBucketName, String targetBucketName, String objectName) {Iterable<Result<Item>> results = getMinioClient().listObjects(ListObjectsArgs.builder().bucket(originBucketName).recursive(true).build());List<String> objectNameList = new ArrayList<>();for (Result<Item> result : results) {Item item = result.get();objectNameList.add(item.objectName());}if (ObjectUtils.isEmpty(objectNameList)) {throw new IllegalArgumentException(originBucketName + "桶中沒有文件,請檢查");}List<ComposeSource> composeSourceList = new ArrayList<>(objectNameList.size());// 對文件名集合進行升序排序objectNameList.sort((o1, o2) -> Integer.parseInt(o2) > Integer.parseInt(o1) ? -1 : 1);for (String object : objectNameList) {composeSourceList.add(ComposeSource.builder().bucket(originBucketName).object(object).build());}return composeObject(composeSourceList, targetBucketName, objectName);}/*** @description 文件合并* @param bucketName          合并文件生成文件所在的桶* @param objectName          原始文件名* @param sourceObjectList    分塊文件集合*/@SneakyThrowspublic String composeObject(List<ComposeSource> sourceObjectList, String bucketName, String objectName) {getMinioClient().composeObject(ComposeObjectArgs.builder().bucket(bucketName).object(objectName).sources(sourceObjectList).build());return getObjectUrl(bucketName, objectName, 100);}
}
  • com.example.blh.file.FileBlh
package com.example.blh.file;import com.alibaba.fastjson2.JSONObject;
import com.example.entity.CommBo;
import com.example.entity.file.FileBo;
import com.example.utils.FileMd5Util;
import com.example.utils.MinioFileUtil;
import com.example.web.dto.file.MinioObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.minio.StatObjectResponse;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;/*** @description 文件上傳下載處理邏輯*/
@Component
public class FileBlh {private static Log logger = LogFactory.getLog(FileBlh.class);private static final String OBJECT_INFO_LIST = "minio.file.objects";private static final String MD5_KEY = "minio.file.md5s";private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMM");@Value("${minio-bucket-name:1}")private String bucketName;@Resourceprivate MinioFileUtil minioFileUtil;@Resourceprivate RedisTemplate redisTemplate;@Autowiredprivate ObjectMapper objectMapper;/*** @description 上傳單個文件*/public void uploadFile(FileBo bo) {InputStream is = null;try {MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) bo.getReq();MultipartFile file = multipartRequest.getFile("file");is = file.getInputStream();String uuid = UUID.randomUUID().toString().replace("-","");String dir = dateFormat.format(new Date());minioFileUtil.uploadObject(is,bucketName,dir,uuid+"-"+file.getOriginalFilename());bo.setFileName(dir+"/"+uuid+"-"+file.getOriginalFilename());CommBo.setSuccessBo(bo);} catch (Exception e) {CommBo.setFailBo(bo, e);} finally {try {if (is!=null) is.close();} catch (IOException eis) {}}}/*** @description 獲取文件路徑*/public void getFileUrl(FileBo bo) {try {bo.setFileUrl(minioFileUtil.getObjectUrl(bucketName,bo.getFileName(),100));CommBo.setSuccessBo(bo);} catch (Exception e) {CommBo.setFailBo(bo, e);}}/*** @description 刪除文件*/public void deleteFile(FileBo bo) {try {minioFileUtil.removeObject(bucketName,bo.getFileName());CommBo.setSuccessBo(bo);} catch (Exception e) {CommBo.setFailBo(bo, e);}}/*** @description 獲取文件分片下載信息*/public void getSplitFileInfo(FileBo bo) {try {StatObjectResponse objectInfo = minioFileUtil.getObjectInfo(bucketName,bo.getFileName());bo.setShardCount((int)Math.ceil((double)objectInfo.size()/(1024*1024*5)));CommBo.setSuccessBo(bo);} catch (Exception e) {CommBo.setFailBo(bo, e);}}/*** @description 文件分片下載*/public void downSplitFile(FileBo bo) {try {StatObjectResponse objectInfo = minioFileUtil.getObjectInfo(bucketName,bo.getFileName());long fileSize = objectInfo.size();long startPos = (bo.getShardCount()-1) * (1024*1024*5);long endPos = bo.getShardCount() * (1024*1024*5);if (endPos>fileSize) {endPos = fileSize;}long rangLength = endPos - startPos;bo.getRes().addHeader("Content-Type", "*/*");BufferedOutputStream bos = new BufferedOutputStream(bo.getRes().getOutputStream());BufferedInputStream bis = new BufferedInputStream(minioFileUtil.getObject(bucketName, bo.getFileName(), startPos, rangLength));IOUtils.copy(bis, bos);} catch (Exception e) {CommBo.setFailBo(bo, e);}}/*** @description 根據文件大小和文件的md5校驗文件是否存在*/public void checkSplitFile(FileBo bo) {try {if (ObjectUtils.isEmpty(bo.getMd5())) {bo.setCode(204);return;}String url = (String) redisTemplate.boundHashOps(MD5_KEY).get(bo.getMd5());if (ObjectUtils.isEmpty(url)) {bo.setCode(204); // 文件不存在return;}bo.setCode(200);bo.setFileUrl(url);} catch (Exception e) {CommBo.setFailBo(bo, e);}}/*** @description 文件上傳,適合大文件,集成了分片上傳*/public void uploadSplitFile(FileBo bo) {try {MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) bo.getReq();MultipartFile file = multipartRequest.getFile("data");if (file == null) {bo.setCode(203);return;}int index = Integer.parseInt(multipartRequest.getParameter("index")); // 第幾片int total = Integer.parseInt(multipartRequest.getParameter("total")); // 總片數String fileName = multipartRequest.getParameter("name");String md5 = multipartRequest.getParameter("md5");minioFileUtil.makeBucket(md5);String objectName = String.valueOf(index);if (index < total) {try {logger.info("上傳文件: " + md5 + " " + objectName);minioFileUtil.putChunkObject(file.getInputStream(), md5, objectName); // 上傳文件bo.setCode(201); // 不是最后一片, 狀態碼為201} catch (Exception e) {logger.error(e.getMessage());bo.setCode(203);}} else {try {minioFileUtil.putChunkObject(file.getInputStream(), md5, objectName);bo.setCode(202); // 最后一片, 狀態碼為202bo.setFileName(objectName);} catch (Exception e) {logger.error(e.getMessage());bo.setCode(203);}}} catch (Exception e) {CommBo.setFailBo(bo, e);}}/*** @description 文件合并*/public void mergeSplitFile(FileBo bo) {logger.info("分片總數: " + bo.getShardCount());Map<String, Object> retMap = new HashMap<>();try {List<String> objectNameList = minioFileUtil.listObjectNames(bo.getMd5());if (bo.getShardCount() != objectNameList.size()) {bo.setCode(203);} else {// 開始合并請求String filenameExtension = StringUtils.getFilenameExtension(bo.getFileName());String uuid = UUID.randomUUID().toString();String dir = dateFormat.format(new Date());String objectName = dir+"/"+uuid+"-"+bo.getFileName();minioFileUtil.composeObject(bo.getMd5(), bucketName, objectName);// 合并成功之后刪除對應的臨時桶minioFileUtil.removeBucket(bo.getMd5());logger.info("創建文件 " + objectName + " ,刪除桶 "+bo.getMd5()+" 成功");// 計算文件的md5String fileMd5 = null;try (InputStream inputStream = minioFileUtil.getObject(bucketName, objectName)) {fileMd5 = FileMd5Util.calculateMd5(inputStream);} catch (IOException e) {logger.error(e.getMessage());}// 計算文件真實的類型String type = null;if (!ObjectUtils.isEmpty(fileMd5) && fileMd5.equalsIgnoreCase(bo.getMd5())) {String url = minioFileUtil.getObjectUrl(bucketName, objectName, 100);redisTemplate.boundHashOps(MD5_KEY).put(fileMd5, objectName);bo.setCode(200);} else {minioFileUtil.removeObject(bucketName, objectName);redisTemplate.boundHashOps(MD5_KEY).delete(fileMd5);bo.setCode(203);}bo.setFileName(objectName);}} catch (Exception e) {logger.error(e.getMessage(), e);bo.setCode(203);}}/*** @description 文件播放*/public void videoPlay(FileBo bo) {logger.info("播放視頻: " + bo.getFileName());// 設置響應報頭String key = bucketName + "." + bo.getFileName();Object obj = redisTemplate.boundHashOps(OBJECT_INFO_LIST).get(key);// 記錄視頻文件的元數據MinioObject minioObject;if (obj == null) {StatObjectResponse objectInfo = null;try {objectInfo = minioFileUtil.getObjectInfo(bucketName,bo.getFileName());} catch (Exception e) {bo.getRes().setCharacterEncoding(StandardCharsets.UTF_8.toString());bo.getRes().setContentType("application/json;charset=utf-8");bo.getRes().setStatus(HttpServletResponse.SC_NOT_FOUND);try {JSONObject json = new JSONObject();json.put("operateSuccess",false);bo.getRes().getWriter().write(objectMapper.writeValueAsString(json));} catch (IOException ex) {throw new RuntimeException(ex);}return;}minioObject = new MinioObject();BeanUtils.copyProperties(objectInfo, minioObject);redisTemplate.boundHashOps(OBJECT_INFO_LIST).put(key, minioObject);} else {minioObject = (MinioObject) obj;}// 獲取文件的長度long fileSize = minioObject.getSize();// Accept-Ranges: bytesbo.getRes().setHeader("Accept-Ranges", "bytes");// pos開始讀取位置;  last最后讀取位置long startPos = 0;long endPos = fileSize - 1;String rangeHeader = bo.getReq().getHeader("Range");if (!ObjectUtils.isEmpty(rangeHeader) && rangeHeader.startsWith("bytes=")) {try {String numRang = bo.getReq().getHeader("Range").replaceAll("bytes=", "");if (numRang.startsWith("-")) {endPos = fileSize - 1;startPos = endPos - Long.parseLong(new String(numRang.getBytes(StandardCharsets.UTF_8), 1,numRang.length() - 1)) + 1;} else if (numRang.endsWith("-")) {endPos = fileSize - 1;startPos = Long.parseLong(new String(numRang.getBytes(StandardCharsets.UTF_8), 0,numRang.length() - 1));} else {String[] strRange = numRang.split("-");if (strRange.length == 2) {startPos = Long.parseLong(strRange[0].trim());endPos = Long.parseLong(strRange[1].trim());} else  {startPos = Long.parseLong(numRang.replaceAll("-", "").trim());}}if (startPos < 0 || endPos < 0 || endPos >= fileSize || startPos > endPos) {// 要求的范圍不滿足bo.getRes().setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);return;}// 斷點續傳 狀態碼206bo.getRes().setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);} catch (NumberFormatException e) {logger.error(e.getMessage());startPos = 0;}}// 總共需要讀取的字節long rangLength = endPos - startPos + 1;bo.getRes().setHeader("Content-Range", String.format("bytes %d-%d/%d", startPos, endPos, fileSize));bo.getRes().addHeader("Content-Length", String.valueOf(rangLength));bo.getRes().addHeader("Content-Type", "video/mp4");try (BufferedOutputStream bos = new BufferedOutputStream(bo.getRes().getOutputStream());BufferedInputStream bis = new BufferedInputStream(minioFileUtil.getObject(bucketName, bo.getFileName(), startPos, rangLength))) {IOUtils.copy(bis, bos);} catch (IOException e) {if (e instanceof ClientAbortException) {// ignore 這里不打印日志,這里的異常原因是用戶在拖拽視頻進度造成的} else {logger.error(e.getMessage());}}}
}
  • com.example.web.rest.file.FileRest
package com.example.web.rest.file;import com.alibaba.fastjson2.JSONObject;
import com.example.blh.file.FileBlh;
import com.example.entity.file.FileBo;
import com.example.web.dto.CommResp;
import com.example.web.dto.file.FileResp;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** @description 文件管理*/
@RestController
@RequestMapping("/api/file")
public class FileRest {private static Log logger = LogFactory.getLog(FileRest.class);@Resourceprivate FileBlh fileBlh;/*** @description 直接上傳文件, 入參:file, 出參:fileName*/@PostMapping(value="uploadFile")public FileResp uploadFileRest(HttpServletRequest req) {FileResp resp = new FileResp();FileBo bo = new FileBo();bo.setReq(req);fileBlh.uploadFile(bo);resp.setFileName(bo.getFileName());CommResp.setResp(resp,bo);resp.setResp();return resp;}/*** @description 獲取文件下載Url, 入參:fileName, 出參:fileUrl*/@PostMapping(value="getFileUrl",consumes="application/json")public FileResp getFileUrlRest(@RequestBody JSONObject req) {FileResp resp = new FileResp();FileBo bo = new FileBo();bo.setFileName(req.getString("fileName"));fileBlh.getFileUrl(bo);resp.setFileUrl(bo.getFileUrl());CommResp.setResp(resp,bo);resp.setResp();return resp;}/*** @description 刪除文件, 入參:fileName*/@PostMapping(value="deleteFile",consumes="application/json")public FileResp deleteFileRest(@RequestBody JSONObject req) {FileResp resp = new FileResp();FileBo bo = new FileBo();bo.setFileName(req.getString("fileName"));fileBlh.deleteFile(bo);CommResp.setResp(resp,bo);resp.setResp();return resp;}/*** @description 校驗文件是否存在, 入參:md5*/@GetMapping(value = "checkSplitFile")public FileResp checkSplitFileRest(String md5) {FileResp resp = new FileResp();FileBo bo = new FileBo();bo.setMd5(md5);fileBlh.checkSplitFile(bo);resp.setFileUrl(bo.getFileUrl());resp.setCode(bo.getCode());CommResp.setResp(resp,bo);resp.setResp();return resp;}/*** @description 分片上傳文件, 入參:data*/@PostMapping(value = "uploadSplitFile")public FileResp uploadSplitFileRest(HttpServletRequest req) {FileResp resp = new FileResp();FileBo bo = new FileBo();bo.setReq(req);fileBlh.uploadSplitFile(bo);resp.setCode(bo.getCode());CommResp.setResp(resp,bo);resp.setResp();return resp;}/*** @description 文件合并, 入參:shardCount/fileName/md5/fileType/fileSize*/@GetMapping(value = "mergeSplitFile")public FileResp mergeSplitFileRest(HttpServletRequest req) {FileResp resp = new FileResp();FileBo bo = new FileBo();bo.setShardCount(Integer.valueOf(req.getParameter("shardCount")));bo.setFileName(req.getParameter("fileName"));bo.setMd5(req.getParameter("md5"));bo.setFileType(req.getParameter("fileType"));bo.setFileSize(Long.valueOf(req.getParameter("fileSize")));fileBlh.mergeSplitFile(bo);resp.setCode(bo.getCode());resp.setFileName(bo.getFileName());CommResp.setResp(resp,bo);resp.setResp();return resp;}/*** @description 獲取文件分片下載信息, 入參:fileName, 出參:shardCount*/@PostMapping(value="getSplitFileInfo",consumes="application/json")public FileResp getSplitFileInfoRest(@RequestBody JSONObject req) {FileResp resp = new FileResp();FileBo bo = new FileBo();bo.setFileName(req.getString("fileName"));fileBlh.getSplitFileInfo(bo);resp.setShardCount(bo.getShardCount());CommResp.setResp(resp,bo);resp.setResp();return resp;}/*** @description 文件分片下載, 入參:fileName/fileNo, 出參:文件流*/@GetMapping(value="downSplitFile")public void downSplitFileRest(HttpServletRequest req, HttpServletResponse res) {FileBo bo = new FileBo();bo.setFileName(req.getParameter("fileName"));bo.setShardCount(Integer.valueOf(req.getParameter("fileNo")));bo.setRes(res);fileBlh.downSplitFile(bo);}/*** @description 視頻播放*/@GetMapping(value = "videoPlay")public void videoPlayRest(HttpServletRequest req, HttpServletResponse res) {FileBo bo = new FileBo();bo.setReq(req);bo.setRes(res);bo.setFileName(req.getParameter("video"));fileBlh.videoPlay(bo);}
}

2. 文件上傳前端代碼

  • HTML效果
    http://127.0.0.1:8081/test/upload.html
    在這里插入圖片描述
  • resources/static/upload.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>upload</title><link rel="icon" href="data:;base64,="><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script><script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body><input type="file" name="file" id="file"><text id="msgtext"></text>
</body>
<script>const baseUrl = "http://127.0.0.1:8081/test/api/file/"/*** 計算文件的md5值* @param file 文件*/function calculateFileMd5(file) {return calculateFileMd5Chunk(file, 2097152);}/*** 分片計算文件的md5值* @param file 文件* @param chunkSize 分片大小*/function calculateFileMd5Chunk(file, chunkSize) {return new Promise((resolve, reject) => {let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;let chunks = Math.ceil(file.size / chunkSize);let currentChunk = 0;let spark = new SparkMD5.ArrayBuffer();let fileReader = new FileReader();fileReader.onload = function (e) {spark.append(e.target.result);currentChunk++;if (currentChunk < chunks) {loadNext();} else {let md5 = spark.end();resolve(md5);}};fileReader.onerror = function (e) {reject(e);};function loadNext() {let start = currentChunk * chunkSize;let end = start + chunkSize;if (end > file.size) {end = file.size;}fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));}loadNext();});}/*** 獲取文件的后綴名* @param fileName 文件名*/function getFileType(fileName) {return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();}/*** 根據文件的md5值判斷文件是否已經上傳* @param md5 文件的md5* @param file 準備上傳的文件*/function checkMd5(md5, file) {$.ajax({url: baseUrl + "checkSplitFile",type: "get",data: {md5: md5},async: true,dataType: "json",success: function (msg) {if (msg.code === 200) {console.log("文件已經存在")$('#msgtext').html('文件已存在: '+ msg.fileUrl);} else if (msg.code === 204) {console.log("文件不存在需要上傳")postFile(file, 0, md5);} else {console.log('未知錯誤');}}})}/*** 分片上傳* @param file 上傳的文件* @param i 第幾分片,從0開始* @param md5 文件的md5值*/function postFile(file, i, md5) {let name = file.name,                           // 文件名size = file.size,                           // 總大小shardSize = 2 * 1024 * 1024,shardSize = 5 * 1024 * 1024,                // 以5MB為一個分片,每個分片的大小shardCount = Math.ceil(size / shardSize);   // 總片數if (i >= shardCount) {return;}let start = i * shardSize;let end = start + shardSize;let packet = file.slice(start, end);            // 將文件進行切片let form = new FormData();form.append("md5", md5);                        // 前端生成uuid作為標識符form.append("data", packet);                    // slice方法用于切出文件的一部分form.append("name", name);form.append("totalSize", size);form.append("total", shardCount);               // 總片數form.append("index", i + 1);                    // 當前是第幾片$.ajax({url: baseUrl + "uploadSplitFile",type: "post",data: form,async: true,dataType: "json",processData: false,contentType: false,success: function (msg) {if (msg.code === 201) {form = '';i++;postFile(file, i, md5);} else if (msg.code === 203) {form = '';setInterval(function () {postFile(file, i, md5)}, 2000);} else if (msg.code === 202) {merge(shardCount, name, md5, getFileType(file.name), file.size)console.log("上傳成功");} else {console.log('未知錯誤');}}})}/*** 合并文件* @param shardCount 分片數* @param fileName 文件名* @param md5 文件md值* @param fileType 文件類型* @param fileSize 文件大小*/function merge(shardCount, fileName, md5, fileType, fileSize) {$.ajax({url: baseUrl + "mergeSplitFile",type: "get",data: {shardCount: shardCount,fileName: fileName,md5: md5,fileType: fileType,fileSize: fileSize},async: true,dataType: "json",success: function (msg) {$('#msgtext').html('文件上傳成功: '+ msg.fileName);}})}// 瀏覽器加載文件后, 計算文件的md5值document.getElementById("file").addEventListener("change", function () {$('#msgtext').html('待上傳');let file = this.files[0];calculateFileMd5(file).then(e => {let md5 = e;checkMd5(md5, file)}).catch(e => {console.error(e);});});$('#msgtext').html('待上傳');
</script>
</html>

3. 視頻播放前端代碼

  • ckplayer
    ckplayer是一款在網頁上播放視頻的軟件,基于javascript和css,其特點是開源,不依賴其它插件。
    視頻播放插件下載、視頻播放插件手冊

  • HTML效果
    http://127.0.0.1:8081/test/video.html?video=202312/f210299b-3988-4ad3-b9a3-fd6677936bda-test.mp4
    在這里插入圖片描述

  • resources/static/video.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>play</title><link rel="icon" href="data:;base64,="><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script><script src="https://www.ckplayer.com/public/static/ckplayer-x3/js/ckplayer.js"></script><link rel="stylesheet" type="text/css" href="https://www.ckplayer.com/public/static/ckplayer-x3/css/ckplayer.css" />
</head>
<body><div class="video" style="width: 100%; height: 500px;max-width: 800px;"></div><p><button type="button" onclick="player.play()">播放</button><button type="button" onclick="player.pause()">暫停</button><button type="button" onclick="player.seek(20)">跳轉</button><button type="button" onclick="player.volume(0.6)">修改音量</button><button type="button" onclick="player.muted()">靜音</button><button type="button" onclick="player.exitMuted()">恢復音量</button><button type="button" onclick="player.full()">全屏</button></p><p id="state1"></p><p id="state2"></p>
</body>
<script>const queryString = window.location.search;const urlParams = new URLSearchParams(queryString);const videoObj = urlParams.get('video');const baseUrl = "http://127.0.0.1:8081/test/api/file/videoPlay"let videoObject = {container: '.video', // 視頻容器的IDvolume: 0.8, // 默認音量,范圍0-1video: baseUrl + '?video=' + videoObj, // 視頻地址};let player = new ckplayer(videoObject) // 調用播放器并賦值給變量player
</script>
</html>

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/209916.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/209916.shtml
英文地址,請注明出處:http://en.pswp.cn/news/209916.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

多線程案例-單例模式

單例模式 設計模式的概念 設計模式好比象棋中的"棋譜".紅方當頭炮,黑方馬來跳.針對紅方的一些走法,黑方應招的時候有一些固定的套路.按照套路來走局勢就不會吃虧. 軟件開發中也有很多常見的"問題場景".針對這些問題的場景,大佬們總結出了一些固定的套路.按…

vue實現可拖拽列表

直接上代碼 <!-- vue實現可拖拽列表 --> <template><div><button click"logcolig">打印數據</button><TransitionGroup name"list" tag"div" class"container"><divclass"item"v-f…

常見請求頭與響應頭你了解哪些?

常見的 HTTP 請求頭和響應頭包括&#xff1a; 常見的請求頭&#xff1a; User-Agent&#xff1a;標識客戶端代理信息&#xff0c;通常用于識別用戶使用的瀏覽器或設備類型。 Accept&#xff1a;指示客戶端可以接受的內容類型&#xff0c;例如 text/html, application/json 等…

深度學習記錄--激活函數

激活函數的種類 對于激活函數的選擇&#xff0c;通常有以下幾種 sigmoid&#xff0c;tanh&#xff0c;ReLU&#xff0c;leaky ReLU 激活函數的選擇 之前logistic回歸一直使用的激活函數都是sigmoid函數&#xff0c;但一般來說&#xff0c;tanh函數是比sigmoid函數更加好的選…

【Python】 生成二維碼

創建了一個使用 python 創建二維碼的程序。 下面是生成的程序的圖像。 功能描述 輸入網址&#xff08;URL&#xff09;。 輸入二維碼的名稱。 當單擊 QR 碼生成按鈕時&#xff0c;將使用 QRname 中輸入的字符將 QR 碼生成為圖像。 程序代碼 import qrcode import tkinterd…

java泛型:泛型類,泛型方法

今日記錄我的泛型使用&#xff0c;供后期查閱。 主要包含泛型類&#xff0c;泛型屬性&#xff0c;泛型方法&#xff0c;靜態方法中使用泛型。 public class GenericOperationResultRep<T> {private boolean success; // 是否操作成功。true&#xff0c;成功&#xff1b;f…

Oracle的錯誤信息幫助:Error Help

今天看手冊時&#xff0c;發現上面有個提示&#xff1a; Error messages are now available in Error Help. 點擊 View Error Help&#xff0c;顯示如下&#xff0c;其實就是oerr命令的圖形化版本&#xff1a; 點擊Database Error Message Index&#xff0c;以下界面等同于命令…

[Kadane算法,前綴和思想]元素和最大的子矩陣

元素和最大的子矩陣 題目描述 輸入一個n級方陣&#xff0c;請找到此矩陣的一個子矩陣&#xff0c;此子矩陣的各個元素的和是所有子矩陣中最大的&#xff0c;輸出這個子矩陣及這個最大的和。 關于輸入 首先輸入方陣的級數n&#xff0c; 然后輸入方陣中各個元素。 關于輸出 …

車載藍牙音樂流程簡單分析

關鍵類&#xff1a; /packages/apps/Bluetooth/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java /packages/apps/Bluetooth/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java 一、音樂播放狀態 CPP中通過JNI接口將接從…

Python中利用遺傳算法探索迷宮出路

更多資料獲取 &#x1f4da; 個人網站&#xff1a;ipengtao.com 當處理迷宮問題時&#xff0c;遺傳算法提供了一種創新的解決方案。本文將深入探討如何運用Python和遺傳算法來解決迷宮問題。迷宮問題是一個經典的尋路問題&#xff0c;尋找從起點到終點的最佳路徑。遺傳算法是一…

ActiveMQ斷線重連技巧,即通信高可用的配置

最近在做一個內部應用的時候&#xff0c;應用到了ActiveMQ作為服務之間消息傳遞&#xff0c;解耦服務之間的關聯&#xff0c;但是在應用的過程中遇到了連接斷線無法重連的問題&#xff0c;下面基于這個問題&#xff0c;深入了解一下ActiveMQ的一些相關原理和知識。 一、前置知…

springboot2 在Java項目中你們是如何配置時間格式響應給前端呢

在 Spring Boot 2 項目中配置時間格式&#xff0c;通常可以通過配置文件&#xff08;application.properties 或 application.yml&#xff09;或者通過 Java 代碼進行配置。以下是兩種常見的配置方式&#xff1a; 1. 通過配置文件配置時間格式&#xff1a; 在 application.pr…

mybaties plus插入數據,自動回顯 機制

結論&#xff1a;mybaties plus會將庫里數據自動回顯到 要插入的數據上 測試表格 SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS 0;-- 表結構 DROP TABLE IF EXISTS t_stu; CREATE TABLE t_stu (id int NOT NULL COMMENT id,name varchar(255) CHARACTER SET utf8mb4 COLLATE…

【PyTorch】計算設備

文章目錄 1. 介紹2. 查詢和使用 1. 介紹 CPU設備意味著所有物理CPU和內存&#xff0c; 這意味著PyTorch的計算將嘗試使用所有CPU核心。可以用以下方式表示&#xff1a; torch.device(cpu) GPU設備只代表一個GPU和相應的顯存。 torch.device(cuda)如果有多個GPU&#xff0c;我們…

Java解決矩陣對角線元素的和問題

Java解決矩陣對角線元素的和問題 01 題目 給你一個正方形矩陣 mat&#xff0c;請你返回矩陣對角線元素的和。 請你返回在矩陣主對角線上的元素和副對角線上且不在主對角線上元素的和。 示例 1&#xff1a; 輸入&#xff1a;mat [[1,2,3],[4,5,6],[7,8,9]] 輸出&#xff1a…

為什么流量對店鋪轉化率重要?亞馬遜、速賣通等跨境賣家通過自養號測評提升店鋪轉化率

亞馬遜、速賣通等電商平臺賣家非常清楚流量對店鋪轉化率的重要性&#xff0c;測評補單在跨境電商賣家中扮演著重要的角色&#xff0c;是一種必要的運營手段之一。在追求更好的產品曝光和更高的轉化率時&#xff0c;Listing的排名是關鍵因素之一。而在各個平臺的Listing中&#…

正確使用AFX_MANAGE_STATE宏管理MFC模塊狀態, AFX_MANAGE_STATE宏作用,真的很重要!!!

簡介&#xff1a; 在使用 MFC&#xff08;Microsoft Foundation Classes&#xff09;開發 DLL&#xff08;動態鏈接庫&#xff09;時&#xff0c;正確管理 MFC 模塊狀態是確保功能正常運行的關鍵。本文將深入探討使用 AFX_MANAGE_STATE 宏的重要性&#xff0c;以及在 DLL 中正確…

連接Redis報錯解決方案

連接Redis報錯&解決方案 問題描述&#xff1a;Could not connect to Redis at 127.0.0.1:6379: 由于目標計算機積極拒絕&#xff0c;無法連接。 問題原因&#xff1a;redis啟動方式不正確 解決方案&#xff1a; 在redis根目錄下打開命令行窗口&#xff0c;輸入命令redi…

聽GPT 講Rust源代碼--src/tools(12)

File: rust/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs 在Rust源代碼中&#xff0c;rust/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs文件的作用是定義和解析rust-analyzer的配置文件。該文件包含了各種配置項的數據結構和枚舉類型&#xf…

MQTT主題、通配符和最佳實踐

MQTT主題在MQTT生態系統非常重要&#xff0c;因為代理&#xff08;broker&#xff09;依賴主題確定哪個客戶端接收指定的主題。本文我們將聚集MQTT主題、MQTT通配符&#xff0c;詳細討論使用它們的最佳實踐&#xff0c;也會探究SYS主題&#xff0c;提供給代理&#xff08;broke…