<properties>
<java.version>11java.version>
<ffmpeg.version>0.6.2ffmpeg.version>
<hutool.version>5.7.15hutool.version>
<aliyun-sdk-oss.version>3.13.2aliyun-sdk-oss.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.bytedecogroupId>
<artifactId>javacv-platformartifactId>
<version>1.5.8version>
dependency>
<dependency>
<groupId>org.bytedecogroupId>
<artifactId>ffmpeg-platform-gplartifactId>
<version>5.1.2-1.5.8version>
dependency>
<dependency>
<groupId>net.bramp.ffmpeggroupId>
<artifactId>ffmpegartifactId>
<version>${ffmpeg.version}version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>${hutool.version}version>
dependency>
<dependency>
<groupId>com.aliyun.ossgroupId>
<artifactId>aliyun-sdk-ossartifactId>
<version>${aliyun-sdk-oss.version}version>
dependency>
dependencies>
注意: ffmpeg-platform
与 javacv-platform
版本要对应
spring:
servlet:
multipart:
max-file-size: 200MB
max-request-size: 500MB
#m3u8视频转换配置
m3u8:
convertor:
base-path: /file/m3u8/
temp-path: /file/temp/
big-path: /file/big/
proxy: m3u8/
ali:
oss:
#oss end-point
end-point:
#oss access-key-id
access-key-id:
#oss access-key-secret
access-key-secret:
#oss bucket-name
bucket-name:
#访问地址 可以与 ali-url my-host-url 一致 例如 https://12312312.oss-cn-shenzhen.aliyuncs.com/
url:
#访问地址 可以与 my-host-url url 一致 例如 https://12312312.oss-cn-shenzhen.aliyuncs.com/
ali-url:
get-file-url: ${aliyun.oss.url}${aliyun.oss.fileDir}
#访问地址 可以与 ali-url url 一致 例如 https://12312312.oss-cn-shenzhen.aliyuncs.com/
my-host-url:
config
包 properties
配置相关类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* 线程池配置
*/
@Configuration
@EnableAsync
public class SpringAsyncConfig {
/**
* oss async
* @return
*/
@Bean("ossUploadTreadPool")
public ThreadPoolTaskExecutor asyncServiceExecutorForOss() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数,采用IO密集 h/(1-拥塞)
executor.setCorePoolSize(8);
// 设置最大线程数,由于minIO连接数量有限,此处尽力设计大点
executor.setMaxPoolSize(120);
// 设置线程活跃时间(秒)
executor.setKeepAliveSeconds(30);
// 设置默认线程名称
executor.setThreadNamePrefix("ossUploadTask-");
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
//执行初始化
executor.initialize();
return executor;
}
}
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "m3u8.convertor")
public class FilePath {
/**
* 文件上传临时路径 (本地文件转换不需要)
*/
private String tempPath = "/file/tmp/";
/**
* m3u8文件转换后,储存的根路径
*/
private String basePath = "/file/m3u8/";
/**
* m3u8文件转换后,储存的根路径
*/
private String bigPath = "/file/big/";
private String proxy = "m3u8/";
}
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "ali.oss")
@Data
public class AliOssProperties {
/**
* OSS配置信息
*/
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
private String myHostUrl;
private String url;
private String aliUrl;
}
component
包 相关组件类
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.CompleteMultipartUploadRequest;
import com.aliyun.oss.model.CompleteMultipartUploadResult;
import com.aliyun.oss.model.InitiateMultipartUploadRequest;
import com.aliyun.oss.model.InitiateMultipartUploadResult;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PartETag;
import com.aliyun.oss.model.PutObjectResult;
import com.aliyun.oss.model.UploadPartRequest;
import com.aliyun.oss.model.UploadPartResult;
import com.laowei.ffmpegm3u8demo.config.AliOssProperties;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.UUID;
/**
* 阿里云 OSS 工具类
*
*
*/
@Component
@Slf4j
@Getter
public class OssComponent{
@Resource
private AliOssProperties aliOssProperties;
/* -----------------对外功能---------------- */
/**
* 本地文件切片上传
*
* @param objectName:文件名
* @param path : 本地完整路径,xxx/xxx.txt
* @return :异常
*/
public String uploadSlice(String objectName, String localPath,String path) throws IOException {
OSS ossClient = new OSSClientBuilder().build(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret());
String keyPath = path+objectName;
// 创建InitiateMultipartUploadRequest对象。
InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(aliOssProperties.getBucketName(), keyPath);
// 如果需要在初始化分片时设置请求头,请参考以下示例代码。
ObjectMetadata metadata = new ObjectMetadata();
// 指定该Object的网页缓存行为。
metadata.setCacheControl("no-cache");
// 指定该Object被下载时的名称。
metadata.setContentDisposition("attachment;filename=" + objectName);
// 指定初始化分片上传时是否覆盖同名Object。此处设置为true,表示禁止覆盖同名Object。
metadata.setHeader("x-oss-forbid-overwrite", "true");
// 初始化分片。
InitiateMultipartUploadResult result = ossClient.initiateMultipartUpload(request);
// 返回uploadId,它是分片上传事件的唯一标识。您可以根据该uploadId发起相关的操作,例如取消分片上传、查询分片上传等。
String uploadId = result.getUploadId();
List<PartETag> partETags = new ArrayList<>();
// 每个分片的大小,用于计算文件有多少个分片。单位为字节。
final long partSize = 5 * 1024 * 1024L; //1 MB。
// 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
final File sampleFile = new File(localPath);
long fileLength = sampleFile.length();
int partCount = (int) (fileLength / partSize);
if (fileLength % partSize != 0) {
partCount++;
}
// 遍历分片上传。
for (int i = 0; i < partCount; i++) {
long startPos = i * partSize;
long curPartSize = (i + 1 == partCount) ? (fileLength - startPos) : partSize;
try (InputStream inStream = new FileInputStream(sampleFile)) {
// 跳过已经上传的分片。
long skip = inStream.skip(startPos);
UploadPartRequest uploadPartRequest = new UploadPartRequest();
uploadPartRequest.setBucketName(aliOssProperties.getBucketName());
uploadPartRequest.setKey(keyPath);
uploadPartRequest.setUploadId(uploadId);
uploadPartRequest.setInputStream(inStream);
// 设置分片大小。除了最后一个分片没有大小限制,其他的分片最小为100 KB。
uploadPartRequest.setPartSize(curPartSize);
// 设置分片号。每一个上传的分片都有一个分片号,取值范围是1~10000,如果超出此范围,OSS将返回InvalidArgument错误码。
uploadPartRequest.setPartNumber(i + 1);
// 每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会按照分片号排序组成完整的文件。
UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
// 每次上传分片之后,OSS的返回结果包含PartETag。PartETag将被保存在partETags中。
partETags.add(uploadPartResult.getPartETag());
}catch (Exception e){
log.error("OSS切片上传异常,e:{}",e.getMessage());
}
}
// 创建CompleteMultipartUploadRequest对象。
// 在执行完成分片上传操作时,需要提供所有有效的partETags。OSS收到提交的partETags后,会逐一验证每个分片的有效性。当所有的数据分片验证通过后,OSS将把这些分片组合成一个完整的文件。
CompleteMultipartUploadRequest completeMultipartUploadRequest =
new CompleteMultipartUploadRequest(aliOssProperties.getBucketName(),keyPath , uploadId, partETags);
// 完成分片上传。
CompleteMultipartUploadResult completeMultipartUploadResult = ossClient.completeMultipartUpload(completeMultipartUploadRequest);
log.info(completeMultipartUploadResult.getETag());
// 关闭OSSClient。
ossClient.shutdown();
return path+objectName;
}
/**
* 单个文件上传
*
* @param file 文件
* @return 返回完整URL地址
*/
public String uploadFile(String fileDir, MultipartFile file) {
String fileUrl = upload2Oss(fileDir, file);
String str = getFileUrl(fileDir, fileUrl);
return str.trim();
}
/**
* 单个文件上传(指定文件名(带后缀))
*
* @param inputStream 文件
* @param fileName 文件名(带后缀)
* @return 返回完整URL地址
*/
public String uploadFile(String fileDir, InputStream inputStream, String fileName) {
try {
this.uploadFile2Oss(fileDir, inputStream, fileName);
String url = getFileUrl(fileDir, fileName);
if (url != null && url.length() > 0) {
return url;
}
} catch (Exception e) {
throw new RuntimeException("获取路径失败");
}
return "";
}
/**
* 多文件上传
*
* @param fileList 文件列表
* @return 返回完整URL,逗号分隔
*/
public String uploadFile(String fileDir, List<MultipartFile> fileList) {
String fileUrl;
String str;
StringBuilder photoUrl = new StringBuilder();
for (int i = 0; i < fileList.size(); i++) {
fileUrl = upload2Oss(fileDir, fileList.get(i));
str = getFileUrl(fileDir, fileUrl);
if (i == 0) {
photoUrl = new StringBuilder(str);
} else {
photoUrl.append(",").append(str);
}
}
return photoUrl.toString().trim();
}
public boolean deleteFile(String fileDir, String fileName) {
OSS ossClient = new OSSClientBuilder().build(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret());
// 删除文件
ossClient.deleteObject(aliOssProperties.getBucketName(), fileDir + fileName);
// 判断文件是否存在
boolean found = ossClient.doesObjectExist(aliOssProperties.getBucketName(), fileDir + fileName);
// 如果文件存在则删除失败
return !found;
}
/**
* 通过文件名获取文完整件路径
*
* @param fileUrl 文件名
* @return 完整URL路径
*/
public String getFileUrl(String fileDir, String fileUrl) {
if (fileUrl != null && fileUrl.length() > 0) {
String[] split = fileUrl.replaceAll("\\\\","/").split("/");
String url = aliOssProperties.getMyHostUrl() + fileDir + split[split.length - 1];
return Objects.requireNonNull(url);
}
return null;
}
public File getFile(String url) {
//对本地文件命名
String fileName = url.substring(url.lastIndexOf("."));
File file = null;
try {
file = File.createTempFile("net_url", fileName);
} catch (Exception e) {
log.error("创建默认文件夹net_url失败!原因e:{}", e.getMessage());
}
if (file != null) {
try (InputStream inStream = new URL(url).openStream();
OutputStream os = new FileOutputStream(file)) {
int bytesRead;
byte[] buffer = new byte[8192];
while ((bytesRead = inStream.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
} catch (Exception e) {
e.printStackTrace();
}
}
return file;
}
/* -----------内部辅助功能------------------------ */
/**
* 获取去掉参数的完整路径
*
* @param url URL
* @return 去掉参数的URL
*/
private String getShortUrl(String url) {
String[] imgUrls = url.split("\\?");
return imgUrls[0].trim();
}
/**
* 获得url真实外网链接
* 不提供使用,因为会产生公网OOS流量下行费用
*
* @param key 文件名
* @return URL
*/
@Deprecated
private String getUrl(String key) {
OSS ossClient = new OSSClientBuilder().build(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret());
// 设置URL过期时间为20年 3600l* 1000*24*365*20
Date expiration = new Date(System.currentTimeMillis() + 3600L * 1000 * 24 * 365 * 20);
URL url = ossClient.generatePresignedUrl(aliOssProperties.getBucketName(), key, expiration);
if (url != null) {
String replaceUrl = url.toString()
.replace(aliOssProperties.getAliUrl(), aliOssProperties.getUrl());
return getShortUrl(replaceUrl);
}
ossClient.shutdown();
return null;
}
/**
* 上传文件
*
* @param file 文件
* @return 文件名
*/
private String upload2Oss(String fileDir, MultipartFile file) {
// 2、重命名文件
String fileName = Objects.requireNonNull(file.getOriginalFilename(), "文件名不能为空");
// 文件后缀
String suffix = fileName.substring(fileName.lastIndexOf(".")).toLowerCase(Locale.ENGLISH);
String uuid = UUID.randomUUID().toString();
String name = uuid + suffix;
try {
InputStream inputStream = file.getInputStream();
this.uploadFile2Oss(fileDir, inputStream, name);
return name;
} catch (Exception e) {
throw new RuntimeException("上传失败");
}
}
/**
* 上传文件(指定文件名)
*
* @param inputStream 输入流
* @param fileName 文件名
*/
private void uploadFile2Oss(String fileDir, InputStream inputStream, String fileName) {
OSS ossClient = new OSSClientBuilder().build(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret());
String ret;
try {
//创建上传Object的Metadata
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(inputStream.available());
objectMetadata.setCacheControl("no-cache");
objectMetadata.setHeader("Pragma", "no-cache");
objectMetadata.setContentType(getContentType(fileName.substring(fileName.lastIndexOf("."))));
objectMetadata.setContentDisposition("inline;filename=" + fileName);
//上传文件
PutObjectResult putResult = ossClient.putObject(aliOssProperties.getBucketName(), fileDir + fileName, inputStream, objectMetadata);
ret = putResult.getETag();
if (StringUtils.isEmpty(ret)) {
log.error("上传失败,文件ETag为空");
}
ossClient.shutdown();
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 请求类型
*
* @param filenameExtension :
* @return :
*/
private static String getContentType(String filenameExtension) {
if (FileNameSuffixEnum.BMP.getSuffix().equalsIgnoreCase(filenameExtension)) {
return "image/bmp";
}
if (FileNameSuffixEnum.GIF.getSuffix().equalsIgnoreCase(filenameExtension)) {
return "image/gif";
}
if (FileNameSuffixEnum.JPEG.getSuffix().equalsIgnoreCase(filenameExtension) ||
FileNameSuffixEnum.JPG.getSuffix().equalsIgnoreCase(filenameExtension) ||
FileNameSuffixEnum.PNG.getSuffix().equalsIgnoreCase(filenameExtension)) {
return "image/jpeg";
}
if (FileNameSuffixEnum.HTML.getSuffix().equalsIgnoreCase(filenameExtension)) {
return "text/html";
}
if (FileNameSuffixEnum.TXT.getSuffix().equalsIgnoreCase(filenameExtension)) {
return "text/plain";
}
if (FileNameSuffixEnum.VSD.getSuffix().equalsIgnoreCase(filenameExtension)) {
return "application/vnd.visio";
}
if (FileNameSuffixEnum.PPTX.getSuffix().equalsIgnoreCase(filenameExtension) ||
FileNameSuffixEnum.PPT.getSuffix().equalsIgnoreCase(filenameExtension)) {
return "application/vnd.ms-powerpoint";
}
if (FileNameSuffixEnum.DOCX.getSuffix().equalsIgnoreCase(filenameExtension) ||
FileNameSuffixEnum.DOC.getSuffix().equalsIgnoreCase(filenameExtension)) {
return "application/msword";
}
if (FileNameSuffixEnum.XML.getSuffix().equalsIgnoreCase(filenameExtension)) {
return "text/xml";
}
if (FileNameSuffixEnum.PDF.getSuffix().equalsIgnoreCase(filenameExtension)) {
return "application/pdf";
}
return "image/jpeg";
}
}
@Getter
enum FileNameSuffixEnum {
/**
* 文件后缀名
*/
BMP(".bmp", "bmp文件"),
GIF(".gif", "gif文件"),
JPEG(".jpeg", "jpeg文件"),
JPG(".jpg", "jpg文件"),
PNG(".png", "png文件"),
HTML(".html", "HTML文件"),
TXT(".txt", "txt文件"),
VSD(".vsd", "vsd文件"),
PPTX(".pptx", "PPTX文件"),
DOCX(".docx", "DOCX文件"),
PPT(".ppt", "PPT文件"),
DOC(".doc", "DOC文件"),
XML(".xml", "XML文件"),
PDF(".pdf", "PDF文件");
/**
* 后缀名
*/
private final String suffix;
/**
* 描述
*/
private final String description;
FileNameSuffixEnum(String suffix, String description) {
this.suffix = suffix;
this.description = description;
}
}
import javax.annotation.Resource;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
@Component
public class M3u8Component {
@Resource
private FilePath filePath;
public String mediaFileToJavaM3u8(MultipartFile file) throws Exception{
if (file.isEmpty()) {
throw new RuntimeException("未发现文件");
}
log.info("开始解析视频");
long start = System.currentTimeMillis();
//临时目录创建
String path = new File(System.getProperty("user.dir")).getAbsolutePath();
String tempFilePath = path+ filePath.getTempPath();
if (!FileUtil.exist(tempFilePath)) {
FileUtil.mkdir(tempFilePath);
}
String filePathName = tempFilePath + file.getOriginalFilename();
File dest = new File(filePathName);
try {
file.transferTo(dest);
}catch (Exception e){
log.error("视频转m3u8格式存在异常,异常原因e:{}",e.getMessage());
}
//m3u8文件 存储路径
String filePath = m3u8Util.generateFilePath(this.filePath.getBasePath());
if (!FileUtil.exist(filePath)) {
FileUtil.mkdir(filePath);
}
long end = System.currentTimeMillis();
log.info("临时文件上传成功......耗时:{} ms", end - start);
String m3u8FilePath = FfmpegUtil.mp4ToM3u8(filePathName,filePath);
log.info("视频转换已完成 !");
return m3u8FilePath;
}
}
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import java.time.LocalDateTime;
public class m3u8Util {
/**
*@Description 根据基础路径,生成文件存储路径
*@param basePath 基础路径(根路径)
*@Return
*/
public static String generateFilePath(String basePath){
String temp = basePath;
if(StrUtil.isNotBlank(basePath)){
if(basePath.endsWith("/")){
temp = basePath.substring(0,basePath.lastIndexOf("/"));
}
}
return temp+"/"+generateDateDir()+"/";
}
/**
*@Description 根据当前时间,生成下级存储目录
*@Return
*/
public static String generateDateDir(){
LocalDateTime now = LocalDateTime.now();
return DateUtil.format(now, "yyyyMMdd/HH/mm/ss");
}
/**
*@Description 根据文件全路径,获取文件主名称
*@param fullPath 文件全路径(包含文件名)
*@Return
*/
public static String getFileMainName(String fullPath){
String fileName = FileUtil.getName(fullPath);
return fileName.substring(0,fileName.lastIndexOf("."));
}
}
import java.io.BufferedReader;
import java.io.CharArrayWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class FileUtil {
public static void deleteFiles(String path) {
File file = new File(path);
if (file.exists()) {
if (file.isDirectory()) {
File[] temp = file.listFiles(); //获取该文件夹下的所有文件
for (File value : temp) {
deleteFile(value.getAbsolutePath());
}
} else {
file.delete(); //删除子文件
}
file.delete(); //删除文件夹
}
}
public static void deleteFile(String path){
File dest = new File(path);
if (dest.isFile() && dest.exists()) {
dest.delete();
}
}
public static void replaceTextContent(String path,String srcStr,String replaceStr) throws IOException {
// 读
File file = new File(path);
FileReader in = new FileReader(file);
BufferedReader bufIn = new BufferedReader(in);
// 内存流, 作为临时流
CharArrayWriter tempStream = new CharArrayWriter();
// 替换
String line = null;
while ( (line = bufIn.readLine()) != null) {
// 替换每行中, 符合条件的字符串
line = line.replaceAll(srcStr, replaceStr);
// 将该行写入内存
tempStream.write(line);
// 添加换行符
tempStream.append(System.getProperty("line.separator"));
}
// 关闭 输入流
bufIn.close();
// 将内存中的流 写入 文件
FileWriter out = new FileWriter(file);
tempStream.writeTo(out);
out.close();
System.out.println("====path:"+path);
}
}
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.FFmpegLogCallback;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.FrameRecorder;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.UUID;
/**
* javacv ffmpeg 工具类
*/
@Slf4j
public class FfmpegUtil {
/**
* mp4, m3u8
*
* @param filePathName 需要转换文件
* @param toFilePath 需要转换的文件路径
*/
public static String mp4ToM3u8(String filePathName, String toFilePath) throws Exception {
avutil.av_log_set_level(avutil.AV_LOG_INFO);
FFmpegLogCallback.set();
boolean isStart = true;// 该变量建议设置为全局控制变量,用于控制录制结束
//加载文件
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(filePathName);
//grabber.setAudioChannels(1);
grabber.start();
String fileName = UUID.randomUUID().toString().replaceAll("-", "");
File tempFile3 = new File(toFilePath, fileName + ".m3u8");
String prefixName = toFilePath + File.separator + fileName;
//生成加密key
String secureFileName = prefixName + ".key";
byte[] secureRandom = getSecureRandom();
FileUtil.writeBytes(secureRandom,secureFileName);
String toHex = Convert.toHex(secureRandom);
String keyInfoPath = toFilePath + File.separator +"key.keyinfo";
//写入加密文件
writeKeyInfo(keyInfoPath,fileName + ".key",secureFileName,toHex);
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(tempFile3, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
//格式方式
recorder.setFormat("hls");
//关于hls_wrap的说明,hls_wrap表示重复覆盖之前ts切片,这是一个过时配置,ffmpeg官方推荐使用hls_list_size 和hls_flags delete_segments代替hls_wrap
//设置单个ts切片的时间长度(以秒为单位)。默认值为2秒
recorder.setOption("hls_time", "10");
//不根据gop间隔进行切片,强制使用hls_time时间进行切割ts分片
recorder.setOption("hls_flags", "split_by_time");
//设置播放列表条目的最大数量。如果设置为0,则列表文件将包含所有片段,默认值为5
// 当切片的时间不受控制时,切片数量太小,就会有卡顿的现象
recorder.setOption("hls_list_size", "0");
//自动删除切片,如果切片数量大于hls_list_size的数量,则会开始自动删除之前的ts切片,只保留hls_list_size个数量的切片
recorder.setOption("hls_flags", "delete_segments");
//ts切片自动删除阈值,默认值为1,表示早于hls_list_size+1的切片将被删除
recorder.setOption("hls_delete_threshold", "1");
/*hls的切片类型:
* 'mpegts':以MPEG-2传输流格式输出ts切片文件,可以与所有HLS版本兼容。
* 'fmp4':以Fragmented MP4(简称:fmp4)格式输出切片文件,类似于MPEG-DASH,fmp4文件可用于HLS version 7和更高版本。
*/
recorder.setOption("hls_segment_type", "mpegts");
//指定ts切片生成名称规则,按数字序号生成切片,例如'file%03d.ts',就会生成file000.ts,file001.ts,file002.ts等切片文件
//recorder.setOption("hls_segment_filename", toFilePath + "-%03d.ts");
recorder.setOption("hls_segment_filename", toFilePath + File.separator + fileName + "-%5d.ts");
//加密
recorder.setOption("hls_key_info_file", keyInfoPath);
// 设置第一个切片的编号
// recorder.setOption("start_number", String.valueOf(tsCont));
// recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
// 转码
log.info("{} | 启动Hls转码录制器……", toFilePath);
// 设置零延迟
//recorder.setVideoOption("tune", "zerolatency");
recorder.setVideoOption("tune", "fastdecode");
// 快速
recorder.setVideoOption("preset", "ultrafast");
// recorder.setVideoOption("crf", "26");
recorder.setVideoOption("threads", "12");
recorder.setVideoOption("vsync", "2");
recorder.setFrameRate(grabber.getFrameRate());// 设置帧率
// recorder.setGopSize(25);// 设置gop,与帧率相同,相当于间隔1秒chan's一个关键帧
// recorder.setVideoBitrate(100 * 1000);// 码率500kb/s
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
// recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
//如果想截取规定时间段视频
// recorder.start();
// Frame frame;
// while ((frame = grabber.grabImage()) != null) {
// try {
// recorder.record(frame);
// } catch (FrameRecorder.Exception e) {
// log.error("转码异常:{}", e);
// }
// }
recorder.start(grabber.getFormatContext());
AVPacket packet;
while ((packet = grabber.grabPacket()) != null) {
try {
recorder.recordPacket(packet);
} catch (FrameRecorder.Exception e) {
log.error("转码异常:{}", e);
}
}
recorder.setTimestamp(grabber.getTimestamp());
recorder.stop();
recorder.release();
grabber.stop();
grabber.release();
File dest = new File(filePathName);
if (dest.isFile() && dest.exists()) {
dest.delete();
log.warn("临时文件 {}已删除", dest.getName());
}
log.info("转码m3u8:{}", tempFile3.getAbsolutePath());
return tempFile3.getAbsolutePath();
}
/**
* 安全安全随机
*
* @return {@link byte[]}
*/
public static byte[] getSecureRandom(){
byte[] bytes = new byte[16];
new SecureRandom().nextBytes(bytes);
return bytes;
}
/**
* 写入关键文件数据
*
* @param keyInfoPath 路径
* @param decrypt 解密
* @param encrypt 加密
*/
public static void writeKeyInfo(String keyInfoPath,String decrypt,String encrypt,String IV) throws IOException {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(keyInfoPath));){
writer.write(decrypt);
writer.newLine();
writer.write(encrypt);
writer.newLine();
if(StringUtils.isNotBlank(IV)){
writer.write(IV);
}
writer.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
FileService
服务/**
* 文件服务
*
*/
public interface FileService {
/**
* 上传java video2 m3u8
*
* @param file 文件
* @return {@link String}
* @throws Exception 异常
*/
String uploadJavaVideo2M3u8(MultipartFile file) throws Exception;
}
/**
* 文件服务impl
*
*/
@Slf4j
@Service
public class FileServiceImpl implements FileService {
@Autowired
private M3u8Component m3U8ComponentTemplate;
@Autowired
private AliOssProperties aliOssProperties;
@Autowired
private OssComponent ossComponent;
@Autowired
private FilePath filePath;
@Resource(name = "ossUploadTreadPool")
private ThreadPoolTaskExecutor poolTaskExecutor;
private static final String projectUrl = System.getProperty("user.dir").replaceAll("\\\\", "/");
@Override
public String uploadJavaVideo2M3u8(MultipartFile file) throws Exception {
String path = m3U8ComponentTemplate.mediaFileToJavaM3u8(file);
return uploadJava2M3u8(path);
}
public String uploadJava2M3u8(String path) throws Exception {
File pathFile = new File(path);
String realPath = pathFile.getParent();
log.info("视频解析后的 realPath {}", realPath);
String name = pathFile.getName();
log.info("解析后视频 name {}", name);
return uploadFile(path, realPath, name);
}
/**
* 上传文件
*
* @param path 路径
* @param realPath 真正路径
* @param name 名字
* @return {@link String}
* @throws Exception 异常
*/
public String uploadFile(String path, String realPath, String name) throws Exception {
File allFile = new File(realPath);
File[] files = allFile.listFiles();
if (null == files || files.length == 0) {
return null;
}
String patch = DateUtil.format(LocalDateTime.now(), "yyyy/MM/") + name.substring(0, name.lastIndexOf(".")) + "/";
log.info("uploadfile--->path:{}", patch);
List<File> errorFile = new ArrayList<>();
long start = System.currentTimeMillis();
//String fileName = UUID.randomUUID().toString().replaceAll("-","");
//替换m3u8文件中的路径
FileUtil.replaceTextContent(path, name.substring(0, name.lastIndexOf(".")),
aliOssProperties.getMyHostUrl() + filePath.getProxy() + patch +
name.substring(0, name.lastIndexOf(".")));
//开始上传
CountDownLatch countDownLatch = new CountDownLatch(files.length);
Arrays.stream(files).forEach(li -> poolTaskExecutor.execute(() -> {
try (FileInputStream fileInputStream = new FileInputStream(li)) {
//minioComponent.FileUploaderExist("m3u8", patch + li.getName(), fileInputStream);
ossComponent.uploadFile(filePath.getProxy() + patch, fileInputStream, li.getName());
log.info("文件:{} 正在上传", li.getName());
} catch (Exception e) {
errorFile.add(li);
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}));
countDownLatch.await();
long end = System.currentTimeMillis();
log.info("解析文件上传成功,共计:{} 个文件,失败:{},共耗时: {}ms", files.length, errorFile.size(), end - start);
// try {
// minioComponent.mkBucket("m3u8");
// } catch (Exception e) {
// log.error("创建Bucket失败!");
// }
//异步移除所有文件
poolTaskExecutor.execute(() -> {
FileUtil.deleteFile(projectUrl + filePath.getTempPath());
});
if (CollectionUtils.isEmpty(errorFile)) {
return aliOssProperties.getMyHostUrl() + filePath.getProxy() + patch + name;
}
return "";
}
}
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class Result {
public String code ;
public String msg;
public Object data;
public static Result success(String msg,String data){
return new Result().setCode("200").setData(data).setMsg(msg);
}
public static Result success(String msg){
return new Result().setCode("200").setMsg(msg);
}
public static Result fileBuild(){
return new Result().setCode("101");
}
public static Result fileSuccess(String data){
return new Result().setCode("201").setData(data);
}
public static Result error(String msg){
return new Result().setCode("500").setMsg(msg);
}
public static Result error(){
return new Result().setCode("500").setMsg("服务器出错");
}
public static Result fileOver() {
return new Result().setCode("202");
}
}
@Slf4j
@RestController
@RequestMapping("/")
public class TestController {
@Autowired
private FileService fileService;
@PostMapping("/uploadJavaVideo")
public Result uploadJavaVideo(@RequestPart("file") MultipartFile file) {
try {
String path = fileService.uploadJavaVideo2M3u8(file);
if (StringUtils.isNotBlank(path)) {
return Result.success("上传成功",path);
}
}catch (Exception e){
log.error("视频上传转码异常,异常原因e:{}",e);
}
return Result.error("上传失败");
}
}