Forráskód Böngészése

提交视频m3u8切片后上传minio与具体配置。暂时注释有需要可以打开已经测试正常

zhaosongshan 7 hónapja
szülő
commit
f9f2d669d9

+ 6 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/pom.xml

@@ -324,6 +324,12 @@
             <scope>system</scope>
             <systemPath>${project.basedir}/src/main/resources/lib/aspose-pdf-22.4.jar</systemPath>
         </dependency>
+
+        <dependency>
+            <groupId>net.bramp.ffmpeg</groupId>
+            <artifactId>ffmpeg</artifactId>
+            <version>0.6.2</version>
+        </dependency>
     </dependencies>
 
     <repositories>

+ 54 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/config/m3u8/FFmpegConfig.java

@@ -0,0 +1,54 @@
+package vip.xiaonuo.disk.config.m3u8;
+
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import net.bramp.ffmpeg.FFmpeg;
+import net.bramp.ffmpeg.FFprobe;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.annotation.Resource;
+import java.io.File;
+
+//@Configuration
+@Slf4j
+public class FFmpegConfig {
+
+    //@Resource
+    private FilePath filePath;
+
+    @SneakyThrows
+    //@Bean
+    public FFmpeg fFmpeg() {
+        String path = "";
+        if (isLinux()){
+            path+=filePath.getFfmpegPathLinux() + File.separator + "ffmpeg";
+        }else if (isWindows()){
+            path+=filePath.getFfmpegPathWin() + File.separator + "ffmpeg.exe";
+        }
+        log.info("ffmpeg 路径为{}",path);
+        return new FFmpeg(path);
+    }
+
+    @SneakyThrows
+    //@Bean
+    public FFprobe fFprobe() {
+        String path = "";
+        if (isLinux()){
+            path+=filePath.getFfmpegPathLinux() + File.separator + "ffprobe";
+        }else if (isWindows()){
+            path+=filePath.getFfmpegPathWin() + File.separator + "ffprobe.exe";
+        }
+        log.info("ffprobe 路径为{}",path);
+        return new FFprobe(path);
+    }
+    public static boolean isLinux() {
+        return System.getProperty("os.name").toLowerCase().contains("linux");
+    }
+
+    public static boolean isWindows() {
+        return System.getProperty("os.name").toLowerCase().contains("windows");
+    }
+
+
+}

+ 33 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/config/m3u8/FilePath.java

@@ -0,0 +1,33 @@
+package vip.xiaonuo.disk.config.m3u8;
+
+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 = "D:\\file\\tmp\\";
+
+    /**
+     * m3u8文件转换后,储存的根路径
+     */
+    private String basePath = "D:\\file\\m3u8\\";
+
+    /**
+     * m3u8文件转换后,储存的根路径
+     */
+    private String bigPath = "D:\\file\\big\\";
+
+    private String proxy = "m3u8/";
+
+    private String proxyURL= "http://127.0.0.1:8080/";
+
+    private String ffmpegPathWin = "C:\\Program Files\\ffmpeg\\bin";
+
+    private String ffmpegPathLinux = "ffmpeg";
+}

+ 128 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/config/m3u8/M3u8Component.java

