Video的断点续传

断点续传含义:

断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分(分块),每一个部分采用一个线程进行上传或下载。如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性。

分块:

    @Test
    public void testChunk() throws IOException {
        //1.获取源文件
        File sourceFile  = new File("D:\\MyVido\\EV\\绝地求生两个倒霉蛋.mp4");
        //2.创建分块目录
        String chunkPath = "E:\\csc\\XueCheng\\minIo\\chunkPath\\";
        File chunkFolder = new File(chunkPath);
        if (!chunkFolder.exists()) {
            chunkFolder.mkdirs();
        }
        //3.设置分块大小--->5M
        int chunkSize = 1024 * 1024 * 5;
        //4.计算分块数量:ceil向上取整
        int chunkNum = (int) Math.ceil(sourceFile.length() * 1.0/chunkSize);
        //5.使用RandomAccessFile访问文件: 读源文件的流
        RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r");
        //6.3创建缓存区
        byte[] bytes = new byte[1024];
        //6.开始分块
        for (int i = 0; i < chunkNum; i++) {
            //6.1创建分款文件
            File chunkFile = new File(chunkPath + i);
            //6.2分块文件的写入流
            RandomAccessFile raf_rw = new RandomAccessFile(chunkFile, "rw");
            int len = -1;
            while ((len = raf_read.read(bytes)) != -1){
                //从缓冲区写数据
                raf_rw.write(bytes,0,len);
                if(chunkFile.length() >= chunkSize){
                    break;
                }
            }
            raf_rw.close();
        }
        raf_read.close();
    }

合并分块:

    @Test
    public void testMerge() throws IOException {
        //分块后的目录
        File chunkFolder  = new File("E:\\csc\\XueCheng\\minIo\\chunkPath\\");
        //原始文件
        File originalFile  = new File("D:\\MyVido\\EV\\绝地求生两个倒霉蛋.mp4");
        //合并后的文件
        File mergeFile  = new File("E:\\csc\\XueCheng\\minIo\\绝地求生两个倒霉蛋.mp4");
        if (mergeFile.exists()) {
            mergeFile.delete();
        }
        //取出所有分块文件
        File[] files = chunkFolder.listFiles();
        //将数组转换成list
        List<File> filesList = Arrays.asList(files);
        //排序
        Collections.sort(filesList, new Comparator<File>() {
            @Override
            public int compare(File o1, File o2) {
                return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName());
            }
        });
        //向合并文件写的流
        RandomAccessFile raf_rw = new RandomAccessFile(mergeFile, "rw");
        byte[] bytes = new byte[1024];
        //遍历分块文件,向合并文件写
        for (File file: filesList) {
            //读分块的流
            RandomAccessFile raf_r = new RandomAccessFile(file, "r");
            int len = -1;
            while ((len = raf_r.read(bytes)) != -1){
                raf_rw.write(bytes,0,len);
            }
            raf_r.close();
        }
        raf_rw.close();


        //校验
        FileInputStream fileInputStream_original = new FileInputStream(originalFile);
        FileInputStream fileInputStream_merge = new FileInputStream(mergeFile);
        String originalMd5 = DigestUtils.md5Hex(fileInputStream_original);
        String mergeMd5 = DigestUtils.md5Hex(fileInputStream_merge);
        if(originalMd5.equals(mergeMd5)){
            System.out.println("合并文件成功");
        }
    }

实例代码:

业务流程

前端将视频分块--->上传分块前先请求服务检查文件是否存在,如果已经存在则不再上传
--->如果不存在,则开始向minio上传分块--->上传完成请求服务开始合并分块--->合并完后清理分块

