minio分布式文件管理方式
一、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中讀取文件(二進(jìn)制,字符)內(nèi)容的幾種方法總結(jié)
本篇文章主要介紹了JAVA中讀取文件(二進(jìn)制,字符)內(nèi)容的方法總結(jié),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02
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
SpringBoot配置lombok與logback過(guò)程解析
這篇文章主要介紹了SpringBoot配置lombok與logback過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05
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é)
下面小編就為大家?guī)?lái)一篇Java一些常見(jiàn)的出錯(cuò)異常處理方法總結(jié)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-06-06