@@ -0,0 +1,128 @@
+package vip.xiaonuo.disk.config.m3u8;
+
+import cn.hutool.core.io.FileUtil;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import net.bramp.ffmpeg.FFmpeg;
+import net.bramp.ffmpeg.FFmpegExecutor;
+import net.bramp.ffmpeg.FFprobe;
+import net.bramp.ffmpeg.builder.FFmpegBuilder;
+import net.bramp.ffmpeg.probe.FFmpegProbeResult;
+import net.bramp.ffmpeg.probe.FFmpegStream;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+import vip.xiaonuo.disk.util.m3u8Util;
+import javax.annotation.Resource;
+import java.io.File;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+
+@Slf4j
+//@Component
+public class M3u8Component {
+
+    //@Resource
+    private FFmpeg ffmpeg;
+
+    //@Resource
+    private FFprobe ffprobe;
+
+    //@Resource
+    private FilePath filePath;
+
+
+
+    /**
+     * 视频文件转 m3u8
+     * 支持: .mp4 | .flv | .avi | .mov | .wmv | .wav
+     * @param file 视频文件
+     * @return 路径
+     */
+    public String mediaFileToM3u8(MultipartFile file){
+        if (file.isEmpty()) {
+            throw new RuntimeException("未发现文件");
+        }
+        log.info("开始解析视频");
+        long start = System.currentTimeMillis();
+        //临时目录创建
+        String tempFilePath = 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());
+        }
+        long end = System.currentTimeMillis();
+        log.info("临时文件上传成功......耗时:{} ms", end - start);
+        String m3u8FilePath = localFileToM3u8(filePathName);
+        log.info("视频转换已完成 !");
+        return m3u8FilePath;
+    }
+
+    /**
+     * 本地媒体资源转换
+     * @param filePathName : 文件路径
+     * @return :
+     */
+    @SneakyThrows
+    public String localFileToM3u8(String filePathName) {
+        long startTime = System.currentTimeMillis();
+        final FFmpegProbeResult probe = ffprobe.probe(filePathName);
+        final List<FFmpegStream> streams = probe.getStreams().stream().filter(fFmpegStream -> fFmpegStream.codec_type != null).collect(Collectors.toList());
+        final Optional<FFmpegStream> audioStream = streams.stream().filter(fFmpegStream -> FFmpegStream.CodecType.AUDIO.equals(fFmpegStream.codec_type)).findFirst();
+        final Optional<FFmpegStream> videoStream = streams.stream().filter(fFmpegStream -> FFmpegStream.CodecType.VIDEO.equals(fFmpegStream.codec_type)).findFirst();
+
+        if (!audioStream.isPresent()) {
+            log.error("未发现音频流");
+        }
+        if (!videoStream.isPresent()) {
+            log.error("未发现视频流");
+        }
+        //m3u8文件 存储路径
+        String filePath = m3u8Util.generateFilePath(this.filePath.getBasePath());
+        if (!FileUtil.exist(filePath)) {
+            FileUtil.mkdir(filePath);
+        }
+        String mainName = m3u8Util.getFileMainName(filePathName);
+        String m3u8FileName = filePath + mainName + ".m3u8";
+
+        //下面这一串参数别乱动,经过调优的,1G视频大概需要10秒左右,如果是大佬随意改
+        //"-vsync", "2", "-c:v", "copy", "-c:a", "copy", "-tune", "fastdecode", "-hls_wrap", "0", "-hls_time", "10", "-hls_list_size", "0", "-threads", "12"
+        FFmpegBuilder builder = new FFmpegBuilder()
+                .setInput(filePathName)
+                .overrideOutputFiles(true)
+                .addOutput(m3u8FileName)//输出文件
+                .setFormat(probe.getFormat().format_name) //"mp4"
+                .setAudioBitRate(audioStream.map(fFmpegStream -> fFmpegStream.bit_rate).orElse(0L))
+                .setAudioChannels(1)
+                .setAudioCodec("aac")        // using the aac codec
+                .setAudioSampleRate(audioStream.get().sample_rate)
+                .setAudioBitRate(audioStream.get().bit_rate)
+                .setStrict(FFmpegBuilder.Strict.STRICT)
+                .setFormat("hls")
+                .setPreset("ultrafast")
+                .addExtraArgs("-vsync", "2", "-c:v", "copy", "-c:a", "copy", "-tune", "fastdecode", "-hls_time", "10", "-hls_list_size", "0", "-threads", "12")
+                .done();
+
+        FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
+        // Run a one-pass encode
+        executor.createJob(builder).run();
+
+        File dest = new File(filePathName);
+        if (dest.isFile() && dest.exists()) {
+            dest.delete();
+            System.gc();
+            log.warn("临时文件 {}已删除", dest.getName());
+        }
+        long endTime = System.currentTimeMillis();
+        log.info("文件:{} 转换完成!共耗时{} ms", dest.getName(), (endTime - startTime));
+        return m3u8FileName;
+    }
+
+}

+ 32 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/config/m3u8/SpringAsyncConfig.java

@@ -0,0 +1,32 @@
+package vip.xiaonuo.disk.config.m3u8;
+
+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 {
+    /**
+     * 线程池参数根据minIO设置,如果开启线程太多会被MinIO拒绝
+     * @return :
+     */
+    @Bean("minIOUploadTreadPool")
+    public ThreadPoolTaskExecutor  asyncServiceExecutorForMinIo() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        // 设置核心线程数,采用IO密集 h/(1-拥塞)
+        executor.setCorePoolSize(6);
+        // 设置最大线程数,由于minIO连接数量有限,此处尽力设计大点
+        executor.setMaxPoolSize(500);
+        // 设置线程活跃时间(秒)
+        executor.setKeepAliveSeconds(30);
+        // 设置默认线程名称
+        executor.setThreadNamePrefix("minio-upload-task-");
+        // 等待所有任务结束后再关闭线程池
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+        //执行初始化
+        executor.initialize();
+        return executor;
+    }
+}

