最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

minio分布式文件管理方式

 更新時(shí)間:2025年08月11日 08:58:11   作者:java_學(xué)習(xí)愛(ài)好者  
文章介紹了MinIO作為分布式存儲(chǔ)系統(tǒng),支持大文件存儲(chǔ)與數(shù)據(jù)恢復(fù),通過(guò)Docker部署并配置多副本存儲(chǔ),同時(shí)涵蓋圖片/視頻上傳、斷點(diǎn)續(xù)傳、格式轉(zhuǎn)換(如AVI轉(zhuǎn)MP4)及xxl-job分布式任務(wù)調(diào)度,強(qiáng)調(diào)樂(lè)觀鎖機(jī)制和補(bǔ)償機(jī)制確保任務(wù)可靠性與數(shù)據(jù)一致性

一、minio 是什么?

MinIO構(gòu)建分布式文件系統(tǒng),MinIO 是一個(gè)非常輕量的服務(wù),可以很簡(jiǎn)單的和其他應(yīng)用的結(jié)合使用,它兼容亞馬遜 S3 云存儲(chǔ)服務(wù)接口,非常適合于存儲(chǔ)大容量非結(jié)構(gòu)化的數(shù)據(jù),例如圖片、視頻、日志文件、備份數(shù)據(jù)和容器/虛擬機(jī)鏡像等。

官網(wǎng):https://www.minio.org.cn/

二、minio的部署

本項(xiàng)目采用docker搭建

首先需要?jiǎng)?chuàng)建,文件存儲(chǔ)的目錄。以后上傳的文件,在這4個(gè)目錄中都會(huì)進(jìn)行存儲(chǔ)(即:一個(gè)文件存儲(chǔ)4份),保證數(shù)據(jù)的安全性

 mkdir -p /root/minio_data/data1
 mkdir -p /root/minio_data/data2
 mkdir -p /root/minio_data/data3
 mkdir -p /root/minio_data/data4
docker run -p 9000:9000 -p 9001:9001 --name minio \
  -v /root/minio_data/data1:/data1 \
  -v /root/minio_data/data2:/data2 \
  -v /root/minio_data/data3:/data3 \
  -v /root/minio_data/data4:/data4 \
  -e "MINIO_ROOT_USER=minioadmin" \
  -e "MINIO_ROOT_PASSWORD=minioadmin" \
  minio/minio server /data{1...4} --console-address ":9001"
  • 9000端口是作為S3 API端口,用于API的調(diào)用,9001端口用于Web控制臺(tái)
  • minio/minio: 這是Docker鏡像的名稱(chēng)
  • server /data{1...4}: 這部分告訴MinIO以服務(wù)器模式啟動(dòng),并且使用/data1/data2/data3, 和 /data4這四個(gè)目錄作為存儲(chǔ)位置。
  • --console-address ":9001": 這個(gè)參數(shù)指定了MinIO Web控制臺(tái)的監(jiān)聽(tīng)地址和端口。這里設(shè)置為":9001",意味著Web控制臺(tái)將監(jiān)聽(tīng)容器內(nèi)的9001端口。
  • 訪問(wèn)地址:http://ip地址:9001   賬號(hào):minioadmin 密碼:minioadmin

windows安裝方法

  • 在D盤(pán)下創(chuàng)建minio_data目錄,在該目錄下創(chuàng)建分別創(chuàng)建data1,data2,data3,data4目錄
  • 進(jìn)入官網(wǎng),下載minio.exe文件,進(jìn)入cmd,執(zhí)行下列命令,啟動(dòng)minio服務(wù)
minio.exe server D:\minio_data\data1  D:\minio_data\data2  D:\minio_data\data3  D:\minio_data\data4

訪問(wèn)地址:

  • fhttp://127.0.0.1:49229/    賬號(hào):minioadmin 密碼:minioadmin

三、基本使用方法

1.創(chuàng)建一個(gè)bucket

創(chuàng)建一個(gè)測(cè)試bucket,用以存儲(chǔ)文件

2.上傳文件

上傳文件后,我們可以發(fā)現(xiàn)在,data1 data2 data3 data4 目錄下都進(jìn)行了存儲(chǔ)

測(cè)試minio的數(shù)據(jù)恢復(fù)過(guò)程:

1、首先刪除一個(gè)目錄。

  • 刪除目錄后仍然可以在web控制臺(tái)上傳文件和下載文件。
  • 稍等片刻刪除的目錄自動(dòng)恢復(fù)。

2、刪除兩個(gè)目錄。

  • 刪除兩個(gè)目錄也會(huì)自動(dòng)恢復(fù)。

3、刪除三個(gè)目錄 。

  • 由于 集合中共有4塊硬盤(pán),有大于一半的硬盤(pán)損壞數(shù)據(jù)無(wú)法恢復(fù)。

此時(shí)報(bào)錯(cuò):We encountered an internal error, please try again.  (Read failed.  Insufficient number of drives online)在線驅(qū)動(dòng)器數(shù)量不足。

四、項(xiàng)目依賴(lài)

這些項(xiàng)目中會(huì)用到的依賴(lài)

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.4.3</version>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.8.1</version>
</dependency>
    <!--根據(jù)擴(kuò)展名取mimetype-->
    <dependency>
        <groupId>com.j256.simplemagic</groupId>
        <artifactId>simplemagic</artifactId>
        <version>1.17</version>
    </dependency>
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.11</version>
    </dependency>

 需要將訪問(wèn)權(quán)限設(shè)置public,這樣遠(yuǎn)程才能夠訪問(wèn)到

需要三個(gè)參數(shù)才能連接到minio服務(wù)。

五、圖片上傳

1.本地測(cè)試

包含上傳文件、刪除文件、下載文件、檢查完整性

package com.xuecheng.media;

import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import io.minio.*;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.compress.utils.IOUtils;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilterInputStream;

/**
 * @description 測(cè)試MinIO
 * @author Mr.M
 * @date 2022/9/11 21:24
 * @version 1.0
 */
public class MinioTest {

