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.mzph.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…

深度学习记录--激活函数

激活函数的种类 对于激活函数的选择&#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…

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;以下界面等同于命令…

Python中利用遗传算法探索迷宫出路

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 当处理迷宫问题时&#xff0c;遗传算法提供了一种创新的解决方案。本文将深入探讨如何运用Python和遗传算法来解决迷宫问题。迷宫问题是一个经典的寻路问题&#xff0c;寻找从起点到终点的最佳路径。遗传算法是一…

Java解决矩阵对角线元素的和问题

Java解决矩阵对角线元素的和问题 01 题目 给你一个正方形矩阵 mat&#xff0c;请你返回矩阵对角线元素的和。 请你返回在矩阵主对角线上的元素和副对角线上且不在主对角线上元素的和。 示例 1&#xff1a; 输入&#xff1a;mat [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a…

连接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…

【npm | npm常用命令及镜像设置】

npm常用命令及镜像设置 概述常用命令对比本地安装全局安装--save &#xff08;或 -S&#xff09;--save-dev &#xff08;或 -D&#xff09; 镜像设置设置镜像方法切换回npm官方镜像选择镜像源 主页传送门&#xff1a;&#x1f4c0; 传送 概述 npm致力于让 JavaScript 开发变得…

iOS——UIPickerView选择器

UIPickerView UIPickerView是 iOS 开发中常用的用户界面组件之一&#xff0c;用于在垂直方向上显示一个滚动的列表&#xff0c;用户可以通过滚动选择其中的一项。 UIPickerView的协议方法 UIPickerView和UItableView差不多&#xff0c;UIPickerView也要设置代理和数据源。UI…

fl studio2024试用版本如何汉化中文?

fl studio2024全称Fruity Loops Studio2024&#xff0c;这款软件也被人们亲切的称之为水果&#xff0c;它是一款功能强大的音乐创作编辑软件&#xff0c;拥有全功能的录音室&#xff0c;大混音盘以及先进的音乐制作工具&#xff0c;用户通过使用该软件&#xff0c;就可以轻松制…

2020年第九届数学建模国际赛小美赛B题血氧饱和度的变异性解题全过程文档及程序

2020年第九届数学建模国际赛小美赛 B题 血氧饱和度的变异性 原题再现&#xff1a; 脉搏血氧饱和度是监测患者血氧饱和度的常规方法。在连续监测期间&#xff0c;我们希望能够使用模型描述血氧饱和度的模式。   我们有36名受试者的数据&#xff0c;每个受试者以1 Hz的频率连…

CSPNet: A New Backbone that can Enhance Learning Capability of CNN(2019)

文章目录 -Abstract1 Introduction2 Related workformer work 3 Method3.1 Cross Stage Partial Network3.2 Exact Fusion Model 4 Experiments5 Conclusion 原文链接 源代码 - 梯度信息重用&#xff08;有别于冗余的梯度信息&#xff09;可以减少计算量和内存占用提高效率&am…

C语言 文件操作

文章目录 前言文件概念文件名数据文件&程序文件文本文件&二进制文件文件缓冲区 文件操作FILE结构体文件指针文件打开&关闭文件输入/输出文件指针控制 前言 主要需要看的是概念部分、以及FILE结构体、文件指针部分。其余函数使用&#xff0c;知道其功能存在即可&am…

【SpringBoot】请求参数

1. BS 架构 BS架构&#xff1a;Browser/Server&#xff0c;浏览器/服务器架构模式。客户端只需要浏览器&#xff0c;应用程序的逻辑和数据都存储在服务端。 在SpringBoot进行web程序开发时&#xff0c;它内置了一个核心的Servlet程序 DispatcherServlet&#xff0c;称之为 核…

EasyExcel之文件导出最佳实践

文件导出 官方文档&#xff1a;写Excel | Easy Excel (alibaba.com) 引言 当使用 EasyExcel 进行 Excel 文件导出时&#xff0c;我最近在工作中遇到了一个需求。因此&#xff0c;我决定写这篇文章来分享我的经验和解决方案。如果你对这个话题感兴趣&#xff0c;那么我希望这篇…

c语言插入排序算法(详解)

插入排序是一种简单直观的排序算法&#xff0c;其主要思想是将一个待排序的元素插入到已经排好序的部分的合适位置。 插入排序的原理如下&#xff1a; 将序列分为两部分&#xff1a;已排序部分和未排序部分。初始时&#xff0c;已排序部分只包含第一个元素&#xff0c;未排序…

php 接入 百度编辑器

按照github上的操作下载百度编辑器的包后&#xff0c;根据文档上的步骤操作&#xff08;可能会遇到报错&#xff09;&#xff1a; 1、git clone 仓库 2、npm install 安装依赖&#xff08;如果没有安装 grunt , 请先在全局安装 grunt&#xff09; 我的是报了下面的错&#…