+ 12 - 1
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/controller/FileController.java

@@ -26,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.util.ClassUtils;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 import vip.xiaonuo.auth.core.util.StpLoginUserUtil;
 import vip.xiaonuo.disk.component.AsyncTaskComp;
 import vip.xiaonuo.disk.component.FileDealComp;
@@ -36,6 +37,7 @@ import vip.xiaonuo.disk.dto.file.*;
 import vip.xiaonuo.disk.io.QiwenFile;
 import vip.xiaonuo.disk.service.IFileService;
 import vip.xiaonuo.disk.service.IUserFileService;
+import vip.xiaonuo.disk.service.M3u8UploadService;
 import vip.xiaonuo.disk.util.OperationLogUtil;
 import vip.xiaonuo.disk.util.QiwenFileUtil;
 import vip.xiaonuo.disk.util.TreeNode;
@@ -82,7 +84,8 @@ public class FileController {
     @Value("${common.account}")
     private String commonUserId;
 
-
+    @Resource
+    private M3u8UploadService m3u8UploadService;
 
 
     public static Executor executor = Executors.newFixedThreadPool(20);
@@ -1077,4 +1080,12 @@ public class FileController {
         return RestResult.success().data(vo);
     }
 
+
+    @Operation(summary = "m3u8格式转换测试", description = "m3u8格式转换测试", tags = {"file"})
+    @RequestMapping(value = "/m3u8Upload", method = RequestMethod.GET)
+    @ResponseBody
+    public RestResult<FileDetailVO> m3u8Upload(
+            @Parameter(description = "视频文件", required = true) MultipartFile file){
+        return RestResult.success().data(m3u8UploadService.m3u8Upload(file));
+    }
 }

+ 7 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/service/M3u8UploadService.java

@@ -0,0 +1,7 @@
+package vip.xiaonuo.disk.service;
+
+import org.springframework.web.multipart.MultipartFile;
+
+public interface M3u8UploadService {
+    String m3u8Upload(MultipartFile file);
+}

+ 128 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/service/impl/M3u8UploadServiceImpl.java

@@ -0,0 +1,128 @@
+package vip.xiaonuo.disk.service.impl;
+
+import cn.hutool.core.date.DateUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.multipart.MultipartFile;
+import vip.xiaonuo.disk.config.m3u8.FilePath;
+import vip.xiaonuo.disk.config.m3u8.M3u8Component;
+import vip.xiaonuo.disk.service.M3u8UploadService;
+import vip.xiaonuo.disk.util.FileUtil;
+import vip.xiaonuo.disk.util.MinioUtil;
+import javax.annotation.Resource;
+import java.io.File;
+import java.io.FileInputStream;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * @PackageName:vip.xiaonuo.exam.service.impl
+ * @ClassName:M3u8UploadServiceImpl
+ * @Author ZSS
+ * @Date 2025-07-14 18:33
+ * @Note: m3u8上传服务实现类
+ **/
+@Service
+@Slf4j
+public class M3u8UploadServiceImpl implements M3u8UploadService {
+
+    @Resource
+    private FilePath filePath;
+
+    @Autowired
+    private MinioUtil minioUtil;
+
+    @Resource(name = "minIOUploadTreadPool")
+    private ThreadPoolTaskExecutor poolTaskExecutor;
+
+    @Resource
+    private M3u8Component m3U8ComponentTemplate;
+
+    @Override
+    public String m3u8Upload(MultipartFile file) {
+        return mediaFileToM3u8(file);
+    }
+
+
+    /**
+     * 上传视频转m3u8后上传至minIO
+     */
+    public String mediaFileToM3u8(MultipartFile file){
+        String paths = m3U8ComponentTemplate.mediaFileToM3u8(file);
+        return upload2M3u8(paths);
+    }
+
+    /**
+     * 本地视频转m3u8后上传至minIO
+     */
+    public String localVideo2M3u8(String path){
+        String paths = m3U8ComponentTemplate.localFileToM3u8(path);
+        return upload2M3u8(paths);
+    }
+
+
+
+    /**
+     * 上传转码后得视频至OSS或minIOn
+     * @return 路径
+     */
+    public String upload2M3u8(String path) {
+        try {
+            //存储转码后文件
+            String realPath = path.substring(0, path.lastIndexOf(File.separator));
+            log.info("视频解析后的 realPath {}", realPath);
+            String name = path.substring(path.lastIndexOf(File.separator) + 1);
+            log.info("解析后视频 name {}", name);
+            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(".")) + "/";
+            List<File> errorFile = new ArrayList<>();
+
+            long start = System.currentTimeMillis();
+            //替换m3u8文件中的路径
+            FileUtil.replaceTextContent(path, name.substring(0, name.lastIndexOf(".")),
+                    filePath.getProxyURL() + 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)) {
+                    minioUtil.FileUploaderExist("m3u8", patch + li.getName(), fileInputStream);
+                    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);
+
+            //异步移除所有文件
+            poolTaskExecutor.execute(() -> {
+                FileUtil.deleteFolder(realPath);
+            });
+            if (CollectionUtils.isEmpty(errorFile)) {
+                String resultUrl = filePath.getProxyURL() + filePath.getProxy() + patch + name;
+                resultUrl = resultUrl.replaceAll("\\\\", "/");
+                return resultUrl;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return "";
+    }
+
+
+}