    static MinioClient minioClient =
            MinioClient.builder()
                    .endpoint("http://124.70.208.223:8089/") //9000端口用于API調(diào)用
                    .credentials("minioadmin", "minioadmin")
                    .build();
    private String getMimeType(String extension){
        if(extension==null)
            extension = "";
        //根據(jù)擴(kuò)展名取出mimeType
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);//根據(jù)擴(kuò)展名獲取MIME類(lèi)型,比如.mp4文件的MIME類(lèi)型是video/mp4
        //通用mimeType,字節(jié)流
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        if(extensionMatch!=null){
            mimeType = extensionMatch.getMimeType();
        }
        return mimeType;
    }

   //上傳文件
   @Test
   void upload() {
       try {
           String filename="E:\\Users\\31118\\Pictures\\Snipaste_2024-11-10_23-08-04.png";
           String bucketName = "001/test001.jpg";
           String bucket ="testbucket";
           String mimeType = getMimeType(".jpg");
           UploadObjectArgs testbucket = UploadObjectArgs.builder()
                   .bucket(bucket)
                   .filename(filename) //本地文件路徑
                   .object(bucketName) //上傳到bucket下的路徑
                   .contentType(mimeType)//默認(rèn)根據(jù)擴(kuò)展名確定文件
                   .build();
           minioClient.uploadObject(testbucket);
           check(filename,bucketName,bucket);
           System.out.println("上傳成功");
       } catch (Exception e) {
           e.printStackTrace();
           System.out.println("上傳失敗");
       }

   }
   //刪除文件
    @Test
    void delete(){
        try {
            RemoveObjectArgs testbucket = RemoveObjectArgs.builder().bucket("testbucket").object("001/test001.jpg").build();
            minioClient.removeObject(testbucket);
            System.out.println("刪除成功");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("刪除失敗");
        }
    }
    //查看/下載文件
    @Test
    void getFile() {
        GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("001/test001.jpg").build();
        try(
                FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
                FileOutputStream outputStream = new FileOutputStream(new File("E:\\圖片.gif"));//輸出路徑
        ) {
            IOUtils.copy(inputStream,outputStream);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //對(duì)上傳之后和下載完成后的文件進(jìn)行完整性檢查,防止丟包
    //將上傳完成后的文件和本地的臨時(shí)文件的md5的值進(jìn)行比對(duì),如果一致,則說(shuō)明上傳和下載成功
    void check(String fileName,String bucketName,String bucket){
        GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket).object(bucketName).build();
        //校驗(yàn)文件的完整性對(duì)文件的內(nèi)容進(jìn)行md5
        try {
            //獲取遠(yuǎn)程文件的md5
            FilterInputStream fileInputStream1 = minioClient.getObject(getObjectArgs);
            String source_md5 = DigestUtils.md5Hex(fileInputStream1);
            //獲取本地文件的md5
            FileInputStream fileInputStream = new FileInputStream(new File(fileName));
            String local_md5 = DigestUtils.md5Hex(fileInputStream);
            if(source_md5.equals(local_md5)){
                System.out.println("下載成功");
            }
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

 上傳文件時(shí)contentType("")屬性并不是強(qiáng)制要求設(shè)置的,但一般建議設(shè)置,以便瀏覽器進(jìn)行識(shí)別該文件的類(lèi)型

2.java服務(wù)器遠(yuǎn)程部署-圖片上傳

minio:
  endpoint: http://124.70.208.223:9000 #API訪問(wèn)路徑
  accessKey: minioadmin #登錄賬號(hào)
  secretKey: minioadmin #登錄密碼
  bucket:
    files: mediafiles #文件/圖片 存在的位置
    videofiles: video #視頻存儲(chǔ)的位置

文件上傳時(shí),獲取md5,作為主鍵保存在文件表中

后續(xù)上傳的如果是同一個(gè)文件時(shí),他們的md5的值是一致的,不在進(jìn)行二次存儲(chǔ)

配置類(lèi)注冊(cè),方便后面直接使用

package com.xuecheng.media.config;

import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @description minio配置
 */
 @Configuration
public class MinioConfig {


 @Value("${minio.endpoint}")
 private String endpoint;
 @Value("${minio.accessKey}")
 private String accessKey;
 @Value("${minio.secretKey}")
 private String secretKey;

 @Bean
 public MinioClient minioClient() {

  return MinioClient.builder()
                  .endpoint(endpoint)
                  .credentials(accessKey, secretKey)
                  .build();
 }
}

控制層接收到MultipartFile后,這是獲取一些常見(jiàn)屬性的辦法,方便對(duì)文件進(jìn)行存儲(chǔ)

    @ApiOperation("上傳文件")
    @RequestMapping(value = "/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE) //對(duì)文件類(lèi)型進(jìn)行聲明
    public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile filedata) throws IOException {
        //文件大小
        long fileSize = filedata.getSize();
        //文件名稱(chēng)
        String originalFilename = filedata.getOriginalFilename();
        //創(chuàng)建臨時(shí)文件
        File tempFile = File.createTempFile("minio", "temp"); //createTempFile 方法會(huì)生成一個(gè)唯一的文件名,該文件名由前綴、一個(gè)隨機(jī)生成的字符串和后綴組成。例如/minio1234567890temp
        //上傳的文件拷貝到臨時(shí)文件
        filedata.transferTo(tempFile);
        //文件路徑
        String absolutePath = tempFile.getAbsolutePath();

    }

3.上傳文件

    /**
     * @description 將文件寫(xiě)入minIO
     * @param localFilePath  文件地址
     * @param bucket  桶
     * @param objectName 對(duì)象名稱(chēng)
     * @return void
     * @author Mr.M
     * @date 2022/10/12 21:22
     */
    public boolean addMediaFilesToMinIO(String localFilePath,String mimeType,String bucket, String objectName) {
        try {
            UploadObjectArgs.Builder  builder = UploadObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .filename(localFilePath);
            if (mimeType != null) {//若上傳的不是分塊文件,指定文件類(lèi)型
                builder.contentType(mimeType);
            }
            UploadObjectArgs testbucket =builder.build();
            minioClient.uploadObject(testbucket);
            log.debug("上傳文件到minio成功,bucket:{},objectName:{}",bucket,objectName);
            System.out.println("上傳成功");
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("上傳文件到minio出錯(cuò),bucket:{},objectName:{},錯(cuò)誤原因:{}",bucket,objectName,e.getMessage(),e);
        }
        return false;
    }

4.需要用到的工具方法

獲取文件Md5

 //獲取文件的md5
 private String getFileMd5(File file) {
  try (FileInputStream fileInputStream = new FileInputStream(file)) {
   String fileMd5 = DigestUtils.md5Hex(fileInputStream);
   return fileMd5;
  } catch (Exception e) {
   e.printStackTrace();
   return null;
  }
 }

獲取年月日結(jié)構(gòu)目錄

 //獲取文件默認(rèn)存儲(chǔ)目錄路徑 年/月/日
 private String getDefaultFolderPath() {
  SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  String format = sdf.format(new Date());
     return format.replace("-", "/")+"/";
 }

根據(jù)擴(kuò)展名獲取MIME類(lèi)型

比如.mp4文件的MIME類(lèi)型是video/mp4

 private String getMimeType(String extension){ //傳入.jpg
  if(extension==null)
   extension = "";
  //根據(jù)擴(kuò)展名取出mimeType
  ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
  //通用mimeType,字節(jié)流
  String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
  if(extensionMatch!=null){
   mimeType = extensionMatch.getMimeType();
  }
  return mimeType;
 }

這邊只進(jìn)行關(guān)鍵信息的展示,數(shù)據(jù)庫(kù)相關(guān)操作根據(jù)項(xiàng)目自行處理

圖片的訪問(wèn)鏈接是:

服務(wù)器ip:9000/mediafiles/2024/11/27/e0abb735ab793fae5568c2ed537ab37c.jpg

注意9000是API地址,9001是web服務(wù)地址

六、視頻上傳-斷點(diǎn)續(xù)傳

minio限制,視頻至少以5mb,劃分

1.文件上傳前檢查文件是否已上傳

先通過(guò)前端計(jì)算出視頻md5的值,傳給后端,檢測(cè)該視頻是否已經(jīng)存在,

    @ApiOperation(value = "文件上傳前檢查文件")
    @PostMapping("/upload/checkfile")
    public RestResponse<Boolean> checkfile(@RequestParam("fileMd5") String fileMd5) throws Exception {
        return bigFilesService.checkFile(fileMd5);
    }
    @Override
    public RestResponse<Boolean> checkFile(String fileMd5) {
        //查詢(xún)文件信息
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles != null) {
            //桶
            String bucket = mediaFiles.getBucket();
            //存儲(chǔ)目錄
            String filePath = mediaFiles.getFilePath();
            //文件流
            InputStream stream = null;
            try {
                stream = minioClient.getObject(
                        GetObjectArgs.builder()
                                .bucket(bucket)
                                .object(filePath)
                                .build());

                if (stream != null) {
                    //文件已存在
                    return RestResponse.success(true);
                }
            } catch (Exception e) {
                log.info("文件不存在,準(zhǔn)備開(kāi)始分塊上傳");
            }
        }
        //文件不存在
        return RestResponse.success(false);
    }

2.分塊上傳前檢測(cè)分塊是否已上傳

根據(jù)后端的響應(yīng)信息,若該視頻不存在,前端對(duì)視頻劃分為一個(gè)個(gè)分塊,并計(jì)算每個(gè)分塊的md5值

將分塊的md5值,傳給后端,判斷該分塊是否存在,若該分塊不存在則上傳分塊

  @ApiOperation(value = "分塊文件上傳前的檢測(cè)")
    @PostMapping("/upload/checkchunk")
    public RestResponse<Boolean> checkchunk(@RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) throws Exception {
        return bigFilesService.checkChunk(fileMd5,chunk);
    }
 @Override
    public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {

        //得到分塊文件目錄
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //得到分塊文件的路徑
        String chunkFilePath = chunkFileFolderPath + chunkIndex;

        //文件流
        InputStream fileInputStream = null;
        try {
            fileInputStream = minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(bucket_videoFiles)
                            .object(chunkFilePath)
                            .build());

            if (fileInputStream != null) {
                //分塊已存在
                log.info("分塊{}已存在",chunkIndex);
                return RestResponse.success(true);
            }
        } catch (Exception e) {
            //minio中沒(méi)有該分塊,上傳分塊
            log.info("分塊{}不存在,開(kāi)始上傳",chunkIndex);
        }
        //分塊未存在
        return RestResponse.success(false);
    }

3.上傳分塊

    @ApiOperation(value = "上傳分塊文件")
    @PostMapping("/upload/uploadchunk")
    public RestResponse uploadchunk(@RequestParam("file") MultipartFile file, @RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) throws Exception {
        //創(chuàng)建臨時(shí)文件
        File tempFile = File.createTempFile("minio", "temp");
        //上傳的文件拷貝到臨時(shí)文件
        file.transferTo(tempFile);
        //文件路徑
        String localFilePath = tempFile.getAbsolutePath();
        return bigFilesService.uploadChunk(fileMd5,chunk,localFilePath);
    }
    @Value("${minio.bucket.videofiles}")
    private String bucket_videoFiles;

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


        //得到分塊文件的目錄路徑
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //得到分塊文件的路徑
        String chunkFilePath = chunkFileFolderPath + chunk;

        try {
            //將文件存儲(chǔ)至minIO
            addMediaFilesToMinIO(localFilePath,null, bucket_videoFiles,chunkFilePath);
            return RestResponse.success(true);
        } catch (Exception ex) {
            ex.printStackTrace();
            log.debug("上傳分塊文件:{},失敗:{}",chunkFilePath,ex.getMessage());
        }
        return RestResponse.validfail(false,"上傳分塊失敗");
    }

 /**
  * @description 將文件寫(xiě)入minIO
  * @param localFilePath  文件地址
  * @param bucket  桶
  * @param objectName 對(duì)象名稱(chēng)
  * @return void
  * @author Mr.M
  * @date 2022/10/12 21:22
  */
 public boolean addMediaFilesToMinIO(String localFilePath,String mimeType,String bucket, String objectName) {
  try {
   UploadObjectArgs testbucket = UploadObjectArgs.builder()
           .bucket(bucket)
           .object(objectName)
           .filename(localFilePath)
           .contentType(mimeType)
           .build();
   minioClient.uploadObject(testbucket);
   log.debug("上傳文件到minio成功,bucket:{},objectName:{}",bucket,objectName);
   System.out.println("上傳成功");
   return true;
  } catch (Exception e) {
   e.printStackTrace();
   log.error("上傳文件到minio出錯(cuò),bucket:{},objectName:{},錯(cuò)誤原因:{}",bucket,objectName,e.getMessage(),e);
   XueChengPlusException.cast("上傳文件到文件系統(tǒng)失敗");
  }
  return false;
 }

