springboot 通过javaCV 实现mp4转m3u8 上传oss

配置依赖

 	<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-platformjavacv-platform 版本要对应

application.yml 项目配置


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: 

configproperties配置相关类

SpringAsyncConfig 配置信息


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;
    }
}

FilePath 配置信息


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/";

}

AliOssProperties 配置信息


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 包 相关组件类

OssComponent 组件



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;
    }
}

M3u8Component 组件

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;
    }
}

工具包

m3u8Util 工具类


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


}

FileUtil 工具类


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);

    }
}

FfmpegUtil 工具类


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);
        }
    }
}

创建service包 并创建 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 "";
    }
}

Result 通用返回类


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

创建测试 TestController


@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("上传失败");
    }
}

你可能感兴趣的:(JAVA,SpringBoot,spring,boot,ffmpeg,java)