+ 61 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/util/FileUtil.java

@@ -0,0 +1,61 @@
+package vip.xiaonuo.disk.util;
+
+import java.io.*;
+
+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 deleteFolder(String folderPath){
+        if(cn.hutool.core.io.FileUtil.exist(folderPath)){
+            cn.hutool.core.io.FileUtil.del(folderPath);
+        }
+    }
+
+    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);
+
+    }
+}

+ 22 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/util/MinioUtil.java

@@ -533,4 +533,26 @@ public class MinioUtil {
 
     }
 
+    /**
+     * 使用putObject上传一个文件到文件分类
+     *
+     * @param bucket   : bucket名称
+     * @param fileName : 文件名
+     * @param stream   : 文件流
+     * @throws Exception : 异常
+     */
+    public void FileUploaderExist(String bucket, String fileName, InputStream stream) throws Exception {
+        PutObjectArgs.Builder putObjectArgsBuilder = PutObjectArgs.builder()
+                .bucket(bucket.toLowerCase())
+                .object(fileName)
+                .stream(stream, stream.available(), -1);
+        if (fileName.contains(".m3u8")) {
+            putObjectArgsBuilder.contentType("application/vnd.apple.mpegurl");
+        } else if (fileName.contains(".ts")) {
+            putObjectArgsBuilder.contentType("video/MP2T");
+        } else {
+            putObjectArgsBuilder.contentType("application/octet-stream");
+        }
+        minioClient.putObject(putObjectArgsBuilder.build());
+    }
 }

+ 52 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/util/m3u8Util.java

@@ -0,0 +1,52 @@
+package vip.xiaonuo.disk.util;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.io.File;
+import java.time.LocalDateTime;
+
+/**
+ * @Description 工具类
+ * @Author sirwsl
+ * @Version 1.0
+ */
+public class m3u8Util {
+
+    /**
+    *@Description 根据基础路径,生成文件存储路径
+    *@param basePath 基础路径(根路径)
+    *@Return
+    */
+    public static String generateFilePath(String basePath){
+        String temp = basePath;
+        if(StrUtil.isNotBlank(basePath)){
+            if(basePath.endsWith(File.separator)){
+                temp = basePath.substring(0,basePath.lastIndexOf(File.separator));
+            }
+        }
+        return temp + File.separator + generateDateDir() + File.separator;
+    }
+
+    /**
+     *@Description 根据当前时间,生成下级存储目录
+     *@Return
+     */
+    public static String generateDateDir(){
+        LocalDateTime now = LocalDateTime.now();
+        return DateUtil.format(now, "yyyyMMdd"+File.separator+"HH"+File.separator+"mm"+File.separator+"ss"+File.separator+"SSS");
+    }
+
+    /**
+     *@Description 根据文件全路径,获取文件主名称
+     *@param fullPath 文件全路径(包含文件名)
+     *@Return
+     */
+    public static String getFileMainName(String fullPath){
+        String fileName = FileUtil.getName(fullPath);
+        return fileName.substring(0,fileName.lastIndexOf("."));
+    }
+
+
+}