md5目錄結(jié)構(gòu)

分塊存儲(chǔ)目錄:d/a/da112e234adasdasd/chunk

    //得到分塊文件的目錄
    private String getChunkFileFolderPath(String fileMd5) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
    }

修改文件大小的限制

 前端對(duì)文件分塊的大小為5MB,SpringBoot web默認(rèn)上傳文件的大小限制為1MB

spring:
  servlet:
    multipart:
      max-file-size: 50MB #單個(gè)文件的大小限制
      max-request-size: 50MB #單次請(qǐng)求的大小限制

4.合并分塊

@ApiOperation(value = "合并文件")
@PostMapping("/upload/mergechunks")
public RestResponse mergechunks(@RequestParam("fileMd5") String fileMd5, @RequestParam("fileName") String fileName, @RequestParam("chunkTotal") int chunkTotal) throws Exception {
    Long companyId = 1232141425L;
    UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
    uploadFileParamsDto.setFileType("001002");
    uploadFileParamsDto.setTags("課程視頻");
    uploadFileParamsDto.setRemark("");
    uploadFileParamsDto.setFilename(fileName);

    return bigFilesService.mergechunks(companyId,fileMd5,chunkTotal,uploadFileParamsDto);
}
 @Override
    public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
        //=====獲取分塊文件路徑=====
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //組成將分塊文件路徑組成 List<ComposeSource>
        List<ComposeSource> sourceObjectList = Stream.iterate(0, i -> ++i) //從0開(kāi)始,迭代到chunkTotal,依次獲取所有分塊文件,0 1 2
                .limit(chunkTotal)
                .map(i -> ComposeSource.builder()
                        .bucket(bucket_videoFiles)
                        .object(chunkFileFolderPath+i)
                        .build())
                .collect(Collectors.toList());
        //=====合并=====
        //文件名稱(chēng)
        String fileName = uploadFileParamsDto.getFilename();
        //文件擴(kuò)展名
        String extName = fileName.substring(fileName.lastIndexOf("."));
        //合并文件路徑
        String mergeFilePath = getFilePathByMd5(fileMd5, extName);
        try {
            //合并文件
            ObjectWriteResponse response = minioClient.composeObject(
                    ComposeObjectArgs.builder()
                            .bucket(bucket_videoFiles)
                            .object(mergeFilePath)
                            .sources(sourceObjectList)
                            .build());
            log.debug("合并文件成功:{}",mergeFilePath);
        } catch (Exception e) {
            log.debug("合并文件失敗,fileMd5:{},異常:{}",fileMd5,e.getMessage(),e);
            return RestResponse.validfail(false, "合并文件失敗。");
        }

        // ====驗(yàn)證md5====
        File minioFile = downloadFileFromMinIO(bucket_videoFiles,mergeFilePath);
        if(minioFile == null){
            log.debug("下載合并后文件失敗,mergeFilePath:{}",mergeFilePath);
            return RestResponse.validfail(false, "下載合并后文件失敗。");
        }

        try (InputStream newFileInputStream = new FileInputStream(minioFile)) {
            //minio上文件的md5值
            String md5Hex = DigestUtils.md5Hex(newFileInputStream);
            //比較md5值,不一致則說(shuō)明文件不完整
            if(!fileMd5.equals(md5Hex)){
                return RestResponse.validfail(false, "文件合并校驗(yàn)失敗,最終上傳失敗。");
            }
            //文件大小
            uploadFileParamsDto.setFileSize(minioFile.length());
        }catch (Exception e){
            log.debug("校驗(yàn)文件失敗,fileMd5:{},異常:{}",fileMd5,e.getMessage(),e);
            return RestResponse.validfail(false, "文件合并校驗(yàn)失敗,最終上傳失敗。");
        }finally {
            if(minioFile!=null){ //刪除下載的臨時(shí)文件
                minioFile.delete();
            }
        }

        //文件入庫(kù)
        currentProxy.addMediaFilesToDb(companyId,fileMd5,uploadFileParamsDto,bucket_videoFiles,mergeFilePath);
        //=====清除分塊文件=====
        clearChunkFiles(chunkFileFolderPath,chunkTotal);
        return RestResponse.success(true);
    }