检查文件是否存在

    @Override
    public RestResponse<Boolean> checkFile(String fileMd5) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles != null) {
            String bucket = mediaFiles.getBucket();
            String filePath = mediaFiles.getFilePath();
            GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(filePath)
                    .build();
            try {
                FilterInputStream filterInputStream = minioClient.getObject(getObjectArgs);
                if (filterInputStream != null) {
                    //文件已经存在
                    return RestResponse.success(true,"文件已经存在");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return RestResponse.success(false);
    }

检查分块是否存在

    @Override
    public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
        //分块目录: md5值的前两位作为两个子目录+md5+chunk
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                .bucket(videofiles)
                .object(chunkFileFolderPath + chunkIndex)
                .build();
        try {
            FilterInputStream filterInputStream = minioClient.getObject(getObjectArgs);
            if (filterInputStream != null) {
                return RestResponse.success(true);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return RestResponse.success(false);
    }

上传分块

    @Override
    public RestResponse uploadChunk(String fileMd5, int chunk, String localFilePath) {

        String mimeType = getMimeType(null);
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5) + chunk;
        boolean b = addMediaFilesToMinio(mimeType, localFilePath, videofiles, chunkFileFolderPath);
        if (!b) {
            return RestResponse.validfail(false, "上传分块文件失败");
        }
        log.info("fileMd5:{}", fileMd5);
        return RestResponse.success(true);
    }
	/**
     * @author changshichao
     * @description 将文件上传到minio
     * @date 2023-08-29 14:48
     */
    public boolean addMediaFilesToMinio(String mimeType, String localFilePath, String bucket, String objectName) {
        try {
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    .bucket(bucket)//桶
                    .filename(localFilePath)//指定本地文件路径
                    .object(objectName)//对象名.在子目录下存储该文件
                    .contentType(mimeType)//设置媒体文件类型
                    .build();
            minioClient.uploadObject(uploadObjectArgs);
            return true;
        } catch (Exception e) {
            log.error("上传文件出错,bucket:{},objectName:{},错误信息:{}", bucket, objectName, e.getMessage());
        }
        return false;
    }

合并分块

public RestResponse mergeChunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
        //分块文件的目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        String filename = uploadFileParamsDto.getFilename();
        String extension = filename.substring(filename.lastIndexOf("."));
        //合并后的文件信息
        String objectName = getFilePathByMd5(fileMd5, extension);
            //组成将分块文件路径组成 List<ComposeSource>
            List<ComposeSource> sources = Stream.iterate(0, i -> ++i)
                    .limit(chunkTotal)
                    .map(i -> ComposeSource.builder()
                            .bucket(videofiles)
                            .object(chunkFileFolderPath.concat(Integer.toString(i)))
                            .build())
                    .collect(Collectors.toList());

        try {
            //1.合并分块
            ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                    .bucket(videofiles)
                    .sources(sources)
                    .object(objectName)
                    .build();
            log.debug("合并文件成功:{}",objectName);
            minioClient.composeObject(composeObjectArgs);
        } catch (Exception e) {
            e.printStackTrace();
            log.debug("合并文件出错,bucket:{},objectName:{},错误信息:{}", videofiles, objectName, e.getMessage());
            return RestResponse.validfail(false, "合并文件异常");
        }
        //2.检验合并后的文件和源文件是否一致,视频上传成功
        //2.1下载合并后的文件
        File file = downloadFileFromMinIo(videofiles, objectName);
        //2.2校验
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            String mergeFile_Md5 = DigestUtils.md5Hex(fileInputStream);
            if (!fileMd5.equals(mergeFile_Md5)) {
                log.error("校验合并文件md5值不一致,原始文件:{},合并文件", fileMd5, mergeFile_Md5);
                return RestResponse.validfail(false, "文件校验失败");
            }
        } catch (Exception e) {
            return RestResponse.validfail(false, "文件校验失败");
        }
        //文件大小
        uploadFileParamsDto.setFileSize(file.length());
        //3.文件入库
        MediaFiles mediaFiles = addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, videofiles, objectName);
        if (mediaFiles == null) {
            return RestResponse.validfail(false, "文件入库失败");
        }
        //4.清理分块
        clearChunkFiles(chunkFileFolderPath, chunkTotal);
        return RestResponse.success(true);
    }

清理分块

    private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal) {
        try {
            List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i)
                    .limit(chunkTotal)
                    .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
                    .collect(Collectors.toList());

            RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
                    .bucket(videofiles)
                    .objects(deleteObjects)
                    .build();
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
            //想要真正删除,必须执行下列
            results.forEach(r -> {
                DeleteError deleteError = null;
                try {
                    deleteError = r.get();
                } catch (Exception e) {
                    e.printStackTrace();
                    log.error("清楚分块文件失败,objectname:{}", deleteError.objectName(), e);
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
            log.error("清楚分块文件失败,chunkFileFolderPath:{}", chunkFileFolderPath, e);
        }
    }

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值