09 视频分片上传Minio和播放


一、流程设计

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: 300MB
      max-request-size: 300MB

minio:
  endpoint: http://127.0.0.1:9000 #MinIO服务所在地址
  accessKey: admin #访问的key
  secretKey: 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;

    // 文件MD5
    private 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     文件名称
     */
    @SneakyThrows
    public 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天则默认最大值
     */
    @SneakyThrows
    public 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     文件路径
     */
    @SneakyThrows
    public 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     文件名称(包含存储目录)
     */
    @SneakyThrows
    public 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     桶名称
     */
    @SneakyThrows
    public 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     桶名称
     */
    @SneakyThrows
    public void makeBucket(String bucketName) {
    
    
        if (bucketExists(bucketName)) {
    
    
            return;
        }
        getMinioClient().makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
    }

    /**
     * @description 获取文件
     * @param bucketName     桶名称
     * @param objectName     文件路径
     * @param offset         截取流的开始位置
     * @param length         截取长度
     */
    @SneakyThrows
    public 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     文件路径
     */
    @SneakyThrows
    public InputStream getObject(String bucketName, String objectName) {
    
    
        return getMinioClient().getObject(
                GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }

    /**
     * @description 上传分片文件
     * @param inputStream    输入流
     * @param objectName     文件路径
     * @param bucketName     桶名称
     */
    @SneakyThrows
    public 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     桶名称
     */
    @SneakyThrows
    public void removeBucket(String bucketName) {
    
    
        removeObjects(bucketName, listObjectNames(bucketName));
        getMinioClient().removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
    }

    /**
     * @description 查询桶中所有的文件
     * @param bucketName     桶名称
     */
    @SneakyThrows
    public 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            存储于桶中的对象名
     */
    @SneakyThrows
    public 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    分块文件集合
     */
    @SneakyThrows
    public 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;
    @Resource
    private MinioFileUtil minioFileUtil;
    @Resource
    private RedisTemplate redisTemplate;
    @Autowired
    private 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); // 最后一片, 状态码为202
                    bo.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()+" 成功");
                // 计算文件的md5
                String 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: bytes
        bo.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;
                }
                // 断点续传 状态码206
                bo.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);

	@Resource
	private 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', // 视频容器的ID
        volume: 0.8, // 默认音量,范围0-1
        video: baseUrl + '?video=' + videoObj, // 视频地址
    };
    let player = new ckplayer(videoObject) // 调用播放器并赋值给变量player
</script>
</html>

猜你喜欢

转载自blog.csdn.net/qq_42308751/article/details/134823088
09
今日推荐