下載至本地,用于md5檢測(cè)

獲得合并后文件存儲(chǔ)路徑

    private String getFilePathByMd5(String fileMd5,String fileExt){
        return   fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" +fileMd5 +fileExt;
    }

將上傳后的文件下載至本地

將下載文件的md5的值與前端傳遞過(guò)來(lái)時(shí)視頻md5值進(jìn)行比較,判斷視頻上傳時(shí)是否出現(xiàn)丟包

 public File downloadFileFromMinIO(String bucket,String objectName){
        //臨時(shí)文件
        File minioFile = null;
        FileOutputStream outputStream = null;
        try{
            InputStream stream = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .build());
            //創(chuàng)建臨時(shí)文件
            minioFile=File.createTempFile("minio", ".merge");
            outputStream = new FileOutputStream(minioFile);
            IOUtils.copy(stream,outputStream);
            return minioFile;
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(outputStream!=null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

刪除分塊

視頻上傳成功后,刪除之前上傳的分塊

   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("video").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);
        }
    }

分塊文件清理問(wèn)題

上傳一個(gè)文件進(jìn)行分塊上傳,上傳一半不傳了,之前上傳到minio的分塊文件要清理嗎?怎么做的?

1、在數(shù)據(jù)庫(kù)中有一張文件表記錄minio中存儲(chǔ)的文件信息。

2、文件開(kāi)始上傳時(shí)會(huì)寫(xiě)入文件表,狀態(tài)為上傳中,上傳完成會(huì)更新?tīng)顟B(tài)為上傳完成。

3、當(dāng)一個(gè)文件傳了一半不再上傳了說(shuō)明該文件沒(méi)有上傳完成,會(huì)有定時(shí)任務(wù)去查詢(xún)文件表中的記錄,如果文件未上傳完成則刪除minio中沒(méi)有上傳成功的文件目錄。

視頻文件格式轉(zhuǎn)換

視頻文件的格式有很多中,我們需要把視頻格式統(tǒng)一轉(zhuǎn)換為mp4,下面以avi文件格式轉(zhuǎn)換為mp4格式舉例

FFmpeg進(jìn)行媒體文件的轉(zhuǎn)換

ffmpeg的安裝及基本使用:

xxl-job分布式任務(wù)調(diào)度

由于媒體文件轉(zhuǎn)換需要處理的時(shí)間,我們采用xxl-job進(jìn)行分布式任務(wù)調(diào)度

xxl-job的基本使用方法:

多服務(wù)執(zhí)行:

-Dserver.port=63051 -Dxxl.job.executor.port=9998

什么是樂(lè)觀鎖、悲觀鎖?

synchronized是一種悲觀鎖,在執(zhí)行被synchronized包裹的代碼時(shí)需要首先獲取鎖,沒(méi)有拿到鎖則無(wú)法執(zhí)行,是總悲觀的認(rèn)為別的線程會(huì)去搶?zhuān)砸^鎖。

樂(lè)觀鎖的思想是它不認(rèn)為會(huì)有線程去爭(zhēng)搶?zhuān)M管去執(zhí)行,如果沒(méi)有執(zhí)行成功就再去重試。

為了防止多個(gè)分布式任務(wù),執(zhí)行同一個(gè)行為,需要使用分布鎖進(jìn)行來(lái)控制

1、基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布鎖

利用數(shù)據(jù)庫(kù)主鍵唯一性的特點(diǎn),或利用數(shù)據(jù)庫(kù)唯一索引、行級(jí)鎖的特點(diǎn),多個(gè)線程同時(shí)去更新相同的記錄,誰(shuí)更新成功誰(shuí)就搶到鎖。

