常见的文件系统:FAT16/FAT32、NTFS、HFS、UFS、APFS、XFS、Ext4等 。
通过概念可以简单理解为:一个计算机无法存储海量的文件,通过网络将若干计算机组织起来共同去存储海量的文件,去接收海量用户的请求,这些组织起来的计算机通过网络进行通信,如下图:
好处:
1、一台计算机的文件系统处理能力扩充到多台计算机同时处理。
2、一台计算机挂了还有另外副本计算机提供数据。
3、每台计算机可以放在不同的地域,这样用户就可以就近访问,提高访问速度。
MinIO
介绍
MinIO 是一个非常轻量的服务,可以很简单的和其他应用的结合使用,它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等。
它一大特点就是轻量,使用简单,功能强大,支持各种平台,单个文件最大5TB,兼容 Amazon S3接口,提供了 Java、Python、GO等多版本SDK支持。
官网:https://min.io
中文:https://www.minio.org.cn/,http://docs.minio.org.cn/docs/
MinIO集群采用去中心化共享架构,每个结点是对等关系,通过Nginx可对MinIO进行负载均衡访问。
去中心化有什么好处?
在大数据领域,通常的设计理念都是无中心和分布式。Minio分布式模式可以帮助你搭建一个高可用的对象存储服务,你可以使用这些存储设备,而不用考虑其真实物理位置。
它将分布在不同服务器上的多块硬盘组成一个对象存储服务。由于硬盘分布在不同的节点上,分布式Minio避免了单点故障。如下图:
Minio使用纠删码技术来保护数据,它是一种恢复丢失和损坏数据的数学算法,它将数据分块冗余的分散存储在各各节点的磁盘上,所有的可用磁盘组成一个集合,上图由8块硬盘组成一个集合,当上传一个文件时会通过纠删码算法计算对文件进行分块存储,除了将文件本身分成4个数据块,还会生成4个校验块,数据块和校验块会分散的存储在这8块硬盘上。
使用纠删码的好处是即便丢失一半数量(N/2)的硬盘,仍然可以恢复数据。 比如上边集合中有4个以内的硬盘损害仍可保证数据恢复,不影响上传和下载,如果多于一半的硬盘坏了则无法恢复。
数据恢复演示
下边在本机演示MinIO恢复数据的过程,在本地创建4个目录表示4个硬盘。
首先下载MinIO,下载地址:https://dl.min.io/server/minio/release/
CMD进入有minio.exe的目录,运行下边的命令:
minio.exe server D:\develop\minio_data\data1 D:\develop\minio_data\data2 D:\develop\minio_data\data3 D:\develop\minio_data\data4
启动结果如下:
说明如下:
WARNING: MINIO_ACCESS_KEY and MINIO_SECRET_KEY are deprecated.
Please use MINIO_ROOT_USER and MINIO_ROOT_PASSWORD
Formatting 1st pool, 1 set(s), 4 drives per set.
WARNING: Host local has more than 2 drives of set. A host failure will result in data becoming unavailable.
WARNING: Detected default credentials 'minioadmin:minioadmin', we recommend that you change these values with 'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD' environment variables
1)老版本使用的MINIO_ACCESS_KEY 和 MINIO_SECRET_KEY不推荐使用,推荐使用MINIO_ROOT_USER 和MINIO_ROOT_PASSWORD设置账号和密码。
2)pool即minio节点组成的池子,当前有一个pool和4个硬盘组成的set集合
3)因为集合是4个硬盘,大于2的硬盘损坏数据将无法恢复。
4)账号和密码默认为minioadmin、minioadmin,可以在环境变量中设置通过'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD' 进行设置。
下边输入http://localhost:9000进行登录。
1)老版本使用的MINIO_ACCESS_KEY 和 MINIO_SECRET_KEY不推荐使用,推荐使用MINIO_ROOT_USER 和MINIO_ROOT_PASSWORD设置账号和密码。
2)pool即minio节点组成的池子,当前有一个pool和4个硬盘组成的set集合
3)因为集合是4个硬盘,大于2的硬盘损坏数据将无法恢复。
4)账号和密码默认为minioadmin、minioadmin,可以在环境变量中设置通过'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD' 进行设置。
下边输入http://localhost:9000进行登录。
登录成功:
下一步创建bucket,桶,它相当于存储文件的目录,可以创建若干的桶。
输入bucket的名称,点击“CreateBucket”,创建成功
点击“upload”上传文件。
下边上传几个文件
下边去四个目录观察文件的存储情况
我们发现上传的1.mp4文件存储在了四个目录,即四个硬盘上。
下边测试minio的数据恢复过程:
1、首先删除一个目录。
删除目录后仍然可以在web控制台上传文件和下载文件。
稍等片刻删除的目录自动恢复。
2、删除两个目录。
删除两个目录也会自动恢复。
3、删除三个目录 。
由于 集合中共有4块硬盘,有大于一半的硬盘损坏数据无法恢复。
此时报错:We encountered an internal error, please try again. (Read failed. Insufficient number of drives online)在线驱动器数量不足。
分布式集群测试
条件允许的情况下可以测试MinIO分布式存储的特性,首先准备环境。
分布式MinIO要求至少四个磁盘,建议至少4个节点,每个节点2个磁盘。
准备四台虚拟机:192.168.101.65、192.168.101.66、192.168.101.67、192.168.101.68
将课程资料下的minio的执行文件拷贝到四台虚拟机的/home/minio/目录下。
在四台虚拟机分别创建下边的脚本run.sh,内容如下:
#!/bin/bash
# 创建日志目录
mkdir -p /boot/mediafiles/logs
# 创建存储目录
mkdir -p /boot/mediafiles/data/d{1,2,3,4}
# 创建配置目录
mkdir -p /etc/minio
export MINIO_ROOT_USER=minioadmin
export MINIO_ROOT_PASSWORD=minioadmin# 在四台机器上都执行该文件,以分布式的方式启动minio
# --address 为api端口(如Java客户端)访问的端口
# --console-address web控制台端口
/home/minio/minio server \
http://192.168.101.65:9000/home/mediafiles/data/export1 \
http://192.168.101.65:9000/home/mediafiles/data/export2 \
http://192.168.101.66:9000/home/mediafiles/data/export1 \
http://192.168.101.66:9000/home/mediafiles/data/export2 \
http://192.168.101.67:9000/home/mediafiles/data/export1 \
http://192.168.101.67:9000/home/mediafiles/data/export2 \
http://192.168.101.68:9000/home/mediafiles/data/export1 \
http://192.168.101.68:9000/home/mediafiles/data/export2
在四台虚拟机执行脚本run.sh,注意观察日志。
启动成功后访问: http://192.168.101.66:9001/、http://192.168.101.67:9001/、http://192.168.101.68:9001/、http://192.168.101.69:9001/。
访问任意一个都可以操作 minio集群。
下边进行测试:
1、向集群上传一个文件,观察每个节点的两个磁盘目录都存储了数据。
2、停止 一个节点,不影响上传和下载。
假如停止了65节点,通过其它节点上传文件,稍后启动65后自动从其它结点同步文件。
3、停止 两个节点,无法上传,可以下载。
此时上传文件客户端报错如下:
上传文件需要至少一半加1个可用的磁盘。
将停止的两个节点的minio启动,稍等片刻 minio恢复可用。
测试Docker环境
开发阶段和生产阶段统一使用Docker下的MINIO。
在下发的虚拟机中已安装了MinIO的镜像和容器,执行sh /data/soft /restart.sh启动Docker下的MinIO
启动完成登录MinIO查看是否正常。
访问http://192.168.101.65:9000
本项目创建两个buckets:
mediafiles: 普通文件
video:视频文件
SDK
上传文件
MinIO提供多个语言版本SDK的支持,下边找到java版本的文档:
地址:https://docs.min.io/docs/java-client-quickstart-guide.html
最低需求Java 1.8或更高版本:
maven依赖如下:
<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>
参数说明:
需要三个参数才能连接到minio服务
参数 | 说明 |
Endpoint | 对象存储服务的URL |
Access Key | Access key就像用户ID,可以唯一标识你的账户。 |
Secret Key | Secret key是你账户的密码。 |
示例代码如下:
import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.UploadObjectArgs;
import io.minio.errors.MinioException;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class FileUploader {public static void main(String[] args)throws IOException, NoSuchAlgorithmException, InvalidKeyException {try {// Create a minioClient with the MinIO server playground, its access key and secret key.MinioClient minioClient =MinioClient.builder().endpoint("https://play.min.io").credentials("Q3AM3UQ867SPQQA43P2F", "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG").build();// Make 'asiatrip' bucket if not exist.boolean found =minioClient.bucketExists(BucketExistsArgs.builder().bucket("asiatrip").build());if (!found) {// Make a new bucket called 'asiatrip'.minioClient.makeBucket(MakeBucketArgs.builder().bucket("asiatrip").build());} else {System.out.println("Bucket 'asiatrip' already exists.");}// Upload '/home/user/Photos/asiaphotos.zip' as object name 'asiaphotos-2015.zip' to bucket// 'asiatrip'.minioClient.uploadObject(UploadObjectArgs.builder().bucket("asiatrip").object("asiaphotos-2015.zip").filename("/home/user/Photos/asiaphotos.zip").build());System.out.println("'/home/user/Photos/asiaphotos.zip' is successfully uploaded as "+ "object 'asiaphotos-2015.zip' to bucket 'asiatrip'.");} catch (MinioException e) {System.out.println("Error occurred: " + e);System.out.println("HTTP trace: " + e.httpTrace());}}
}
测试上传文件功能,
首先创建一个用于测试的bucket
点击“Manage”修改bucket的访问权限
选择public权限
测试代码如下:
package com.xuecheng.media;import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.UploadObjectArgs;
import io.minio.errors.MinioException;import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;/*** @description 测试MinIO* @author Mr.M* @date 2022/9/11 21:24* @version 1.0*/
public class MinIOTest {static MinioClient minioClient =MinioClient.builder().endpoint("http://192.168.101.65:9000").credentials("minioadmin", "minioadmin").build();//上传文件
public static void upload()throws IOException, NoSuchAlgorithmException, InvalidKeyException {try {boolean found =minioClient.bucketExists(BucketExistsArgs.builder().bucket("testbucket").build());//检查testbucket桶是否创建,没有创建自动创建if (!found) {minioClient.makeBucket(MakeBucketArgs.builder().bucket("testbucket").build());} else {System.out.println("Bucket 'testbucket' already exists.");}//上传1.mp4minioClient.uploadObject(UploadObjectArgs.builder().bucket("testbucket").object("1.mp4").filename("D:\\develop\\upload\\1.mp4").build());//上传1.avi,上传到avi子目录minioClient.uploadObject(UploadObjectArgs.builder().bucket("testbucket").object("avi/1.avi").filename("D:\\develop\\upload\\1.avi").build());System.out.println("上传成功");} catch (MinioException e) {System.out.println("Error occurred: " + e);System.out.println("HTTP trace: " + e.httpTrace());}}
public static void main(String[] args)throws IOException, NoSuchAlgorithmException, InvalidKeyException {upload();
}}
执行main方法,共上传两个文件,1.mp4上传到桶根目录下,1.avi上传到 桶中的avi目录下,avi目录会自动创建。
上传成功,通过web控制台查看文件,并预览文件。
删除文件
下边测试删除文件
参考:https://docs.min.io/docs/java-client-api-reference#removeObject
//删除文件
public static void delete(String bucket,String filepath)throws IOException, NoSuchAlgorithmException, InvalidKeyException {try {minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(filepath).build());System.out.println("删除成功");} catch (MinioException e) {System.out.println("Error occurred: " + e);System.out.println("HTTP trace: " + e.httpTrace());}}public static void main(String[] args)throws IOException, NoSuchAlgorithmException, InvalidKeyException {
// upload();delete("testbucket","1.mp4");delete("testbucket","avi/1.avi");}
查询文件
通过查询文件查看文件是否存在minio中。
参考:https://docs.min.io/docs/java-client-api-reference#getObject
//下载文件public static void getFile(String bucket,String filepath,String outFile)throws IOException, NoSuchAlgorithmException, InvalidKeyException {try {try (InputStream stream = minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(filepath).build());FileOutputStream fileOutputStream = new FileOutputStream(new File(outFile));) {// Read data from streamIOUtils.copy(stream,fileOutputStream);System.out.println("下载成功");}} catch (MinioException e) {System.out.println("Error occurred: " + e);System.out.println("HTTP trace: " + e.httpTrace());}}public static void main(String[] args)throws IOException, NoSuchAlgorithmException, InvalidKeyException {upload();
// delete("testbucket","1.mp4");
// delete("testbucket","avi/1.avi");getFile("testbucket","avi/1.avi","D:\\develop\\minio_data\\1.avi");}
上传图片
业务流程
图片上传至分布式文件系统,在课程信息中保存课程图片路径,如下流程:
1、前端进入上传图片界面
2、上传图片,请求媒资管理服务。
3、媒资管理服务将图片文件存储在MinIO。
4、媒资管理记录文件信息到数据库。
5、保存课程信息,在内容管理数据库保存图片地址。
媒资管理服务由接口层和业务层共同完成,具体分工如下:
用户上传图片请求至媒资管理的接口层,接口层解析文件信息通过业务层将文件保存至minio及数据库。如下图:
数据模型
涉及到的数据表有:课程信息表中的图片字段、媒资数据库的文件表,下边主要看媒资数据库的文件表。
各字段描述如下:
准备环境
首先在minio配置bucket,bucket名称为:mediafiles,并设置bucket的权限为公开。
在nacos配置中minio的相关信息,进入media-service-dev.yaml:
配置信息如下:
minio:endpoint: http://192.168.101.65:9000accessKey: minioadminsecretKey: minioadminbucket:files: mediafilesvideofiles: video
编写minio的配置类
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配置* @author Mr.M* @date 2022/9/12 19:32* @version 1.0*/@Configuration
public class MinioConfig {@Value("${minio.endpoint}")private String endpoint;@Value("${minio.accessKey}")private String accessKey;@Value("${minio.secretKey}")private String secretKey;@Beanpublic MinioClient minioClient() {MinioClient minioClient =MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();return minioClient;}
}
接口定义
根据需求分析,下边进行接口定义,此接口定义为一个通用的上传文件接口,可以上传图片或其它文件。
首先分析接口:
请求地址:/media/upload/coursefile
请求参数:
Content-Type: multipart/form-data;boundary=.....
FormData: filedata=??
响应参数:文件信息,如下
{"id": "a16da7a132559daf9e1193166b3e7f52","companyId": 1232141425,"companyName": null,"filename": "1.jpg","fileType": "001001","tags": "","bucket": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg","fileId": "a16da7a132559daf9e1193166b3e7f52","url": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg","timelength": null,"username": null,"createDate": "2022-09-12T21:57:18","changeDate": null,"status": "1","remark": "","auditStatus": null,"auditMind": null,"fileSize": 248329
}
接口开发
定义上传响应模型类
package com.xuecheng.media.model.dto;import com.xuecheng.media.model.po.MediaFiles;
import lombok.Data;
import lombok.ToString;/*** @description 上传普通文件成功响应结果* @author Mr.M* @date 2022/9/12 18:49* @version 1.0*/@Data
public class UploadFileResultDto extends MediaFiles {}
DAO开发
根据需求分析DAO层实现向media_files表插入一条记录,使用media_files表生成的mapper即可。
domain
package com.xuecheng.media.model.dto;import com.xuecheng.media.model.po.MediaFiles;
import lombok.Data;
import lombok.ToString;/*** @description 上传普通文件请求参数* @author Mr.M* @date 2022/9/12 18:49* @version 1.0*/@Data
public class UploadFileParamsDto {/*** 文件名称*/private String filename;/*** 文件content-type*/private String contentType;/*** 文件类型(文档,音频,视频)*/private String fileType;/*** 文件大小*/private Long fileSize;/*** 标签*/private String tags;/*** 上传人*/private String username;/*** 备注*/private String remark;}
Service开发
Service方法需要提供一个更加通用的保存文件的方法。
定义service方法
mediaFileService
/*** @description 上传文件* @param uploadFileParamsDto 上传文件信息* @param folder 文件目录,如果不传则默认年、月、日* @return com.xuecheng.media.model.dto.UploadFileResultDto 上传文件结果* @author Mr.M* @date 2022/9/12 19:31
*/
public UploadFileResultDto uploadFile(Long companyId,UploadFileParamsDto uploadFileParamsDto,byte[] bytes,String folder,String objectName);
实现方法如下:
Java@Autowired
MinioClient minioClient;@Autowired
MediaFilesMapper mediaFilesMapper;//普通文件桶
@Value("${minio.bucket.files}")
private String bucket_Files;@Transactional
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {//生成文件id,文件的md5值String fileId = DigestUtils.md5Hex(bytes);//文件名称String filename = uploadFileParamsDto.getFilename();//构造objectnameif (StringUtils.isEmpty(objectName)) {objectName = fileId + filename.substring(filename.lastIndexOf("."));}if (StringUtils.isEmpty(folder)) {//通过日期构造文件存储路径folder = getFileFolder(new Date(), true, true, true);} else if (folder.indexOf("/") < 0) {folder = folder + "/";}//对象名称objectName = folder + objectName;MediaFiles mediaFiles = null;try {//转为流ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(bucket_Files).object(objectName)//-1表示文件分片按5M(不小于5M,不大于5T),分片数量最大10000,.stream(byteArrayInputStream, byteArrayInputStream.available(), -1).contentType(uploadFileParamsDto.getContentType()).build();minioClient.putObject(putObjectArgs);//从数据库查询文件mediaFiles = mediaFilesMapper.selectById(fileId);if (mediaFiles == null) {mediaFiles = new MediaFiles();//拷贝基本信息BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);mediaFiles.setFileId(fileId);mediaFiles.setCompanyId(companyId);mediaFiles.setUrl("/" + bucket_Files + "/" + objectName);mediaFiles.setBucket(bucket_Files);mediaFiles.setCreateDate(LocalDateTime.now());mediaFiles.setStatus("1");//保存文件信息到文件表int insert = mediaFilesMapper.insert(mediaFiles);if (insert < 0) {XueChengPlusException.cast("保存文件信息失败");}UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);return uploadFileResultDto;}} catch (Exception e) {e.printStackTrace();XueChengPlusException.cast("上传过程中出错");}return null;
}//根据日期拼接目录
private String getFileFolder(Date date, boolean year, boolean month, boolean day){SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");//获取当前日期字符串String dateString = sdf.format(new Date());//取出年、月、日String[] dateStringArray = dateString.split("-");StringBuffer folderString = new StringBuffer();if(year){folderString.append(dateStringArray[0]);folderString.append("/");}if(month){folderString.append(dateStringArray[1]);folderString.append("/");}if(day){folderString.append(dateStringArray[2]);folderString.append("/");}return folderString.toString();
}
controller
@ApiOperation("上传文件")
@RequestMapping(value = "/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseBody
public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile upload,@RequestParam(value = "folder",required=false) String folder,@RequestParam(value = "objectName",required=false) String objectName) throws IOException {String contentType = upload.getContentType();Long companyId = 1232141425L;UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();uploadFileParamsDto.setFileSize(upload.getSize());if(contentType.indexOf("image")>=0){//图片uploadFileParamsDto.setFileType("001001");}else{//其它uploadFileParamsDto.setFileType("001003");}uploadFileParamsDto.setRemark("");uploadFileParamsDto.setFilename(upload.getOriginalFilename());uploadFileParamsDto.setContentType(contentType);return mediaFileService.uploadFile(companyId,uploadFileParamsDto,upload.getBytes(),folder,objectName);
}
使用httpclient测试
### 上传文件
POST {{media_host}}/media/upload/coursefile
Content-Type: multipart/form-data; boundary=WebAppBoundary--WebAppBoundary
Content-Disposition: form-data; name="filedata"; filename="1.jpg"
Content-Type: application/octet-stream< d:/develop/upload/1.jpg
Service代码优化
在上传文件的方法中包括两部分:向MinIO存储文件,向数据库存储文件信息,下边将这两部分抽取出来,后期可供其它Service方法调用。
@Transactional
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {//生成文件id,文件的md5值String fileId = DigestUtils.md5Hex(bytes);//文件名称String filename = uploadFileParamsDto.getFilename();//构造objectnameif (StringUtils.isEmpty(objectName)) {objectName = fileId + filename.substring(filename.lastIndexOf("."));}if (StringUtils.isEmpty(folder)) {//通过日期构造文件存储路径folder = getFileFolder(new Date(), true, true, true);} else if (folder.indexOf("/") < 0) {folder = folder + "/";}//对象名称objectName = folder + objectName;MediaFiles mediaFiles = null;try {//上传至文件系统addMediaFilesToMinIO(bytes,bucket_Files,objectName,uploadFileParamsDto.getContentType());//写入文件表mediaFiles = addMediaFilesToDb(companyId,fileId,uploadFileParamsDto,bucket_Files,objectName);UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);return uploadFileResultDto;} catch (Exception e) {e.printStackTrace();XueChengPlusException.cast("上传过程中出错");}return null;}/*** @description 将文件写入minIO* @param bytes 文件字节数组* @param bucket 桶* @param objectName 对象名称* @param contentType 内容类型* @return void* @author Mr.M* @date 2022/10/12 21:22
*/
public void addMediaFilesToMinIO(byte[] bytes, String bucket, String objectName, String contentType) {//转为流ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);try {PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(bucket).object(objectName)//-1表示文件分片按5M(不小于5M,不大于5T),分片数量最大10000,.stream(byteArrayInputStream, byteArrayInputStream.available(), -1).contentType(contentType).build();minioClient.putObject(putObjectArgs);} catch (Exception e) {e.printStackTrace();XueChengPlusException.cast("上传文件到文件系统出错");}
}/*** @description 将文件信息添加到文件表* @param companyId 机构id* @param fileMd5 文件md5值* @param uploadFileParamsDto 上传文件的信息* @param bucket 桶* @param objectName 对象名称* @return com.xuecheng.media.model.po.MediaFiles* @author Mr.M* @date 2022/10/12 21:22
*/
public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){//从数据库查询文件MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);if (mediaFiles == null) {mediaFiles = new MediaFiles();//拷贝基本信息BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);mediaFiles.setId(fileMd5);mediaFiles.setFileId(fileMd5);mediaFiles.setCompanyId(companyId);mediaFiles.setUrl("/" + bucket + "/" + objectName);mediaFiles.setBucket(bucket);mediaFiles.setCreateDate(LocalDateTime.now());mediaFiles.setAuditStatus("002003");mediaFiles.setStatus("1");//保存文件信息到文件表int insert = mediaFilesMapper.insert(mediaFiles);if (insert < 0) {XueChengPlusException.cast("保存文件信息失败");}}return mediaFiles;}
优化后进行测试。
Service事务优化
上边的service方法优化后并测试通过,现在思考关于uploadFile方法的是否应该开启事务。
目前是在uploadFile方法上添加@Transactional,当调用uploadFile方法前会开启数据库事务,如果上传文件过程时间较长那么数据库的事务持续时间就会变长,这样数据库链接释放就慢,最终导致数据库链接不够用。
我们只将addMediaFilesToDb方法添加事务控制即可,uploadFile方法上的@Transactional注解去掉。
优化后如下:
@Transactional
public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){//从数据库查询文件MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);if (mediaFiles == null) {mediaFiles = new MediaFiles();//拷贝基本信息BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);mediaFiles.setId(fileMd5);mediaFiles.setFileId(fileMd5);mediaFiles.setCompanyId(companyId);mediaFiles.setUrl("/" + bucket + "/" + objectName);mediaFiles.setBucket(bucket);mediaFiles.setCreateDate(LocalDateTime.now());mediaFiles.setAuditStatus("002003");mediaFiles.setStatus("1");//保存文件信息到文件表int insert = mediaFilesMapper.insert(mediaFiles);if (insert < 0) {XueChengPlusException.cast("保存文件信息失败");}}return mediaFiles;}
我们人为在int insert = mediaFilesMapper.insert(mediaFiles);下边添加一个异常代码int a=1/0;
测试是否事务控制。
很遗憾,事务控制失败。方法上已经添加了@Transactional注解为什么该方法不能被事务控制呢?
如果是在uploadFile方法上添加@Transactional注解就可以控制事务,去掉则不行。
现在的问题其实是一个非事务方法调同类一个事务方法,事务无法控制,这是为什么?
下边分析原因:
如果在uploadFile方法上添加@Transactional注解,代理对象执行此方法前会开启事务,如下图:
如果在uploadFile方法上没有@Transactional注解,代理对象执行此方法前不进行事务控制,如下图:
现在在addMediaFilesToDb方法上添加@Transactional注解,也不会进行事务是因为并不是通过代理对象执行的addMediaFilesToDb方法。为了判断在uploadFile方法中去调用addMediaFilesToDb方法是否是通过代理对象去调用,我们可以打断点跟踪。
我们发现在uploadFile方法中去调用addMediaFilesToDb方法不是通过代理对象去调用。
如何解决呢?通过代理对象去调用addMediaFilesToDb方法即可解决。
在MediaFileService的实现类中注入MediaFileService的代理对象,如下:
@Autowired
MediaFileService currentProxy;
将addMediaFilesToDb方法提成接口。
/*** @description 将文件信息添加到文件表* @param companyId 机构id* @param fileMd5 文件md5值* @param uploadFileParamsDto 上传文件的信息* @param bucket 桶* @param objectName 对象名称* @return com.xuecheng.media.model.po.MediaFiles* @author Mr.M* @date 2022/10/12 21:22*/
@Transactional
public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName);
调用addMediaFilesToDb方法的代码处改为如下:
try {
.....//写入文件表mediaFiles = currentProxy.addMediaFilesToDb(companyId,fileId,uploadFileParamsDto,bucket_Files,objectName);....
service断点续传+addMediaFilesToDb方法提成接口。
package com.xuecheng.media.service;import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestBody;import java.util.List;/*** @description 媒资文件管理业务类* @author Mr.M* @date 2022/9/10 8:55* @version 1.0*/
public interface MediaFileService {/*** @description 媒资文件查询方法* @param pageParams 分页参数* @param queryMediaParamsDto 查询条件* @return com.xuecheng.base.model.PageResult<com.xuecheng.media.model.po.MediaFiles>* @author Mr.M* @date 2022/9/10 8:57*/public PageResult<MediaFiles> queryMediaFiels(Long companyId,PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto);/*** @description 上传文件的通用接口* @param companyId 机构id* @param uploadFileParamsDto 文件信息* @param bytes 文件字节数组* @param folder 桶下边的子目录* @param objectName 对象名称* @return com.xuecheng.media.model.dto.UploadFileResultDto* @author Mr.M* @date 2022/10/13 15:51*/public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes,String folder,String objectName);/*** @param companyId* @param fileId* @param uploadFileParamsDto* @param bucket* @param objectName* @return com.xuecheng.media.model.po.MediaFiles* @description 将文件信息入库* @author Mr.M* @date 2022/10/14 9:14*/@Transactionalpublic MediaFiles addMediaFilesToDb(Long companyId, String fileId, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName);/*** @description 检查文件是否存在* @param fileMd5 文件的md5* @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在,true存在* @author Mr.M* @date 2022/9/13 15:38*/public RestResponse<Boolean> checkFile(String fileMd5);/*** @description 检查分块是否存在* @param fileMd5 文件的md5* @param chunkIndex 分块序号* @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在,true存在* @author Mr.M* @date 2022/9/13 15:39*/public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex);/*** @description 上传分块* @param fileMd5 文件md5* @param chunk 分块序号* @param bytes 文件字节* @return com.xuecheng.base.model.RestResponse* @author Mr.M* @date 2022/9/13 15:50*/public RestResponse uploadChunk(String fileMd5,int chunk,byte[] bytes);/*** @description 合并分块* @param companyId 机构id* @param fileMd5 文件md5* @param chunkTotal 分块总和* @param uploadFileParamsDto 文件信息* @return com.xuecheng.base.model.RestResponse* @author Mr.M* @date 2022/9/13 15:56*/public RestResponse mergechunks(Long companyId,String fileMd5,int chunkTotal,UploadFileParamsDto uploadFileParamsDto);
}
serviceimpl
package com.xuecheng.media.service.impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.service.MediaFileService;
import io.minio.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.io.*;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;/*** @author Mr.M* @version 1.0* @description TODO* @date 2022/9/10 8:58*/
@Slf4j
@Service
public class MediaFileServiceImpl implements MediaFileService {@AutowiredMediaFilesMapper mediaFilesMapper;@AutowiredMinioClient minioClient;//普通文件存储的桶@Value("${minio.bucket.files}")private String bucket_files;//视频文件存储的桶@Value("${minio.bucket.videofiles}")private String bucket_videofiles;@AutowiredMediaFileService currentProxy;@Overridepublic PageResult<MediaFiles> queryMediaFiels(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {//构建查询条件对象LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();//分页对象Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());// 查询数据内容获得结果Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);// 获取数据列表List<MediaFiles> list = pageResult.getRecords();// 获取数据总数long total = pageResult.getTotal();// 构建结果集PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());return mediaListResult;}@Overridepublic UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {//得到文件的md5值String fileMd5 = DigestUtils.md5Hex(bytes);if (StringUtils.isEmpty(folder)) {//自动生成目录的路径 按年月日生成,folder = getFileFolder(new Date(), true, true, true);} else if (folder.indexOf("/") < 0) {folder = folder + "/";}//文件名称String filename = uploadFileParamsDto.getFilename();if (StringUtils.isEmpty(objectName)) {//如果objectName为空,使用文件的md5值为objectNameobjectName = fileMd5 + filename.substring(filename.lastIndexOf("."));}objectName = folder + objectName;try {addMediaFilesToMinIO(bytes, bucket_files, objectName);MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_files, objectName);//准备返回数据UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);return uploadFileResultDto;} catch (Exception e) {log.debug("上传文件失败:{}", e.getMessage());throw new RuntimeException(e.getMessage());}// return null;}// @Override
// public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
//
//
// //得到文件的md5值
// String fileMd5 = DigestUtils.md5Hex(bytes);
//
// if(StringUtils.isEmpty(folder)){
// //自动生成目录的路径 按年月日生成,
// folder = getFileFolder(new Date(), true, true, true);
// }else if(folder.indexOf("/")<0){
// folder = folder+"/";
// }
// //文件名称
// String filename = uploadFileParamsDto.getFilename();
//
// if(StringUtils.isEmpty(objectName)){
// //如果objectName为空,使用文件的md5值为objectName
// objectName = fileMd5 + filename.substring(filename.lastIndexOf("."));
// }
//
// objectName = folder + objectName;
//
// try {
// ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
// String contentType = uploadFileParamsDto.getContentType();
//
// PutObjectArgs putObjectArgs = PutObjectArgs.builder()
// .bucket(bucket_files)
// .object(objectName)
// //InputStream stream, long objectSize 对象大小, long partSize 分片大小(-1表示5M,最大不要超过5T,最多10000)
// .stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
// .contentType(contentType)
// .build();
// //上传到minio
// minioClient.putObject(putObjectArgs);
//
// //保存到数据库
// MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
// if(mediaFiles == null){
// mediaFiles = new MediaFiles();
//
// //封装数据
// BeanUtils.copyProperties(uploadFileParamsDto,mediaFiles);
// mediaFiles.setId(fileMd5);
// mediaFiles.setFileId(fileMd5);
// mediaFiles.setCompanyId(companyId);
// mediaFiles.setFilename(filename);
// mediaFiles.setBucket(bucket_files);
// mediaFiles.setFilePath(objectName);
// mediaFiles.setUrl("/"+bucket_files+"/"+objectName);
// mediaFiles.setCreateDate(LocalDateTime.now());
// mediaFiles.setStatus("1");
// mediaFiles.setAuditStatus("002003");
//
// //插入文件表
// mediaFilesMapper.insert(mediaFiles);
//
// }
//
// //准备返回数据
// UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
// BeanUtils.copyProperties(mediaFiles,uploadFileResultDto);
// return uploadFileResultDto;
//
//
// } catch (Exception e) {
// log.debug("上传文件失败:{}",e.getMessage());
// }
//
// return null;
// }/*** @param companyId* @param fileId* @param uploadFileParamsDto* @param bucket* @param objectName* @return com.xuecheng.media.model.po.MediaFiles* @description 将文件信息入库* @author Mr.M* @date 2022/10/14 9:14*/@Transactionalpublic MediaFiles addMediaFilesToDb(Long companyId, String fileId, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName) {//保存到数据库MediaFiles mediaFiles = mediaFilesMapper.selectById(fileId);if (mediaFiles == null) {mediaFiles = new MediaFiles();//封装数据BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);mediaFiles.setId(fileId);mediaFiles.setFileId(fileId);mediaFiles.setCompanyId(companyId);mediaFiles.setBucket(bucket);mediaFiles.setFilePath(objectName);mediaFiles.setUrl("/" + bucket + "/" + objectName);mediaFiles.setCreateDate(LocalDateTime.now());mediaFiles.setStatus("1");mediaFiles.setAuditStatus("002003");//插入文件表mediaFilesMapper.insert(mediaFiles);//抛出异常,制造异常
// int i=1/0;}return mediaFiles;}@Overridepublic RestResponse<Boolean> checkFile(String fileMd5) {//在文件表存在,并且在文件系统存在,此文件才存在MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);if(mediaFiles==null){return RestResponse.success(false);}//查看是否在文件系统存在GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(mediaFiles.getBucket()).object(mediaFiles.getFilePath()).build();try {InputStream inputStream = minioClient.getObject(getObjectArgs);if(inputStream==null){//文件不存在return RestResponse.success(false);}}catch (Exception e){//文件不存在return RestResponse.success(false);}//文件已存在return RestResponse.success(true);}@Overridepublic RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {//得到分块文件所在目录String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);//分块文件的路径String chunkFilePath = chunkFileFolderPath + chunkIndex;//查询文件系统分块文件是否存在//查看是否在文件系统存在GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket_videofiles).object(chunkFilePath).build();try {InputStream inputStream = minioClient.getObject(getObjectArgs);if(inputStream==null){//文件不存在return RestResponse.success(false);}}catch (Exception e){//文件不存在return RestResponse.success(false);}return RestResponse.success(true);}@Overridepublic RestResponse uploadChunk(String fileMd5, int chunk, byte[] bytes) {//得到分块文件所在目录String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);//分块文件的路径String chunkFilePath = chunkFileFolderPath + chunk;try {//将分块上传到文件系统addMediaFilesToMinIO(bytes, bucket_videofiles, chunkFilePath);//上传成功return RestResponse.success(true);} catch (Exception e) {log.debug("上传分块文件失败:{}", e.getMessage());return RestResponse.validfail(false,"上传分块失败");}}//合并分块@Overridepublic RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {//下载分块File[] chunkFiles = checkChunkStatus(fileMd5, chunkTotal);//得到合并后文件的扩展名String filename = uploadFileParamsDto.getFilename();//扩展名String extension = filename.substring(filename.lastIndexOf("."));File tempMergeFile = null;try {try {//创建一个临时文件作为合并文件tempMergeFile = File.createTempFile("'merge'", extension);} catch (IOException e) {XueChengPlusException.cast("创建临时合并文件出错");}//创建合并文件的流对象try( RandomAccessFile raf_write =new RandomAccessFile(tempMergeFile, "rw")) {byte[] b = new byte[1024];for (File file : chunkFiles) {//读取分块文件的流对象try(RandomAccessFile raf_read = new RandomAccessFile(file, "r");) {int len = -1;while ((len = raf_read.read(b)) != -1) {//向合并文件写数据raf_write.write(b, 0, len);}}}} catch (IOException e) {XueChengPlusException.cast("合并文件过程出错");}//校验合并后的文件是否正确try {FileInputStream mergeFileStream = new FileInputStream(tempMergeFile);String mergeMd5Hex = DigestUtils.md5Hex(mergeFileStream);if (!fileMd5.equals(mergeMd5Hex)) {log.debug("合并文件校验不通过,文件路径:{},原始文件md5:{}", tempMergeFile.getAbsolutePath(), fileMd5);XueChengPlusException.cast("合并文件校验不通过");}} catch (IOException e) {log.debug("合并文件校验出错,文件路径:{},原始文件md5:{}", tempMergeFile.getAbsolutePath(), fileMd5);XueChengPlusException.cast("合并文件校验出错");}//拿到合并文件在minio的存储路径String mergeFilePath = getFilePathByMd5(fileMd5, extension);//将合并后的文件上传到文件系统addMediaFilesToMinIO(tempMergeFile.getAbsolutePath(), bucket_videofiles, mergeFilePath);//将文件信息入库保存uploadFileParamsDto.setFileSize(tempMergeFile.length());//合并文件的大小addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_videofiles, mergeFilePath);return RestResponse.success(true);}finally {//删除临时分块文件if(chunkFiles!=null){for (File chunkFile : chunkFiles) {if(chunkFile.exists()){chunkFile.delete();}}}//删除合并的临时文件if(tempMergeFile!=null){tempMergeFile.delete();}}}private String getFilePathByMd5(String fileMd5,String fileExt){return fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" +fileMd5 +fileExt;}/*** @description 下载分块* @param fileMd5* @param chunkTotal 分块数量* @return java.io.File[] 分块文件数组* @author Mr.M* @date 2022/10/14 15:07*/private File[] checkChunkStatus(String fileMd5,int chunkTotal ){//得到分块文件所在目录String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);//分块文件数组File[] chunkFiles = new File[chunkTotal];//开始下载for (int i = 0; i < chunkTotal; i++) {//分块文件的路径String chunkFilePath = chunkFileFolderPath + i;//分块文件File chunkFile = null;try {chunkFile = File.createTempFile("chunk", null);} catch (IOException e) {e.printStackTrace();XueChengPlusException.cast("创建分块临时文件出错"+e.getMessage());}//下载分块文件downloadFileFromMinIO(chunkFile, bucket_videofiles, chunkFilePath);chunkFiles[i] = chunkFile;}return chunkFiles;}//根据桶和文件路径从minio下载文件public File downloadFileFromMinIO(File file,String bucket,String objectName){GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket).object(objectName).build();try(InputStream inputStream = minioClient.getObject(getObjectArgs);FileOutputStream outputStream =new FileOutputStream(file);) {IOUtils.copy(inputStream,outputStream);return file;}catch (Exception e){e.printStackTrace();XueChengPlusException.cast("查询分块文件出错");}return null;}//得到分块文件的目录private String getChunkFileFolderPath(String fileMd5) {return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";}//将文件上传到文件系统private void addMediaFilesToMinIO(String filePath, String bucket, String objectName){try {UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder().bucket(bucket).object(objectName).filename(filePath).build();//上传minioClient.uploadObject(uploadObjectArgs);log.debug("文件上传成功:{}",filePath);} catch (Exception e) {XueChengPlusException.cast("文件上传到文件系统失败");}}//将文件上传到分布式文件系统private void addMediaFilesToMinIO(byte[] bytes, String bucket, String objectName) {//资源的媒体类型String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//默认未知二进制流if (objectName.indexOf(".") >= 0) {//取objectName中的扩展名String extension = objectName.substring(objectName.lastIndexOf("."));ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);if (extensionMatch != null) {contentType = extensionMatch.getMimeType();}}try {ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(bucket).object(objectName)//InputStream stream, long objectSize 对象大小, long partSize 分片大小(-1表示5M,最大不要超过5T,最多10000).stream(byteArrayInputStream, byteArrayInputStream.available(), -1).contentType(contentType).build();//上传到miniominioClient.putObject(putObjectArgs);} catch (Exception e) {e.printStackTrace();log.debug("上传文件到文件系统出错:{}", e.getMessage());XueChengPlusException.cast("上传文件到文件系统出错");}}//根据日期拼接目录private String getFileFolder(Date date, boolean year, boolean month, boolean day) {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");//获取当前日期字符串String dateString = sdf.format(new Date());//取出年、月、日String[] dateStringArray = dateString.split("-");StringBuffer folderString = new StringBuffer();if (year) {folderString.append(dateStringArray[0]);folderString.append("/");}if (month) {folderString.append(dateStringArray[1]);folderString.append("/");}if (day) {folderString.append(dateStringArray[2]);folderString.append("/");}return folderString.toString();}public static void main(String[] args) {String extension = ".jpg";ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);if (extensionMatch != null) {System.out.println(extensionMatch.getMimeType());}}
}
上传视频
理解断点续传
什么是断点续传
通常视频文件都比较大,所以对于媒资系统上传文件的需求要满足大文件的上传要求。http协议本身对上传文件大小没有限制,但是客户的网络环境质量、电脑硬件环境等参差不齐,如果一个大文件快上传完了网断了没有上传完成,需要客户重新上传,用户体验非常差,所以对于大文件上传的要求最基本的是断点续传。
什么是断点续传:
引用百度百科:断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性。
断点续传流程如下图:
流程如下:
1、前端上传前先把文件分成块
2、一块一块的上传,上传中断后重新上传,已上传的分块则不用再上传
3、各分块上传完成最后在服务端合并文件
分块与合并测试
为了更好的理解文件分块上传的原理,下边用java代码测试文件的分块与合并。
文件分块的流程如下:
1、获取源文件长度
2、根据设定的分块文件的大小计算出块数
3、从源文件读数据依次向每一个块文件写数据。
测试代码如下:
package com.xuecheng.media;import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;/*** @author Mr.M* @version 1.0* @description 大文件处理测试* @date 2022/9/13 9:21*/
public class BigFileTest {//测试文件分块方法@Testpublic void testChunk() throws IOException {//源文件File sourceFile = new File("d:/develop/bigfile_test/nacos.avi");//分块文件存储路径String chunkPath = "d:/develop/bigfile_test/chunk/";File chunkFolder = new File(chunkPath);if (!chunkFolder.exists()) {chunkFolder.mkdirs();}//分块大小long chunkSize = 1024 * 1024 * 1;//分块数量long chunkNum = (long) Math.ceil(sourceFile.length() * 1.0 / chunkSize);System.out.println("分块总数:"+chunkNum);//缓冲区大小byte[] b = new byte[1024];//使用RandomAccessFile访问文件RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r");//分块,for (int i = 0; i < chunkNum; i++) {//创建分块文件File file = new File(chunkPath + i);if(file.exists()){file.delete();}boolean newFile = file.createNewFile();if (newFile) {//向分块文件中写数据RandomAccessFile raf_write = new RandomAccessFile(file, "rw");int len = -1;while ((len = raf_read.read(b)) != -1) {raf_write.write(b, 0, len);//达到分块的大小就不在写if (file.length() > chunkSize) {break;}}raf_write.close();System.out.println("完成分块"+i);}}raf_read.close();}}
文件合并流程:
1、找到要合并的文件并按文件合并的先后进行排序。
2、创建合并文件
3、依次从合并的文件中读取数据向合并文件写入数
文件合并的测试代码 :
//测试文件合并方法@Testpublic void testMerge() throws IOException {//块文件目录File chunkFolder = new File("d:/develop/bigfile_test/chunk/");//原始文件File originalFile = new File("d:/develop/bigfile_test/nacos.avi");//合并文件File mergeFile = new File("d:/develop/bigfile_test/nacos01.avi");if (mergeFile.exists()) {mergeFile.delete();}//创建新的合并文件mergeFile.createNewFile();//用于写文件RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");//指针指向文件顶端raf_write.seek(0);//缓冲区byte[] b = new byte[1024];//分块列表File[] fileArray = chunkFolder.listFiles();// 转成集合,便于排序List<File> fileList = new ArrayList<File>(Arrays.asList(fileArray));// 从小到大排序Collections.sort(fileList, new Comparator<File>() {@Overridepublic int compare(File o1, File o2) {return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName());}});//合并文件for (File chunkFile : fileList) {RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "rw");int len = -1;while ((len = raf_read.read(b)) != -1) {raf_write.write(b, 0, len);}raf_read.close();}raf_write.close();//校验文件try (FileInputStream fileInputStream = new FileInputStream(originalFile);FileInputStream mergeFileStream = new FileInputStream(mergeFile);) {//取出原始文件的md5String originalMd5 = DigestUtils.md5Hex(fileInputStream);//取出合并文件的md5进行比较String mergeFileMd5 = DigestUtils.md5Hex(mergeFileStream);if (originalMd5.equals(mergeFileMd5)) {System.out.println("合并文件成功");} else {System.out.println("合并文件失败");}}}
上传视频流程
下图是上传视频的整体流程:
1、前端上传文件前请求媒资接口层检查文件是否存在,如果已经存在则不再上传。
2、如果文件在系统不存在前端开始上传,首先对视频文件进行分块(前端分块)
3、前端分块进行上传,上传前首先检查分块是否上传,如已上传则不再上传,如果未上传则开始上传分块。
4、前端请求媒资管理接口层请求上传分块。
5、接口层请求服务层上传分块。
6、服务端将分块信息上传到MinIO。
7、前端将分块上传完毕请求接口层合并分块。
8、接口层请求服务层合并分块。
9、服务层根据文件信息找到MinIO中的分块文件,下载到本地临时目录,将所有分块下载完毕后开始合并 。
10、合并完成将合并后的文件上传到MinIO。
接口定义
根据上传视频流程,定义如下接口。
package com.xuecheng.media.api;import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.service.MediaFileService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;/*** @author Mr.M* @version 1.0* @description 大文件上传接口* @date 2022/9/6 11:29*/
@Api(value = "大文件上传接口", tags = "大文件上传接口")
@RestController
public class BigFilesController {@AutowiredMediaFileService mediaFileService;@ApiOperation(value = "文件上传前检查文件")@PostMapping("/upload/checkfile")public RestResponse<Boolean> checkfile(@RequestParam("fileMd5") String fileMd5) throws Exception {}@ApiOperation(value = "分块文件上传前的检测")@PostMapping("/upload/checkchunk")public RestResponse<Boolean> checkchunk(@RequestParam("fileMd5") String fileMd5,@RequestParam("chunk") int chunk) throws Exception {}@ApiOperation(value = "上传分块文件")@PostMapping("/upload/uploadchunk")public RestResponse uploadchunk(@RequestParam("file") MultipartFile file,@RequestParam("fileMd5") String fileMd5,@RequestParam("chunk") int chunk) throws Exception {}@ApiOperation(value = "合并文件")@PostMapping("/upload/mergechunks")public RestResponse mergechunks(@RequestParam("fileMd5") String fileMd5,@RequestParam("fileName") String fileName,@RequestParam("chunkTotal") int chunkTotal) throws Exception {}}
RestResponse 是一个通用的模型类,在base工程定义如下,从课程资料中拷贝RestResponse.java类到base工程下的model包下。
package com.xuecheng.base.model;import lombok.Data;
import lombok.ToString;/*** @description 通用结果类型* @author Mr.M* @date 2022/9/13 14:44* @version 1.0*/@Data@ToString
public class RestResponse<T> {/*** 响应编码,0为正常,-1错误*/private int code;/*** 响应提示信息*/private String msg;/*** 响应内容*/private T result;public RestResponse() {this(0, "success");}public RestResponse(int code, String msg) {this.code = code;this.msg = msg;}/*** 错误信息的封装** @param msg* @param <T>* @return*/public static <T> RestResponse<T> validfail(String msg) {RestResponse<T> response = new RestResponse<T>();response.setCode(-1);response.setMsg(msg);return response;}/*** 添加正常响应数据(包含响应内容)** @return RestResponse Rest服务封装相应数据*/public static <T> RestResponse<T> success(T result) {RestResponse<T> response = new RestResponse<T>();response.setResult(result);return response;}/*** 添加正常响应数据(不包含响应内容)** @return RestResponse Rest服务封装相应数据*/public static <T> RestResponse<T> success() {return new RestResponse<T>();}public Boolean isSuccessful() {return this.code == 0;}}
接口开发
DAO开发
向媒资数据库的文件表插入记录,使用自动生成的Mapper接口即可满足要求。
Service开发
接口定义
首先定义接口,从课程资料中拷贝RestResponse.java类到base工程下的model包下。
1、检查文件方法
检查文件是否在系统存在
/*** @description 检查文件是否存在* @param fileMd5 文件的md5* @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在,true存在* @author Mr.M* @date 2022/9/13 15:38
*/
public RestResponse<Boolean> checkFile(String fileMd5);
2、检查分块是否存在
/*** @description 检查分块是否存在* @param fileMd5 文件的md5* @param chunkIndex 分块序号* @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在,true存在* @author Mr.M* @date 2022/9/13 15:39
*/
public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex);
3、上传分块
/*** @description 上传分块* @param fileMd5 文件md5* @param chunk 分块序号* @param bytes 文件字节* @return com.xuecheng.base.model.RestResponse* @author Mr.M* @date 2022/9/13 15:50
*/
public RestResponse uploadChunk(String fileMd5,int chunk,byte[] bytes);
4、合并分块
/*** @description 合并分块* @param companyId 机构id* @param fileMd5 文件md5* @param chunkTotal 分块总和* @param uploadFileParamsDto 文件信息* @return com.xuecheng.base.model.RestResponse* @author Mr.M* @date 2022/9/13 15:56
*/
public RestResponse mergechunks(Long companyId,String fileMd5,int chunkTotal,UploadFileParamsDto uploadFileParamsDto);
检查文件和分块
接口完成进行接口实现,首先实现检查文件方法和检查分块方法。
service接口定义
/*** @description 检查文件是否存在* @param fileMd5 文件的md5* @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在,true存在* @author Mr.M* @date 2022/9/13 15:38
*/
public RestResponse<Boolean> checkFile(String fileMd5);/*** @description 检查分块是否存在* @param fileMd5 文件的md5* @param chunkIndex 分块序号* @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在,true存在* @author Mr.M* @date 2022/9/13 15:39
*/
public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex);
service接口实现方法:
@Override
public RestResponse<Boolean> checkFile(String fileMd5) {//查询文件信息MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);if (mediaFiles != null) {//桶String bucket = mediaFiles.getBucket();//存储目录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) {}}//文件不存在return RestResponse.success(false);
}@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) {//分块已存在return RestResponse.success(true);}} catch (Exception e) {}//分块未存在return RestResponse.success(false);
}//得到分块文件的目录
private String getChunkFileFolderPath(String fileMd5) {return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
}
上传分块
定义service接口
/*** @description 上传分块* @param fileMd5 文件md5* @param chunk 分块序号* @param bytes 文件字节* @return com.xuecheng.base.model.RestResponse* @author Mr.M* @date 2022/9/13 15:50
*/
public RestResponse uploadChunk(String fileMd5,int chunk,byte[] bytes);
接口实现:
@Override
public RestResponse uploadChunk(String fileMd5, int chunk, byte[] bytes) {//得到分块文件的目录路径String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);//得到分块文件的路径String chunkFilePath = chunkFileFolderPath + chunk;try {//将文件存储至minIOaddMediaFilesToMinIO(bytes, bucket_videoFiles,chunkFilePath,"application/octet-stream");} catch (Exception ex) {ex.printStackTrace();XueChengPlusException.cast("上传过程出错请重试");
}return RestResponse.success();
}
下载分块
合并分块前要检查分块文件是否全部上传完成,如果完成则将已经上传的分块文件下载下来,然后再进行合并,下边先实现检查及下载所有分块的方法。
//检查所有分块是否上传完毕
private File[] checkChunkStatus(String fileMd5, int chunkTotal) {//得到分块文件的目录路径String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);File[] files = new File[chunkTotal];//检查分块文件是否上传完毕for (int i = 0; i < chunkTotal; i++) {String chunkFilePath = chunkFileFolderPath + i;//下载文件File chunkFile =null;try {chunkFile = File.createTempFile("chunk" + i, null);} catch (IOException e) {e.printStackTrace();XueChengPlusException.cast("下载分块时创建临时文件出错");}downloadFileFromMinIO(chunkFile,bucket_videoFiles,chunkFilePath);files[i]=chunkFile;}return files;
}
//得到分块文件的目录
private String getChunkFileFolderPath(String fileMd5) {return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
}
//根据桶和文件路径从minio下载文件
public File downloadFileFromMinIO(File file,String bucket,String objectName){InputStream fileInputStream = null;OutputStream fileOutputStream = null;try {fileInputStream = minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(objectName).build());try {fileOutputStream = new FileOutputStream(file);IOUtils.copy(fileInputStream, fileOutputStream);} catch (IOException e) {XueChengPlusException.cast("下载文件"+objectName+"出错");}} catch (Exception e) {e.printStackTrace();XueChengPlusException.cast("文件不存在"+objectName);} finally {if (fileInputStream != null) {try {fileInputStream.close();} catch (IOException e) {e.printStackTrace();}}if (fileOutputStream != null) {try {fileOutputStream.close();} catch (IOException e) {e.printStackTrace();}}}return file;
}
合并分块
所有分块文件下载成功后开始合并这些分块文件。
@Override
public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {String fileName = uploadFileParamsDto.getFilename();//下载所有分块文件File[] chunkFiles = checkChunkStatus(fileMd5, chunkTotal);//扩展名String extName = fileName.substring(fileName.lastIndexOf("."));//创建临时文件作为合并文件File mergeFile = null;try {mergeFile = File.createTempFile(fileMd5, extName);} catch (IOException e) {XueChengPlusException.cast("合并文件过程中创建临时文件出错");}try {//开始合并byte[] b = new byte[1024];try(RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");) {for (File chunkFile : chunkFiles) {try (FileInputStream chunkFileStream = new FileInputStream(chunkFile);) {int len = -1;while ((len = chunkFileStream.read(b)) != -1) {//向合并后的文件写raf_write.write(b, 0, len);}}}} catch (IOException e) {e.printStackTrace();XueChengPlusException.cast("合并文件过程中出错");}log.debug("合并文件完成{}",mergeFile.getAbsolutePath());uploadFileParamsDto.setFileSize(mergeFile.length());try (InputStream mergeFileInputStream = new FileInputStream(mergeFile);) {//对文件进行校验,通过比较md5值String newFileMd5 = DigestUtils.md5Hex(mergeFileInputStream);if (!fileMd5.equalsIgnoreCase(newFileMd5)) {//校验失败XueChengPlusException.cast("合并文件校验失败");}log.debug("合并文件校验通过{}",mergeFile.getAbsolutePath());} catch (Exception e) {e.printStackTrace();//校验失败XueChengPlusException.cast("合并文件校验异常");}//将临时文件上传至minioString mergeFilePath = getFilePathByMd5(fileMd5, extName);try {//上传文件到minIOaddMediaFilesToMinIO(mergeFile.getAbsolutePath(), bucket_videoFiles, mergeFilePath);log.debug("合并文件上传MinIO完成{}",mergeFile.getAbsolutePath());} catch (Exception e) {e.printStackTrace();XueChengPlusException.cast("合并文件时上传文件出错");}//入数据库MediaFiles mediaFiles = addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_videoFiles, mergeFilePath);if (mediaFiles == null) {XueChengPlusException.cast("媒资文件入库出错");}return RestResponse.success();} finally {//删除临时文件for (File file : chunkFiles) {try {file.delete();} catch (Exception e) {}}try {mergeFile.delete();} catch (Exception e) {}}
}private String getFilePathByMd5(String fileMd5,String fileExt){return fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" +fileMd5 +fileExt;
}//将文件上传到minIO,传入文件绝对路径
public void addMediaFilesToMinIO(String filePath, String bucket, String objectName) {try {minioClient.uploadObject(UploadObjectArgs.builder().bucket(bucket).object(objectName).filename(filePath).build());} catch (Exception e) {e.printStackTrace();XueChengPlusException.cast("上传文件到文件系统出错");}
}
接口层完善
下边完善接口层
@ApiOperation(value = "文件上传前检查文件")
@PostMapping("/upload/checkfile")
public RestResponse<Boolean> checkfile(@RequestParam("fileMd5") String fileMd5
) throws Exception {return mediaFileService.checkFile(fileMd5);
}@ApiOperation(value = "分块文件上传前的检测")
@PostMapping("/upload/checkchunk")
public RestResponse<Boolean> checkchunk(@RequestParam("fileMd5") String fileMd5,@RequestParam("chunk") int chunk) throws Exception {return mediaFileService.checkChunk(fileMd5,chunk);
}@ApiOperation(value = "上传分块文件")
@PostMapping("/upload/uploadchunk")
public RestResponse uploadchunk(@RequestParam("file") MultipartFile file,@RequestParam("fileMd5") String fileMd5,@RequestParam("chunk") int chunk) throws Exception {return mediaFileService.uploadChunk(fileMd5,chunk,file.getBytes());
}@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);ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(fileName);String mimeType = extensionMatch.getMimeType();uploadFileParamsDto.setContentType(mimeType);return mediaFileService.mergechunks(companyId,fileMd5,chunkTotal,uploadFileParamsDto);}
接口测试
如果是单个接口测试使用httpclient
### 检查文件
POST{{media_host}}/media/upload/register
Content-Type: application/x-www-form-urlencoded;fileMd5=c5c75d70f382e6016d2f506d134eee11### 上传分块前检查
POST {{media_host}}/media/upload/checkchunk
Content-Type: application/x-www-form-urlencoded;fileMd5=c5c75d70f382e6016d2f506d134eee11&chunk=0### 上传分块文件
POST {{media_host}}/media/upload/uploadchunk?fileMd5=c5c75d70f382e6016d2f506d134eee11&chunk=1
Content-Type: multipart/form-data; boundary=WebAppBoundary--WebAppBoundary
Content-Disposition: form-data; name="file"; filename="1"
Content-Type: application/octet-stream< E:/ffmpeg_test/chunks/1### 合并文件
POST {{media_host}}/media/upload/mergechunks
Content-Type: application/x-www-form-urlencoded;fileMd5=dcb37b85c9c03fc5243e20ab4dfbc1c8&fileName=8.avi&chunkTotal=1
下边介绍采用前后联调:
1、首先在每个接口层方法上打开断点
在前端上传视频,观察接口层是否收到参数。
2、进入service方法逐行跟踪。
3、断点续传测试
上传一部分后,停止刷新浏览器再重新上传,通过浏览器日志发现已经上传过的分块不再重新上传
文件预览
需求分析
图片上传成功、视频上传成功可以通过预览功能查看文件的内容。
预览的方式是通过浏览器直接打文件,对于图片和浏览器支持的视频格式的视频文件可以直接预览。
业务流程如下:
说明如下:
1、前端请求接口层预览文件
2、接口层将文件id传递给服务层
3、服务层使用文件id查询媒资数据库文件表,获取文件的url
4、接口层将文件url返回给前端,通过浏览器打开URL。
接口定义
根据需求分析定义接口如下:
@ApiOperation("预览文件")
@GetMapping("/preview/{mediaId}")
public RestResponse<String> getPlayUrlByMediaId(@PathVariable String mediaId){}
接口开发
6.3.1 DAO开发
使用自动生成的MediaFiels表的Mapper接口。
6.3.2 Service开发
定义根据id查询媒资文件接口
/*** @description 根据id查询文件信息* @param id 文件id* @return com.xuecheng.media.model.po.MediaFiles 文件信息* @author Mr.M* @date 2022/9/13 17:47
*/
public MediaFiles getFileById(String id);
方法实现:
public MediaFiles getFileById(String id) {return mediaFilesMapper.selectById(id);}
接口层完善
对接口层完善:
@ApiOperation("预览文件")
@GetMapping("/preview/{mediaId}")
public RestResponse<String> getPlayUrlByMediaId(@PathVariable String mediaId){MediaFiles mediaFiles = mediaFileService.getFileById(mediaId);if(mediaFiles == null || StringUtils.isEmpty(mediaFiles.getUrl())){XueChengPlusException.cast("视频还没有转码处理");}return RestResponse.success(mediaFiles.getUrl());}
接口测试
使用前后端联调。
上传mp4视频文件,预览文件。
上传图片文件,预览文件。
对于无法预览的视频文件,稍后通过视频处理对视频转码。
视频处理
视频编码技术
什么是视频编码
视频上传成功后需要对视频进行转码处理。
什么是视频编码?查阅百度百科如下:
详情参考 :百度百科-验证
首先我们要分清文件格式和编码格式:
文件格式:是指.mp4、.avi、.rmvb等 这些不同扩展名的视频文件的文件格式 ,视频文件的内容主要包括视频和音频,其文件格式是按照一 定的编码格式去编码,并且按照该文件所规定的封装格式将视频、音频、字幕等信息封装在一起,播放器会根据它们的封装格式去提取出编码,然后由播放器解码,最终播放音视频。
音视频编码格式:通过音视频的压缩技术,将视频格式转换成另一种视频格式,通过视频编码实现流媒体的传输。比如:一个.avi的视频文件原来的编码是a,通过编码后编码格式变为b,音频原来为c,通过编码后变为d。
音视频编码格式各类繁多,主要有几下几类:
MPEG系列
(由ISO[国际标准组织机构]下属的MPEG[运动图象专家组]开发 )视频编码方面主要是Mpeg1(vcd用的就是它)、Mpeg2(DVD使用)、Mpeg4(的DVDRIP使用的都是它的变种,如:divx,xvid等)、Mpeg4 AVC(正热门);音频编码方面主要是MPEG Audio Layer 1/2、MPEG Audio Layer 3(大名鼎鼎的mp3)、MPEG-2 AAC 、MPEG-4 AAC等等。注意:DVD音频没有采用Mpeg的。
H.26X系列
(由ITU[国际电传视讯联盟]主导,侧重网络传输,注意:只是视频编码)
包括H.261、H.262、H.263、H.263+、H.263++、H.264(就是MPEG4 AVC-合作的结晶)
目前最常用的编码标准是视频H.264,音频AAC。
提问:
H.264是编码格式还是文件格式?
mp4是编码格式还是文件格式?
FFmpeg 的基本使用
我们将视频录制完成后,使用视频编码软件对视频进行编码,本项目 使用FFmpeg对视频进行编码
。
FFmpeg被许多开源项目采用,QQ影音、暴风影音、VLC等。
下载:FFmpeg Download FFmpeg
请从课程资料目录解压ffmpeg.zip,并将解压得到的exe文件加入环境变量。
测试是否正常:cmd运行 ffmpeg -v
安装成功,作下简单测试
将一个.avi文件转成mp4、mp3、gif等。
比如我们将nacos.avi文件转成mp4,运行如下命令:
ffmpeg -i nacos.avi nacos.mp4
转成mp3:ffmpeg -i nacos.avi nacos.mp3
转成gif:ffmpeg -i nacos.avi nacos.gif
官方文档(英文):ffmpeg Documentation
视频转码工具类
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 = "D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe";//ffmpeg的安装位置String video_path = "D:\\BaiduNetdiskDownload\\test1.avi";String mp4_name = "test1.mp4";String mp4folder_path = "D:/BaiduNetdiskDownload/Movies/test1/";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;}//清除已生成的mp4private void clear_mp4(String mp4_path){//删除原来已经生成的m3u8及ts文件File mp4File = new File(mp4_path);if(mp4File.exists() && mp4File.isFile()){mp4File.delete();}}/*** 视频编码,生成mp4文件* @return 成功返回success,失败返回控制台日志*/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 );commend.add(mp4folder_path );String outstring = null;try {ProcessBuilder builder = new ProcessBuilder();builder.command(commend);//将标准输入流和错误输入流合并,通过标准输入流程读取信息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);Boolean check_video_time = this.check_video_time(video_path, mp4folder_path);if(!check_video_time){return outstring;}else{return "success";}}public static void main(String[] args) throws IOException {// ProcessBuilder builder = new ProcessBuilder();
// builder.command("C:\\Program Files (x86)\\Tencent\\QQ\\Bin\\QQScLauncher.exe");
// //将标准输入流和错误输入流合并,通过标准输入流程读取信息
// builder.redirectErrorStream(true);
// Process p = builder.start();//ffmpeg的路径String ffmpeg_path = "D:\\soft\\ffmpeg\\ffmpeg.exe";//ffmpeg的安装位置//源avi视频的路径String video_path = "D:\\develop\\bigfile_test\\nacos_01.avi";//转换后mp4文件的名称String mp4_name = "nacos_01.mp4";//转换后mp4文件的路径String mp4_path = "D:\\develop\\bigfile_test\\nacos_01.mp4";//创建工具类对象Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4_path);//开始视频转换,成功将返回successString s = videoUtil.generateMp4();System.out.println(s);}
}