目录
一、功能介绍
实现了vue上传视频到阿里云oss,当上传视频的时候会查询是否上传过,如果上传过则返回,没有上传过则实现上传配合md5,利用ffmpeg实现自动配置封面图等功能。


二、配置阿里云OSS
1、阿里云OSS的相关依赖:
xml
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
2、引入JavaCV相关依赖:
xml
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.9</version>
</dependency>
xml
aliyun:
oss:
# 1. endpoint - OSS服务端点
endpoint: #####
# 2. bucketName - 存储空间名称
bucketName: ####
# 3. region - 地域节点
region: cn-beijing
# 4. accessKeyId - 访问密钥ID
accessKeyId: ####
# 5. accessKeySecret - 访问密钥密钥
accessKeySecret: ########
将上面的application.yaml添加上以上的配置信息。
三、定义阿里云OSS配置类文件
java
package com.qcby.util;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSProperties {
private String endpoint;
private String bucketName;
private String region;
private String accessKeyId;
private String accessKeySecret;
public String getEndpoint() {
return endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public String getBucketName() {
return bucketName;
}
public void setBucketName(String bucketName) {
this.bucketName = bucketName;
}
public String getRegion() {
return region;
}
public void setRegion(String region) {
this.region = region;
}
public String getAccessKeyId() {
return accessKeyId;
}
public void setAccessKeyId(String accessKeyId) {
this.accessKeyId = accessKeyId;
}
public String getAccessKeySecret() {
return accessKeySecret;
}
public void setAccessKeySecret(String accessKeySecret) {
this.accessKeySecret = accessKeySecret;
}
}
使用@Component将类注册为Spring Bean,使用@ConfigurationProperties(prefix = "aliyun.oss")读取application.yml文件的配置信息。
四、阿里云OSS操作类
java
package com.qcby.util;
import com.aliyun.oss.ClientBuilderConfiguration;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.auth.DefaultCredentialProvider;
import com.aliyun.oss.common.comm.SignVersion;
import com.aliyun.oss.model.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Component
public class AliyunOSSOperator {
@Autowired
private AliyunOSSProperties aliyunOSSProperties;
public String upload(byte[] content, String originalFilename) {
String endpoint = aliyunOSSProperties.getEndpoint();
String bucketName = aliyunOSSProperties.getBucketName();
String region = aliyunOSSProperties.getRegion();
String accessKeyId = aliyunOSSProperties.getAccessKeyId();
String accessKeySecret = aliyunOSSProperties.getAccessKeySecret();
// 生成存储路径:yyyy/MM/uuid.扩展名
String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
String objectName = dir + "/" + newFileName;
// 使用 DefaultCredentialProvider 直接传入 AK/SK
DefaultCredentialProvider credentialProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
// 配置 OSS Client(可选,如启用 V4 签名)
ClientBuilderConfiguration clientConfig = new ClientBuilderConfiguration();
clientConfig.setSignatureVersion(SignVersion.V4);
// 创建 OSSClient 实例
OSS ossClient = OSSClientBuilder.create()
.endpoint(endpoint)
.credentialsProvider(credentialProvider) // 使用 AK/SK 凭证
.clientConfiguration(clientConfig)
.region(region)
.build();
try {
// 上传文件
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));
// 返回文件的访问 URL(如:https://bucket.endpoint/objectName)
return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
} finally {
ossClient.shutdown(); // 关闭 OSSClient
}
}
// 新的 InputStream 方法
public String upload(InputStream inputStream, String originalFilename) {
String endpoint = aliyunOSSProperties.getEndpoint();
String bucketName = aliyunOSSProperties.getBucketName();
String region = aliyunOSSProperties.getRegion();
String accessKeyId = aliyunOSSProperties.getAccessKeyId();
String accessKeySecret = aliyunOSSProperties.getAccessKeySecret();
// 生成存储路径:yyyy/MM/uuid.扩展名
String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
String objectName = dir + "/" + newFileName;
DefaultCredentialProvider credentialProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
ClientBuilderConfiguration clientConfig = new ClientBuilderConfiguration();
clientConfig.setSignatureVersion(SignVersion.V4);
OSS ossClient = OSSClientBuilder.create()
.endpoint(endpoint)
.credentialsProvider(credentialProvider)
.clientConfiguration(clientConfig)
.region(region)
.build();
try {
// 上传文件流
ossClient.putObject(bucketName, objectName, inputStream);
// 返回文件的访问 URL
return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
} finally {
ossClient.shutdown();
}
}
/**
* 分片上传 - 适用于大文件
*/
public String uploadMultipart(InputStream inputStream, String originalFilename, long fileSize) throws Exception {
String endpoint = aliyunOSSProperties.getEndpoint();
String bucketName = aliyunOSSProperties.getBucketName();
String region = aliyunOSSProperties.getRegion();
String accessKeyId = aliyunOSSProperties.getAccessKeyId();
String accessKeySecret = aliyunOSSProperties.getAccessKeySecret();
// 生成存储路径
String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
String newFileName = UUID.randomUUID() + getFileExtension(originalFilename);
String objectName = dir + "/" + newFileName;
DefaultCredentialProvider credentialProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
ClientBuilderConfiguration clientConfig = new ClientBuilderConfiguration();
clientConfig.setSignatureVersion(SignVersion.V4);
OSS ossClient = OSSClientBuilder.create()
.endpoint(endpoint)
.credentialsProvider(credentialProvider)
.clientConfiguration(clientConfig)
.region(region)
.build();
try {
// 1. 初始化分片上传
InitiateMultipartUploadRequest initiateRequest = new InitiateMultipartUploadRequest(bucketName, objectName);
InitiateMultipartUploadResult initiateResult = ossClient.initiateMultipartUpload(initiateRequest);
String uploadId = initiateResult.getUploadId();
// 2. 设置分片大小(5MB)
final long partSize = 5 * 1024 * 1024;
List<PartETag> partETags = new ArrayList<>();
// 3. 上传分片
int partNumber = 1;
long bytesRead = 0;
byte[] buffer = new byte[(int) partSize];
int bytes = 0;
while ((bytes = inputStream.read(buffer)) != -1) {
// 最后一个分片可能小于 partSize
if (bytes < partSize) {
byte[] lastBuffer = new byte[bytes];
System.arraycopy(buffer, 0, lastBuffer, 0, bytes);
buffer = lastBuffer;
}
UploadPartRequest uploadPartRequest = new UploadPartRequest();
uploadPartRequest.setBucketName(bucketName);
uploadPartRequest.setKey(objectName);
uploadPartRequest.setUploadId(uploadId);
uploadPartRequest.setInputStream(new ByteArrayInputStream(buffer));
uploadPartRequest.setPartSize(bytes);
uploadPartRequest.setPartNumber(partNumber);
UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
partETags.add(uploadPartResult.getPartETag());
System.out.println("上传分片 " + partNumber + " 完成, 大小: " + bytes + " bytes");
partNumber++;
bytesRead += bytes;
}
// 4. 完成分片上传
CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(
bucketName, objectName, uploadId, partETags);
CompleteMultipartUploadResult completeResult = ossClient.completeMultipartUpload(completeRequest);
// 返回文件的访问 URL
return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
} catch (Exception e) {
throw new Exception("分片上传失败: " + e.getMessage(), e);
} finally {
ossClient.shutdown();
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception e) {
System.err.println("关闭输入流失败: " + e.getMessage());
}
}
}
}
/**
* 智能上传 - 根据文件大小自动选择上传方式
*/
public String smartUpload(InputStream inputStream, String originalFilename, long fileSize) throws Exception {
// 小于50MB使用简单上传,大于50MB使用分片上传
long threshold = 50 * 1024 * 1024;
if (fileSize <= threshold) {
return upload(inputStream, originalFilename);
} else {
return uploadMultipart(inputStream, originalFilename, fileSize);
}
}
/**
* 获取文件扩展名
*/
private String getFileExtension(String filename) {
if (filename == null || filename.lastIndexOf(".") == -1) {
return "";
}
return filename.substring(filename.lastIndexOf("."));
}
}
将阿里云OSS配置文件类引入阿里云OSS操作类当中,执行upload上传到阿里云OSS文件中。
五、编写文件上传接口
java
package com.qcby.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.qcby.dao.VideoDao;
import com.qcby.entity.Video;
import com.qcby.result.Result;
import com.qcby.util.AliyunOSSOperator;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.FrameGrabber;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Arrays;
import java.util.UUID;
//上传接口
@RestController
public class UploadController {
@Autowired
private AliyunOSSOperator aliyunOSSOperator;
@Autowired
private VideoDao videoDao;
@RequestMapping("/upload")
public Result uploadFile(MultipartFile file) throws Exception {
String originalFilename = file.getOriginalFilename();
byte[] bytes = file.getBytes();
String url = aliyunOSSOperator.upload(bytes,originalFilename);
return new Result().success(url);
}
@RequestMapping("/uploadFile")
public Result uploadFile(@RequestParam("file") MultipartFile file,
@RequestParam("classId") Integer classId,
HttpServletRequest request) throws Exception {
// 1. 基础验证
if (file.isEmpty()) {
return Result.error("文件不能为空");
}
// 2. 文件大小限制 (100MB)
long maxSize = 100 * 1024 * 1024;
System.out.println("文件大小:" + file.getSize());
System.out.println("文件允许大小:" + maxSize);
if (file.getSize() > maxSize) {
return Result.error("文件大小不能超过100MB");
}
// 3. 文件类型验证
String originalFilename = file.getOriginalFilename();
String fileExtension = getFileExtension(originalFilename);
String[] allowedExtensions = {"mp4", "avi", "mov", "wmv", "flv", "m4v"};
if (!Arrays.asList(allowedExtensions).contains(fileExtension.toLowerCase())) {
return Result.error("只支持视频文件格式:mp4, avi, mov, wmv, flv, m4v");
}
try {
// 4. 计算文件MD5
String md5Hash = calculateMD5(file);
// 5. 检查文件是否已存在(避免重复上传)
Video existingVideo = videoDao.selectOne(new QueryWrapper<Video>()
.eq("md5_hash", md5Hash)
.eq("class_id", classId)
.eq("status", 1));
if (existingVideo != null) {
return Result.success("文件已存在", existingVideo.getStoragePath());
}
Video video = new Video();
// 6. 上传到OSS
String url;
try (InputStream inputStream = file.getInputStream()) {
url = aliyunOSSOperator.upload(inputStream, originalFilename);
}
try {
// 使用FFmpeg截取视频第一帧作为封面
String coverUrl = extractVideoFrame(url);
video.setCoverUrl(coverUrl);
video.setCoverStatus(1); // 生成成功
} catch (Exception e) {
// 生成失败,使用默认封面
video.setCoverUrl(getDefaultCoverUrl());
video.setCoverStatus(2); // 生成失败
}
// 7. 获取视频时长(需要额外实现)
Integer duration = getVideoDuration(file);
System.out.println("视频时长:" + duration);
// 8. 保存到数据库
video.setOriginalName(originalFilename);
video.setStoragePath(url);
video.setFileSize(file.getSize());
video.setDuration(duration);
video.setMimeType(file.getContentType());
video.setUploaderId(getCurrentUserId(request)); // 从session或token获取用户ID
video.setStatus(1);
video.setMd5Hash(md5Hash);
video.setClassId(classId);
videoDao.insert(video);
return new Result().success("上传成功", url);
} catch (Exception e) {
return Result.error("上传失败: " + e.getMessage());
}
}
// 辅助方法
private String getFileExtension(String filename) {
if (filename == null || filename.lastIndexOf(".") == -1) {
return "";
}
return filename.substring(filename.lastIndexOf(".") + 1);
}
private String calculateMD5(MultipartFile file) throws Exception {
try (InputStream is = file.getInputStream()) {
return DigestUtils.md5DigestAsHex(is);
}
}
// 获取视频时长(需要依赖)
private Integer getVideoDuration(MultipartFile file) {
File tempFile = null;
try {
// 创建临时文件
tempFile = File.createTempFile("video_", "_temp");
file.transferTo(tempFile);
// 使用 FFmpegFrameGrabber 获取视频信息
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(tempFile)) {
grabber.start();
// 方法1:直接获取时长(微秒)
long durationInMicroseconds = grabber.getLengthInTime();
if (durationInMicroseconds > 0) {
int durationInSeconds = (int) (durationInMicroseconds / 1000000);
System.out.println("通过 getLengthInTime 获取时长: " + durationInSeconds + "秒");
return durationInSeconds;
}
// 方法2:通过帧率和总帧数计算
double frameRate = grabber.getFrameRate();
long totalFrames = grabber.getLengthInFrames();
if (frameRate > 0 && totalFrames > 0) {
int durationInSeconds = (int) (totalFrames / frameRate);
System.out.println("通过帧率计算时长 - 总帧数: " + totalFrames + ", 帧率: " + frameRate + ", 时长: " + durationInSeconds + "秒");
return durationInSeconds;
}
// 方法3:尝试从格式上下文获取
try {
double duration = grabber.getFormatContext().duration();
if (!Double.isNaN(duration) && duration > 0) {
int durationInSeconds = (int) (duration / 1000000);
System.out.println("通过 FormatContext 获取时长: " + durationInSeconds + "秒");
return durationInSeconds;
}
} catch (Exception e) {
System.out.println("FormatContext 获取时长失败: " + e.getMessage());
}
grabber.stop();
System.out.println("无法获取视频时长");
return null;
} catch (FrameGrabber.Exception e) {
System.out.println("FrameGrabber 异常: " + e.getMessage());
return null;
}
} catch (Exception e) {
System.out.println("获取视频时长失败: " + e.getMessage());
return null;
} finally {
// 删除临时文件
if (tempFile != null && tempFile.exists()) {
try {
tempFile.delete();
} catch (Exception e) {
System.out.println("删除临时文件失败: " + e.getMessage());
}
}
}
}
private Long getCurrentUserId(HttpServletRequest request) {
// 从session、token或SecurityContext中获取当前用户ID
// 示例:return (Long) request.getSession().getAttribute("userId");
// TODO 获取当前用户ID
return 1L; // 临时返回固定值
}
private String extractVideoFrame(String videoUrl) throws Exception {
FFmpegFrameGrabber grabber = null;
try {
grabber = new FFmpegFrameGrabber(videoUrl);
grabber.start();
// 获取视频总帧数
int totalFrames = grabber.getLengthInFrames();
System.out.println("视频总帧数: " + totalFrames);
// 选择提取帧的位置(例如:第10帧或视频的1/10处)
int targetFrame = Math.min(10, totalFrames / 10);
if (targetFrame > 0) {
grabber.setFrameNumber(targetFrame);
}
// 获取帧
Frame frame = grabber.grabImage();
if (frame == null) {
// 如果获取失败,尝试获取第一帧
grabber.setFrameNumber(0);
frame = grabber.grabImage();
}
if (frame == null) {
throw new Exception("无法从视频中提取帧");
}
// 转换为 BufferedImage
Java2DFrameConverter converter = new Java2DFrameConverter();
BufferedImage image = converter.getBufferedImage(frame);
// 调整图片尺寸(可选)
image = resizeImage(image, 320, 180);
// 转换为 InputStream 并上传
InputStream imageStream = imageToInputStream(image, "jpg", 0.8f);
String coverFileName = "covers/" + System.currentTimeMillis() + "_" + (int)(Math.random() * 1000) + ".jpg";
String coverUrl = aliyunOSSOperator.upload(imageStream, coverFileName);
return coverUrl;
} finally {
if (grabber != null) {
try {
grabber.stop();
} catch (Exception e) {
System.err.println("关闭FFmpegFrameGrabber失败: " + e.getMessage());
}
}
}
}
/**
* 调整图片尺寸
*/
private BufferedImage resizeImage(BufferedImage originalImage, int targetWidth, int targetHeight) {
BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
java.awt.Graphics2D g = resizedImage.createGraphics();
g.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null);
g.dispose();
return resizedImage;
}
/**
* 将 BufferedImage 转换为 InputStream
*/
private InputStream imageToInputStream(BufferedImage image, String format, float quality) throws Exception {
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
// 使用ImageIO写入
boolean result = ImageIO.write(image, format, os);
if (!result) {
// 如果指定格式不支持,尝试JPEG
result = ImageIO.write(image, "jpg", os);
if (!result) {
throw new Exception("不支持的图片格式: " + format);
}
}
return new ByteArrayInputStream(os.toByteArray());
} finally {
try {
os.close();
} catch (Exception e) {
System.err.println("关闭ByteArrayOutputStream失败: " + e.getMessage());
}
}
}
private String getDefaultCoverUrl() {
// 返回默认封面图URL
return "https://lyf-java-ai.oss-cn-beijing.aliyuncs.com/20250428/1745853671798234.jpg";
}
}
简单的上传接口:@RequestMapping("/upload"),视频文件上传接口:@RequestMapping("/uploadFile")
六、Video相关配置
1、Video实体类
java
package com.qcby.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
@Data
@TableName("video")
public class Video {
@TableId(type = IdType.AUTO)
private Long id;
private String originalName;
private String storagePath;
private Long fileSize;
private Integer duration;
private String mimeType;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date uploadTime;
private Long uploaderId;
private Integer status;
private String md5Hash;
private Integer classId;
private String coverUrl;
private Integer coverStatus;
}
2、Video数据库和Mapper层
数据库表定义如下:
sql
CREATE TABLE `video` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`original_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '原始文件名',
`storage_path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '存储路径',
`file_size` bigint(0) NOT NULL COMMENT '文件大小(字节)',
`duration` int(0) DEFAULT NULL COMMENT '视频时长(秒)',
`mime_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '文件类型',
`upload_time` datetime(0) DEFAULT CURRENT_TIMESTAMP,
`uploader_id` bigint(0) DEFAULT NULL COMMENT '上传者ID',
`status` tinyint(0) DEFAULT 1 COMMENT '状态:1正常,0删除',
`md5_hash` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '文件MD5哈希',
`class_id` int(0) NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
java
package com.qcby.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.qcby.entity.Video;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface VideoDao extends BaseMapper<Video> {
}