數(shù)據(jù)庫(kù)表的設(shè)計(jì)

在上傳文件之后,將需要格式轉(zhuǎn)換的文件,存入media_process數(shù)據(jù)庫(kù)

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for media_process
-- ----------------------------
DROP TABLE IF EXISTS `media_process`;
CREATE TABLE `media_process`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `file_id` varchar(120) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '文件標(biāo)識(shí)',
  `filename` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '文件名稱(chēng)',
  `bucket` varchar(128) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '存儲(chǔ)桶',
  `file_path` varchar(512) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '存儲(chǔ)路徑',
  `status` varchar(12) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '狀態(tài),1:未處理,2:處理成功  3處理失敗 4處理中',
  `create_date` datetime NOT NULL COMMENT '上傳時(shí)間',
  `finish_date` datetime NULL DEFAULT NULL COMMENT '完成時(shí)間',
  `fail_count` int NULL DEFAULT 0 COMMENT '失敗次數(shù)',
  `url` varchar(1024) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '媒資文件訪問(wèn)地址',
  `errormsg` varchar(1024) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '失敗原因',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `unique_fileid`(`file_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

誰(shuí)先搶到,誰(shuí)處理

視頻處理完成后,轉(zhuǎn)存如 media_process_history表中,在media_process表中,刪除該條記錄

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for media_process_history
-- ----------------------------
DROP TABLE IF EXISTS `media_process_history`;
CREATE TABLE `media_process_history`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `file_id` varchar(120) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '文件標(biāo)識(shí)',
  `filename` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '文件名稱(chēng)',
  `bucket` varchar(128) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '存儲(chǔ)源',
  `status` varchar(12) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '狀態(tài),1:未處理,2:處理成功  3處理失敗',
  `create_date` datetime NOT NULL COMMENT '上傳時(shí)間',
  `finish_date` datetime NOT NULL COMMENT '完成時(shí)間',
  `url` varchar(1024) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '媒資文件訪問(wèn)地址',
  `fail_count` int NULL DEFAULT 0 COMMENT '失敗次數(shù)',
  `file_path` varchar(512) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '文件路徑',
  `errormsg` varchar(1024) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '失敗原因',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;
   @XxlJob("videoJobHandler")
    public void videoJobHandler() throws Exception {
        // 分片參數(shù)
    int shardIndex = XxlJobHelper.getShardIndex();
    int shardTotal = XxlJobHelper.getShardTotal();
    List<MediaProcess> mediaProcessList = null;
    int size = 0;
    try {
        //取出cpu核心數(shù)作為一次處理數(shù)據(jù)的條數(shù)
        int processors = Runtime.getRuntime().availableProcessors();
        //獲取待處理視頻
        //一次處理視頻數(shù)量不要超過(guò)cpu核心數(shù),避免CPU超載
        mediaProcessList = mediaFileProcessService.getMediaProcessList(shardIndex, shardTotal, processors);
        size = mediaProcessList.size();
        log.debug("取出待處理視頻任務(wù){(diào)}條", size);
        if (size < 0) {
            return;
        }
    } catch (Exception e) {
        e.printStackTrace();
        return;
    }
    //啟動(dòng)size個(gè)線程的線程池
    ExecutorService threadPool = Executors.newFixedThreadPool(size);
    //計(jì)數(shù)器,用于等待所有線程執(zhí)行完畢
    CountDownLatch countDownLatch = new CountDownLatch(size);
    //將處理任務(wù)加入線程池
    mediaProcessList.forEach(mediaProcess -> {
        threadPool.execute(() -> { //所以線程,通過(guò)循環(huán)同時(shí)啟動(dòng)
            try {
                //任務(wù)id
                Long taskId = mediaProcess.getId();
                //搶占任務(wù),將任務(wù)status狀態(tài)改為4正在處理
                boolean b = mediaFileProcessService.startTask(taskId);
                if (!b) {
                    return;
                }
                log.debug("開(kāi)始執(zhí)行任務(wù):{}", mediaProcess);
                //下邊是處理邏輯
                //桶
                String bucket = mediaProcess.getBucket();
                //存儲(chǔ)路徑
                String filePath = mediaProcess.getFilePath();
                //原始視頻的md5值
                String fileId = mediaProcess.getFileId();
                //原始文件名稱(chēng)
                String filename = mediaProcess.getFilename();
                //將要處理的文件下載到服務(wù)器上
                File originalFile = bigFilesService.downloadFileFromMinIO(mediaProcess.getBucket(), mediaProcess.getFilePath());
                if (originalFile == null) {
                    log.debug("下載待處理文件失敗,originalFile:{}", mediaProcess.getBucket().concat(mediaProcess.getFilePath()));
                    mediaFileProcessService.saveProcessFinishStatus(mediaProcess.getId(), "3", fileId, null, "下載待處理文件失敗");
                    return;
                }
                //處理結(jié)束的視頻文件
                File mp4File = null;
                //創(chuàng)建臨時(shí)文件,作為轉(zhuǎn)化后的文件
                try {
                    mp4File = File.createTempFile("mp4", ".mp4");
                } catch (IOException e) {
                    log.error("創(chuàng)建mp4臨時(shí)文件失敗");
                    //保存任務(wù)是失敗的結(jié)果
                    mediaFileProcessService.saveProcessFinishStatus(mediaProcess.getId(), "3", fileId, null, "創(chuàng)建mp4臨時(shí)文件失敗");
                    return;
                }
                //視頻處理結(jié)果
                String result = "";
                try {
                    String absolutePath = mp4File.getAbsolutePath();//包含了,文件名
                    String localPath = absolutePath.substring(0, absolutePath.lastIndexOf("\\")+1);
                    //開(kāi)始處理視頻
                    Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpegpath, originalFile.getAbsolutePath(), mp4File.getName(),localPath);
                    //開(kāi)始視頻轉(zhuǎn)換,成功將返回success
                    result = videoUtil.generateMp4();
                } catch (Exception e) {
                    e.printStackTrace();
                    log.error("處理視頻文件:{},出錯(cuò):{}", mediaProcess.getFilePath(), e.getMessage());
                    mediaFileProcessService.saveProcessFinishStatus(mediaProcess.getId(), "3", fileId, null, "下載待處理文件失敗");
                }
                if (!result.equals("success")) {
                    //記錄錯(cuò)誤信息
                    log.error("處理視頻失敗,視頻地址:{},錯(cuò)誤信息:{}", bucket + filePath, result);
                    mediaFileProcessService.saveProcessFinishStatus(mediaProcess.getId(), "3", fileId, null, result);
                    return;
                }
    
                //將mp4上傳至minio
                //mp4在minio的存儲(chǔ)路徑
                String objectName = getFilePath(fileId, ".mp4");
                //訪問(wèn)url
                String url = "/" + bucket + "/" + objectName;
                try {
                    bigFilesService.addMediaFilesToMinIO(mp4File.getAbsolutePath(), "video/mp4", bucket, objectName);
                    //將url存儲(chǔ)至數(shù)據(jù),并更新?tīng)顟B(tài)為成功,并將待處理視頻記錄刪除存入歷史
                    mediaFileProcessService.saveProcessFinishStatus(mediaProcess.getId(), "2", fileId, url, null);
                } catch (Exception e) {
                    log.error("上傳視頻失敗或入庫(kù)失敗,視頻地址:{},錯(cuò)誤信息:{}", bucket + objectName, e.getMessage());
                    //最終還是失敗了
                    mediaFileProcessService.saveProcessFinishStatus(mediaProcess.getId(), "3", fileId, null, "處理后視頻上傳或入庫(kù)失敗");
                }
            }finally {
                countDownLatch.countDown();  //線程數(shù)減一
            }
        });
    });
    //等待,給一個(gè)充裕的超時(shí)時(shí)間,防止無(wú)限等待,到達(dá)超時(shí)時(shí)間還沒(méi)有處理完成則結(jié)束任務(wù)
    countDownLatch.await(30, TimeUnit.MINUTES);
    }

    private String getFilePath(String fileMd5,String fileExt){
        return   fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" +fileMd5 +fileExt;
    }

當(dāng)前需要處理的視頻文件,需要根據(jù)計(jì)算機(jī) 當(dāng)前計(jì)算機(jī)啟動(dòng)的服務(wù)下標(biāo)(從0開(kāi)始...),當(dāng)前計(jì)算機(jī)啟動(dòng)服務(wù)總個(gè)數(shù) 和 計(jì)算機(jī)的線程數(shù)計(jì)算得出,因?yàn)槿粲?jì)算機(jī)的線程數(shù)為8,一次性最多處理8個(gè)視頻

sql語(yǔ)句這樣設(shè)計(jì)的目的是為了給每個(gè)服務(wù)(執(zhí)行器),分配任務(wù)。一臺(tái)8核的計(jì)算機(jī),一次性最多分配8個(gè)任務(wù)

 @Override
 public List<MediaProcess> getMediaProcessList(int shardIndex, int shardTotal, int count) {
     return mediaProcessMapper.selectListByShardIndex(shardTotal, shardIndex, count);
 }
    /**
     * @description 根據(jù)分片參數(shù)獲取待處理任務(wù),一次處理視頻數(shù)量不要超過(guò)cpu核心數(shù),避免CPU超載
     * @param shardTotal  分片總數(shù)
     * @param shardIndex  分片序號(hào)
     * @param count 任務(wù)數(shù)
     * @return java.util.List<com.xuecheng.media.model.po.MediaProcess>
     * @author Mr.M
     * @date 2022/9/14 8:54
     */
    @Select("select * from media_process t where t.id % #{shardTotal} = #{shardIndex} and (t.status = '1' or t.status = '3') and t.fail_count < 3 limit #{count}")
    List<MediaProcess> selectListByShardIndex(@Param("shardTotal") int shardTotal, @Param("shardIndex") int shardIndex, @Param("count") int count);

 Sql語(yǔ)句的查詢(xún),原理如下

上邊兩個(gè)執(zhí)行器實(shí)例那么分片總數(shù)為2,序號(hào)為0、1,從任務(wù)1開(kāi)始,如下:

1  %  2 = 1    執(zhí)行器2執(zhí)行

2  %  2 =  0    執(zhí)行器1執(zhí)行

3  %  2 =  1     執(zhí)行器2執(zhí)行

以此類(lèi)推.

一個(gè)服務(wù)(執(zhí)行器),所以線程同時(shí)執(zhí)行,為了防止多個(gè)線程執(zhí)行的是同一個(gè)任務(wù),當(dāng)前線程執(zhí)行時(shí)需要前開(kāi)啟任務(wù)時(shí),將數(shù)據(jù)庫(kù)的狀態(tài)設(shè)置為4,表示正在處理中,防止下次執(zhí)行時(shí),被其他執(zhí)行器搶占

    /**
     * 開(kāi)啟一個(gè)任務(wù)
     * @param id 任務(wù)id
     * @return 更新記錄數(shù)
     */
    @Update("update media_process m set m.status='4' where (m.status='1' or m.status='3') and m.fail_count<3 and m.id=#{id}")
    int startTask(@Param("id") long id);

若視頻轉(zhuǎn)換過(guò)程中出現(xiàn)異常,失敗次數(shù)+1,失敗次數(shù)達(dá)到3此不在執(zhí)行。

若視頻轉(zhuǎn)換成功,修改任務(wù)狀態(tài)為2,并將其存入歷史進(jìn)程表中,在當(dāng)前表中刪除該條記錄

@Transactional
 @Override
 public void saveProcessFinishStatus(Long taskId, String status, String fileId, String url, String errorMsg) {
  //查出任務(wù),如果不存在則直接返回
  MediaProcess mediaProcess = mediaProcessMapper.selectById(taskId);
  if(mediaProcess == null){
   return ;
  }
  //處理失敗,更新任務(wù)處理結(jié)果
  LambdaQueryWrapper<MediaProcess> queryWrapperById = new LambdaQueryWrapper<MediaProcess>().eq(MediaProcess::getId, taskId);
  //處理失敗
  if(status.equals("3")){
   MediaProcess mediaProcess_u = new MediaProcess();
   mediaProcess_u.setStatus("3");
   mediaProcess_u.setErrormsg(errorMsg);
   mediaProcess_u.setFailCount(mediaProcess.getFailCount()+1);
   mediaProcessMapper.update(mediaProcess_u,queryWrapperById);
   log.debug("更新任務(wù)處理狀態(tài)為失敗,任務(wù)信息:{}",mediaProcess_u);
   return ;
  }
  //任務(wù)處理成功
  MediaFiles mediaFiles = mediaFilesMapper.selectById(fileId);
  if(mediaFiles!=null){
   //更新媒資文件中的訪問(wèn)url
   mediaFiles.setUrl(url);
   mediaFilesMapper.updateById(mediaFiles);
  }
  //處理成功,更新url和狀態(tài)
  mediaProcess.setUrl(url);
  mediaProcess.setStatus("2");
  mediaProcess.setFinishDate(LocalDateTime.now());
  mediaProcessMapper.updateById(mediaProcess);

  //添加到歷史記錄
  MediaProcessHistory mediaProcessHistory = new MediaProcessHistory();
  BeanUtils.copyProperties(mediaProcess, mediaProcessHistory);
  mediaProcessHistoryMapper.insert(mediaProcessHistory);
  //刪除mediaProcess
  mediaProcessMapper.deleteById(mediaProcess.getId());

 }

工具類(lèi)

檢查視頻時(shí)長(zhǎng),校驗(yàn)兩個(gè)視頻時(shí)長(zhǎng)是否相等,等待進(jìn)程處理完畢

package com.xuecheng.base.utils;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * 此文件作為視頻文件處理父類(lèi),提供:
 * 1、查看視頻時(shí)長(zhǎng)
 * 2、校驗(yàn)兩個(gè)視頻的時(shí)長(zhǎng)是否相等
 *
 */
public class VideoUtil {

    String ffmpeg_path;//ffmpeg的安裝位置

    public VideoUtil(String ffmpeg_path){
        this.ffmpeg_path = ffmpeg_path;
    }


    //檢查視頻時(shí)間是否一致
    public Boolean check_video_time(String source,String target) {
        String source_time = get_video_time(source);
        //取出時(shí)分秒
        source_time = source_time.substring(source_time.lastIndexOf(":")+1);
        String target_time = get_video_time(target);
        //取出時(shí)分秒
        target_time = target_time.substring(target_time.lastIndexOf(":")+1);
        if(source_time == null || target_time == null){
            return false;
        }
        float v1 = Float.parseFloat(source_time);
        float v2 = Float.parseFloat(target_time);
        float abs = Math.abs(v1 - v2);
        if(abs<1){//轉(zhuǎn)化是會(huì)有細(xì)微差距,屬于正?,F(xiàn)象
            return true;
        }
        return false;
    }


    //獲取視頻時(shí)間(時(shí):分:秒:毫秒)
    public String get_video_time(String video_path) {
        /*
        ffmpeg -i  lucene.mp4
         */
        List<String> commend = new ArrayList<String>();
        commend.add(ffmpeg_path);
        commend.add("-i");
        commend.add(video_path);
        try {
            ProcessBuilder builder = new ProcessBuilder();
            builder.command(commend);
            //將標(biāo)準(zhǔn)輸入流和錯(cuò)誤輸入流合并,通過(guò)標(biāo)準(zhǔn)輸入流程讀取信息
            builder.redirectErrorStream(true);
            Process p = builder.start();
            String outstring = waitFor(p);
            System.out.println(outstring);
            int start = outstring.trim().indexOf("Duration: ");
            if(start>=0){
                int end = outstring.trim().indexOf(", start:");
                if(end>=0){
                    String time = outstring.substring(start+10,end);
                    if(time!=null && !time.equals("")){
                        return time.trim();
                    }
                }
            }

        } catch (Exception ex) {

            ex.printStackTrace();

        }
        return null;
    }
    //等待一個(gè)外部進(jìn)程(通過(guò)Process對(duì)象表示)完成,并在此過(guò)程中捕獲該進(jìn)程的標(biāo)準(zhǔn)輸出和錯(cuò)誤輸出。
    public String waitFor(Process p) {
        InputStream in = null;
        InputStream error = null;
        String result = "error";
        int exitValue = -1;
        StringBuffer outputString = new StringBuffer();
        try {
            in = p.getInputStream();
            error = p.getErrorStream();
            boolean finished = false;
            int maxRetry = 600;//每次休眠1秒,最長(zhǎng)執(zhí)行時(shí)間10分種
            int retry = 0;
            while (!finished) {
                if (retry > maxRetry) {
                    return "error";
                }
                try {
                    while (in.available() > 0) {
                        Character c = new Character((char) in.read());
                        outputString.append(c);
                        System.out.print(c);
                    }
                    while (error.available() > 0) {
                        Character c = new Character((char) in.read());
                        outputString.append(c);
                        System.out.print(c);
                    }
                    //進(jìn)程未結(jié)束時(shí)調(diào)用exitValue將拋出異常
                    exitValue = p.exitValue();
                    finished = true;

                } catch (IllegalThreadStateException e) {
                    Thread.currentThread().sleep(1000);//休眠1秒
                    retry++;
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    System.out.println(e.getMessage());
                }
            }
        }
        return outputString.toString();

    }


    public static void main(String[] args) throws IOException {
        String ffmpeg_path = "D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe";//ffmpeg的安裝位置
        VideoUtil videoUtil = new VideoUtil(ffmpeg_path);
        String video_time = videoUtil.get_video_time("E:\\ffmpeg_test\\1.avi");
        System.out.println(video_time);
    }
}

avi格式轉(zhuǎn)mp4格式

package com.xuecheng.base.utils;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class Mp4VideoUtil extends VideoUtil {

    String ffmpeg_path;//ffmpeg的安裝位置
    String video_path;
    String mp4_name;
    String mp4folder_path;
    public Mp4VideoUtil(String ffmpeg_path, String video_path, String mp4_name, String mp4folder_path){
        super(ffmpeg_path);
        this.ffmpeg_path = ffmpeg_path;
        this.video_path = video_path;
        this.mp4_name = mp4_name;
        this.mp4folder_path = mp4folder_path;
    }
    //清除已生成的mp4
    private void clear_mp4(String mp4_path){
        //刪除原來(lái)已經(jīng)生成的m3u8及ts文件
        File mp4File = new File(mp4_path);
        if(mp4File.exists() && mp4File.isFile()){
            mp4File.delete();
        }
    }
    /**
     * 視頻編碼,生成mp4文件
     * @return 成功返回success,失敗返回控制臺(tái)日志
     */
    public String generateMp4(){
        //清除已生成的mp4
//        clear_mp4(mp4folder_path+mp4_name);
        clear_mp4(mp4folder_path);
        /*
        ffmpeg.exe -i  lucene.avi -c:v libx264 -s 1280x720 -pix_fmt yuv420p -b:a 63k -b:v 753k -r 18 .\lucene.mp4
         */
        List<String> commend = new ArrayList<String>();
        //commend.add("D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe");
        commend.add(ffmpeg_path);
        commend.add("-i");
//        commend.add("D:\\BaiduNetdiskDownload\\test1.avi");
        commend.add(video_path);
        commend.add("-c:v");
        commend.add("libx264");
        commend.add("-y");//覆蓋輸出文件
        commend.add("-s");
        commend.add("1280x720");
        commend.add("-pix_fmt");
        commend.add("yuv420p");
        commend.add("-b:a");
        commend.add("63k");
        commend.add("-b:v");
        commend.add("753k");
        commend.add("-r");
        commend.add("18");
        commend.add(mp4folder_path  + mp4_name );
        String outstring = null;
        try {
            ProcessBuilder builder = new ProcessBuilder();
            builder.command(commend);
            //將標(biāo)準(zhǔn)輸入流和錯(cuò)誤輸入流合并,通過(guò)標(biāo)準(zhǔn)輸入流程讀取信息
            builder.redirectErrorStream(true);
            Process p = builder.start();
            outstring = waitFor(p);

        } catch (Exception ex) {

            ex.printStackTrace();

        }
        Boolean check_video_time = this.check_video_time(video_path, mp4folder_path + mp4_name);
        if(!check_video_time){
            return outstring;
        }else{
            return "success";
        }
    }

    public static void main(String[] args) throws IOException {
        //ffmpeg的路徑
        String ffmpeg_path = "F:\\environment\\ffmpeg-7.0.2-full_build\\bin\\ffmpeg.exe";//ffmpeg的安裝位置
        //源avi視頻的路徑
        String video_path = "E:\\Users\\31118\\Videos\\1.avi";
        //轉(zhuǎn)換后mp4文件的名稱(chēng)
        String mp4_name = "1.mp4";
        //轉(zhuǎn)換后mp4文件的路徑
        String mp4_path = "E:\\Users\\31118\\Videos\\"; //結(jié)尾路徑,需要加上\\
        //創(chuàng)建工具類(lèi)對(duì)象
        Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4_path);
        //開(kāi)始視頻轉(zhuǎn)換,成功將返回success
        String s = videoUtil.generateMp4();
        System.out.println(s);

    }
}

任務(wù)補(bǔ)償機(jī)制

如果有線程搶占了某個(gè)視頻的處理任務(wù),如果線程處理過(guò)程中掛掉了,該視頻的狀態(tài)將會(huì)一直是處理中,其它線程將無(wú)法處理,這個(gè)問(wèn)題需要用補(bǔ)償機(jī)制。

單獨(dú)啟動(dòng)一個(gè)任務(wù)找到待處理任務(wù)表中超過(guò)執(zhí)行期限但仍在處理中的任務(wù),將任務(wù)的狀態(tài)改為執(zhí)行失敗。

任務(wù)執(zhí)行期限是處理一個(gè)視頻的最大時(shí)間,比如定為30分鐘,通過(guò)任務(wù)的啟動(dòng)時(shí)間去判斷任務(wù)是否超過(guò)執(zhí)行期限。

總結(jié)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Java實(shí)現(xiàn)桌面日歷

    Java實(shí)現(xiàn)桌面日歷

    這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)桌面日歷,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-06-06
  • JAVA中讀取文件(二進(jìn)制,字符)內(nèi)容的幾種方法總結(jié)

    JAVA中讀取文件(二進(jìn)制,字符)內(nèi)容的幾種方法總結(jié)

    本篇文章主要介紹了JAVA中讀取文件(二進(jìn)制,字符)內(nèi)容的方法總結(jié),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-02-02
  • Java代碼精簡(jiǎn)之道(推薦)

    Java代碼精簡(jiǎn)之道(推薦)

    這篇文章主要給大家介紹了Java代碼精簡(jiǎn)之道,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-11-11
  • Java數(shù)據(jù)結(jié)構(gòu)之?dāng)?shù)組(動(dòng)力節(jié)點(diǎn)之Java學(xué)院整理)

    Java數(shù)據(jù)結(jié)構(gòu)之?dāng)?shù)組(動(dòng)力節(jié)點(diǎn)之Java學(xué)院整理)

    這篇文章主要介紹了Java數(shù)據(jù)結(jié)構(gòu)之?dāng)?shù)組(動(dòng)力節(jié)點(diǎn)之Java學(xué)院整理)的相關(guān)資料,包括創(chuàng)建和內(nèi)存分配,數(shù)組封裝后的使用等,需要的朋友參考下吧
    2017-04-04
  • 如何查看java進(jìn)程gc情況

    如何查看java進(jìn)程gc情況

    這篇文章主要介紹了如何查看java進(jìn)程gc情況,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2025-07-07
  • SpringBoot配置lombok與logback過(guò)程解析

    SpringBoot配置lombok與logback過(guò)程解析

    這篇文章主要介紹了SpringBoot配置lombok與logback過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-05-05
  • 淺談Java中的橋接方法與泛型的逆變和協(xié)變

    淺談Java中的橋接方法與泛型的逆變和協(xié)變

    對(duì)應(yīng)于Java當(dāng)中,協(xié)變對(duì)應(yīng)的就是<? extends XXX>,而逆變對(duì)應(yīng)的就是<? super XXX>,本文詳細(xì)的介紹了Java中的橋接方法與泛型的逆變和協(xié)變,感興趣的可以了解一下
    2022-04-04
  • Java中的字節(jié)流文件讀取教程(二)

    Java中的字節(jié)流文件讀取教程(二)

    這篇文章主要給大家介紹了關(guān)于Java中字節(jié)流文件讀取的相關(guān)資料,本文屬于之前文章的延長(zhǎng)篇,有需要的朋友可以先看看上一篇文章,相信會(huì)對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2018-07-07
  • Java二叉搜索樹(shù)基礎(chǔ)原理與實(shí)現(xiàn)方法詳解

    Java二叉搜索樹(shù)基礎(chǔ)原理與實(shí)現(xiàn)方法詳解

    這篇文章主要介紹了Java二叉搜索樹(shù)基礎(chǔ)原理與實(shí)現(xiàn)方法,結(jié)合圖文與實(shí)例形式詳細(xì)分析了Java二叉搜索樹(shù)的基本概念、原理、實(shí)現(xiàn)方法與操作注意事項(xiàng),需要的朋友可以參考下
    2020-03-03
  • Java一些常見(jiàn)的出錯(cuò)異常處理方法總結(jié)

    Java一些常見(jiàn)的出錯(cuò)異常處理方法總結(jié)

    下面小編就為大家?guī)?lái)一篇Java一些常見(jiàn)的出錯(cuò)異常處理方法總結(jié)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2016-06-06

最新評(píng)論

鄢陵县| 陆丰市| 革吉县| 仁化县| 清河县| 奇台县| 靖州| 营山县| 会同县| 顺平县| 江安县| 禹州市| 屯门区| 浪卡子县| 聂荣县| 延吉市| 马公市| 法库县| 建宁县| 庆阳市| 四平市| 义马市| 中江县| 威信县| 南溪县| 仁怀市| 德阳市| 三穗县| 江油市| 武隆县| 麻栗坡县| 革吉县| 邻水| 桓仁| 自贡市| 姚安县| 镇康县| 托里县| 潞西市| 苍南县| 扎兰屯市|