Browse Source

集成视频格式转换功能1.0(基于javacv)

zhangxiaoxiao9527 4 years ago
parent
commit
bcdb5ce0e6

+ 58 - 0
server/pom.xml

@@ -192,6 +192,64 @@
             <artifactId>galimatias</artifactId>
             <version>0.2.1</version>
         </dependency>
+
+        <!-- 以下是bytedeco 基于opencv ffmpeg封装的javacv,用于视频处理 -->
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>javacv</artifactId>
+            <version>1.5.2</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>javacpp</artifactId>
+            <version>1.5.2</version>
+        </dependency>
+
+        <!-- 此版本中主要兼容linux和windows系统,如需兼容其他系统平台,请引入对应依赖即可 -->
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>opencv</artifactId>
+            <version>4.1.2-1.5.2</version>
+            <classifier>linux-x86_64</classifier>
+        </dependency>
+
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>opencv</artifactId>
+            <version>4.1.2-1.5.2</version>
+            <classifier>windows-x86_64</classifier>
+        </dependency>
+
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>openblas</artifactId>
+            <version>0.3.6-1.5.1</version>
+            <classifier>linux-x86_64</classifier>
+        </dependency>
+
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>openblas</artifactId>
+            <version>0.3.6-1.5.1</version>
+            <classifier>windows-x86_64</classifier>
+        </dependency>
+
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>ffmpeg</artifactId>
+            <version>4.2.1-1.5.2</version>
+            <classifier>linux-x86_64</classifier>
+        </dependency>
+
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>ffmpeg</artifactId>
+            <version>4.2.1-1.5.2</version>
+            <classifier>windows-x86_64</classifier>
+        </dependency>
+
+
     </dependencies>
 
     <build>

+ 5 - 0
server/src/main/config/application.properties

@@ -55,6 +55,11 @@ cache.enabled = ${KK_CACHE_ENABLED:true}
 simText = ${KK_SIMTEXT:txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd}
 #多媒体类型,默认如下,可自定义添加
 media = ${KK_MEDIA:mp3,wav,mp4,flv}
+#是否开启多媒体类型转视频格式转换,目前可转换视频格式有:avi,mov,wmv,3gp,rm
+#请谨慎开启此功能,建议异步调用添加到处理队列,并且增加任务队列处理线程,防止视频转换占用完线程资源,转换比较耗费时间,并且控制了只能串行处理转换任务
+media.convert.disable = ${KK_MEDIA_CONVERT_DISABLE:false}
+#支持转换的视频类型
+convertMedias = ${KK_CONVERTMEDIAS:avi,mov,wmv,mkv,3gp,rm}
 #office类型文档(word ppt)样式,默认为图片(image),可配置为pdf(预览时也有按钮切换)
 office.preview.type = ${KK_OFFICE_PREVIEW_TYPE:image}
 #是否关闭office预览切换开关,默认为false,可配置为true关闭

+ 29 - 0
server/src/main/java/cn/keking/config/ConfigConstants.java

@@ -24,6 +24,8 @@ public class ConfigConstants {
     private static Boolean cacheEnabled;
     private static String[] simTexts = {};
     private static String[] medias = {};
+    private static String[] convertMedias = {};
+    private static String mediaConvertDisable;
     private static String officePreviewType;
     private static String officePreviewSwitchDisabled;
     private static String ftpUsername;
@@ -89,6 +91,33 @@ public class ConfigConstants {
         ConfigConstants.medias = Media;
     }
 
+    public static String[] getConvertMedias() {
+        return convertMedias;
+    }
+
+    @Value("${convertMedias:avi,mov,wmv,mkv,3gp,rm}")
+    public void setConvertMedias(String convertMedia) {
+        String[] mediaArr = convertMedia.split(",");
+        setConvertMediaValue(mediaArr);
+    }
+
+    public static void setConvertMediaValue(String[] ConvertMedia) {
+        ConfigConstants.convertMedias = ConvertMedia;
+    }
+
+    public static String getMediaConvertDisable() {
+        return mediaConvertDisable;
+    }
+
+
+    @Value("${media.convert.disable:true}")
+    public void setMediaConvertDisable(String mediaConvertDisable) {
+        setMediaConvertDisableValue(mediaConvertDisable);
+    }
+    public static void setMediaConvertDisableValue(String mediaConvertDisable) {
+        ConfigConstants.mediaConvertDisable = mediaConvertDisable;
+    }
+
     public static String getOfficePreviewType() {
         return officePreviewType;
     }

+ 4 - 0
server/src/main/java/cn/keking/model/FileType.java

@@ -33,6 +33,7 @@ public enum FileType {
     private static final String[] SSIM_TEXT_TYPES = ConfigConstants.getSimText();
     private static final String[] CODES = {"java", "c", "php", "go", "python", "py", "js", "html", "ftl", "css", "lua", "sh", "rb", "yml", "json", "h", "cpp", "cs", "aspx", "jsp"};
     private static final String[] MEDIA_TYPES = ConfigConstants.getMedia();
+    public static final String[] MEDIA_TYPES_CONVERT = ConfigConstants.getConvertMedias();
     private static final Map<String, FileType> FILE_TYPE_MAPPER = new HashMap<>();
 
     static {
@@ -51,6 +52,9 @@ public enum FileType {
         for (String media : MEDIA_TYPES) {
             FILE_TYPE_MAPPER.put(media, FileType.MEDIA);
         }
+        for (String media : MEDIA_TYPES_CONVERT) {
+            FILE_TYPE_MAPPER.put(media, FileType.MEDIA);
+        }
         for (String tif : TIFF_TYPES) {
             FILE_TYPE_MAPPER.put(tif, FileType.TIFF);
         }

+ 23 - 0
server/src/main/java/cn/keking/service/FileHandlerService.java

@@ -286,4 +286,27 @@ public class FileHandlerService {
         }
         return attribute;
     }
+
+    /**
+     * @return 已转换过的视频文件集合(缓存)
+     */
+    public Map<String, String> listConvertedMedias() {
+        return cacheService.getMediaConvertCache();
+    }
+
+    /**
+     * 添加转换后的视频文件缓存
+     * @param fileName
+     * @param value
+     */
+    public void addConvertedMedias(String fileName, String value) {
+        cacheService.putMediaConvertCache(fileName, value);
+    }
+
+    /**
+     * @return 已转换视频文件缓存,根据文件名获取
+     */
+    public String getConvertedMedias(String key) {
+        return cacheService.getMediaConvertCache(key);
+    }
 }

+ 6 - 0
server/src/main/java/cn/keking/service/cache/CacheService.java

@@ -12,15 +12,18 @@ public interface CacheService {
     String FILE_PREVIEW_PDF_KEY = "converted-preview-pdf-file";
     String FILE_PREVIEW_IMGS_KEY = "converted-preview-imgs-file";//压缩包内图片文件集合
     String FILE_PREVIEW_PDF_IMGS_KEY = "converted-preview-pdfimgs-file";
+    String FILE_PREVIEW_MEDIA_CONVERT_KEY = "converted-preview-media-file";
     String TASK_QUEUE_NAME = "convert-task";
 
     Integer DEFAULT_PDF_CAPACITY = 500000;
     Integer DEFAULT_IMG_CAPACITY = 500000;
     Integer DEFAULT_PDFIMG_CAPACITY = 500000;
+    Integer DEFAULT_MEDIACONVERT_CAPACITY = 500000;
 
     void initPDFCachePool(Integer capacity);
     void initIMGCachePool(Integer capacity);
     void initPdfImagesCachePool(Integer capacity);
+    void initMediaConvertCachePool(Integer capacity);
     void putPDFCache(String key, String value);
     void putImgCache(String key, List<String> value);
     Map<String, String> getPDFCache();
@@ -29,6 +32,9 @@ public interface CacheService {
     List<String> getImgCache(String key);
     Integer getPdfImageCache(String key);
     void putPdfImageCache(String pdfFilePath, int num);
+    Map<String, String> getMediaConvertCache();
+    void putMediaConvertCache(String key, String value);
+    String getMediaConvertCache(String key);
     void cleanCache();
     void addQueueTask(String url);
     String takeQueueTask() throws InterruptedException;

+ 25 - 0
server/src/main/java/cn/keking/service/cache/impl/CacheServiceJDKImpl.java

@@ -26,6 +26,7 @@ public class CacheServiceJDKImpl implements CacheService {
     private Map<String, String> pdfCache;
     private Map<String, List<String>> imgCache;
     private Map<String, Integer> pdfImagesCache;
+    private Map<String, String> mediaConvertCache;
     private static final int QUEUE_SIZE = 500000;
     private final BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(QUEUE_SIZE);
 
@@ -34,6 +35,7 @@ public class CacheServiceJDKImpl implements CacheService {
         initPDFCachePool(CacheService.DEFAULT_PDF_CAPACITY);
         initIMGCachePool(CacheService.DEFAULT_IMG_CAPACITY);
         initPdfImagesCachePool(CacheService.DEFAULT_PDFIMG_CAPACITY);
+        initMediaConvertCachePool(CacheService.DEFAULT_MEDIACONVERT_CAPACITY);
     }
 
     @Override
@@ -79,6 +81,21 @@ public class CacheServiceJDKImpl implements CacheService {
         pdfImagesCache.put(pdfFilePath, num);
     }
 
+    @Override
+    public Map<String, String> getMediaConvertCache() {
+        return mediaConvertCache;
+    }
+
+    @Override
+    public void putMediaConvertCache(String key, String value) {
+        mediaConvertCache.put(key, value);
+    }
+
+    @Override
+    public String getMediaConvertCache(String key) {
+        return mediaConvertCache.get(key);
+    }
+
     @Override
     public void cleanCache() {
         initPDFCachePool(CacheService.DEFAULT_PDF_CAPACITY);
@@ -116,4 +133,12 @@ public class CacheServiceJDKImpl implements CacheService {
                 .maximumWeightedCapacity(capacity).weigher(Weighers.singleton())
                 .build();
     }
+
+    @Override
+    public void initMediaConvertCachePool(Integer capacity) {
+        mediaConvertCache = new ConcurrentLinkedHashMap.Builder<String, String>()
+                .maximumWeightedCapacity(capacity).weigher(Weighers.singleton())
+                .build();
+    }
+
 }

+ 22 - 0
server/src/main/java/cn/keking/service/cache/impl/CacheServiceRedisImpl.java

@@ -34,6 +34,11 @@ public class CacheServiceRedisImpl implements CacheService {
     @Override
     public void initPdfImagesCachePool(Integer capacity) { }
 
+    @Override
+    public void initMediaConvertCachePool(Integer capacity) {
+
+    }
+
     @Override
     public void putPDFCache(String key, String value) {
         RMapCache<String, String> convertedList = redissonClient.getMapCache(FILE_PREVIEW_PDF_KEY);
@@ -80,6 +85,23 @@ public class CacheServiceRedisImpl implements CacheService {
         convertedList.fastPut(pdfFilePath, num);
     }
 
+    @Override
+    public Map<String, String> getMediaConvertCache() {
+        return redissonClient.getMapCache(FILE_PREVIEW_MEDIA_CONVERT_KEY);
+    }
+
+    @Override
+    public void putMediaConvertCache(String key, String value) {
+        RMapCache<String, String> convertedList = redissonClient.getMapCache(FILE_PREVIEW_MEDIA_CONVERT_KEY);
+        convertedList.fastPut(key, value);
+    }
+
+    @Override
+    public String getMediaConvertCache(String key) {
+        RMapCache<String, String> convertedList = redissonClient.getMapCache(FILE_PREVIEW_MEDIA_CONVERT_KEY);
+        return convertedList.get(key);
+    }
+
     @Override
     public void cleanCache() {
         cleanPdfCache();

+ 39 - 0
server/src/main/java/cn/keking/service/cache/impl/CacheServiceRocksDBImpl.java

@@ -73,6 +73,11 @@ public class CacheServiceRocksDBImpl implements CacheService {
 
     }
 
+    @Override
+    public void initMediaConvertCachePool(Integer capacity) {
+
+    }
+
     @Override
     public void putPDFCache(String key, String value) {
         try {
@@ -171,6 +176,40 @@ public class CacheServiceRocksDBImpl implements CacheService {
         }
     }
 
+    @Override
+    public Map<String, String> getMediaConvertCache() {
+        Map<String, String> result = new HashMap<>();
+        try{
+            result = (Map<String, String>) toObject(db.get(FILE_PREVIEW_MEDIA_CONVERT_KEY.getBytes()));
+        } catch (RocksDBException | IOException | ClassNotFoundException e) {
+            LOGGER.error("Get from RocksDB Exception" + e);
+        }
+        return result;
+    }
+
+    @Override
+    public void putMediaConvertCache(String key, String value) {
+        try {
+            Map<String, String> mediaConvertCacheItem = getMediaConvertCache();
+            mediaConvertCacheItem.put(key, value);
+            db.put(FILE_PREVIEW_MEDIA_CONVERT_KEY.getBytes(), toByteArray(mediaConvertCacheItem));
+        } catch (RocksDBException | IOException e) {
+            LOGGER.error("Put into RocksDB Exception" + e);
+        }
+    }
+
+    @Override
+    public String getMediaConvertCache(String key) {
+        String result = "";
+        try{
+            Map<String, String> map = (Map<String, String>) toObject(db.get(FILE_PREVIEW_MEDIA_CONVERT_KEY.getBytes()));
+            result = map.get(key);
+        } catch (RocksDBException | IOException | ClassNotFoundException e) {
+            LOGGER.error("Get from RocksDB Exception" + e);
+        }
+        return result;
+    }
+
     @Override
     public void cleanCache() {
         try {

+ 122 - 3
server/src/main/java/cn/keking/service/impl/MediaFilePreviewImpl.java

@@ -1,13 +1,21 @@
 package cn.keking.service.impl;
 
+import cn.keking.config.ConfigConstants;
 import cn.keking.model.FileAttribute;
+import cn.keking.model.FileType;
 import cn.keking.model.ReturnResponse;
 import cn.keking.service.FilePreview;
 import cn.keking.utils.DownloadUtils;
 import cn.keking.service.FileHandlerService;
 import cn.keking.web.filter.BaseUrlFilter;
+import org.artofsolving.jodconverter.util.ConfigUtils;
+import org.bytedeco.ffmpeg.global.avcodec;
+import org.bytedeco.javacv.FFmpegFrameGrabber;
+import org.bytedeco.javacv.FFmpegFrameRecorder;
+import org.bytedeco.javacv.Frame;
 import org.springframework.stereotype.Service;
 import org.springframework.ui.Model;
+import java.io.File;
 
 /**
  * @author : kl
@@ -21,6 +29,8 @@ public class MediaFilePreviewImpl implements FilePreview {
     private final FileHandlerService fileHandlerService;
     private final OtherFilePreviewImpl otherFilePreview;
 
+    private static Object LOCK=new Object();
+
     public MediaFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
         this.fileHandlerService = fileHandlerService;
         this.otherFilePreview = otherFilePreview;
@@ -34,14 +44,123 @@ public class MediaFilePreviewImpl implements FilePreview {
             if (response.isFailure()) {
                 return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
             } else {
-                model.addAttribute("mediaUrl", BaseUrlFilter.getBaseUrl() + fileHandlerService.getRelativePath(response.getContent()));
+                url=BaseUrlFilter.getBaseUrl() + fileHandlerService.getRelativePath(response.getContent());
+                fileAttribute.setUrl(url);
             }
-        } else {
-            model.addAttribute("mediaUrl", url);
+        }
+
+        if(checkNeedConvert(fileAttribute.getSuffix())){
+            url=convertUrl(fileAttribute);
+        }else{
+            //正常media类型
+            String[] medias = ConfigConstants.getMedia();
+            for(String media:medias){
+                if(media.equals(fileAttribute.getSuffix())){
+                    model.addAttribute("mediaUrl", url);
+                    return MEDIA_FILE_PREVIEW_PAGE;
+                }
+            }
+            return otherFilePreview.notSupportedFile(model, fileAttribute, "暂不支持");
         }
         model.addAttribute("mediaUrl", url);
         return MEDIA_FILE_PREVIEW_PAGE;
     }
 
+    /**
+     * 检查视频文件处理逻辑
+     * 返回处理过后的url
+     * @return url
+     */
+    private String convertUrl(FileAttribute fileAttribute) {
+        String url = fileAttribute.getUrl();
+        if(fileHandlerService.listConvertedMedias().containsKey(url)){
+            url= fileHandlerService.getConvertedMedias(url);
+        }else{
+            if(!fileHandlerService.listConvertedMedias().containsKey(url)){
+                synchronized(LOCK){
+                    if(!fileHandlerService.listConvertedMedias().containsKey(url)){
+                        String convertedUrl=convertToMp4(fileAttribute);
+                        //加入缓存
+                        fileHandlerService.addConvertedMedias(url,convertedUrl);
+                        url=convertedUrl;
+                    }
+                }
+            }
+        }
+        return url;
+    }
+
+    /**
+     * 检查视频文件转换是否已开启,以及当前文件是否需要转换
+     * @return
+     */
+    private boolean checkNeedConvert(String suffix) {
+        //1.检查开关是否开启
+        if("false".equals(ConfigConstants.getMediaConvertDisable())){
+            return false;
+        }
+        //2.检查当前文件是否需要转换
+        String[] mediaTypesConvert = FileType.MEDIA_TYPES_CONVERT;
+        String type = suffix;
+        for(String temp : mediaTypesConvert){
+            if(type.equals(temp)){
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 将浏览器不兼容视频格式转换成MP4
+     * @param fileAttribute
+     * @return
+     */
+    private static String convertToMp4(FileAttribute fileAttribute) {
+
+        //说明:这里做临时处理,取上传文件的目录
+        String homePath = ConfigUtils.getHomePath();
+        String filePath = homePath+File.separator+"file"+File.separator+"demo"+File.separator+fileAttribute.getName();
+        String convertFileName=fileAttribute.getUrl().replace(fileAttribute.getSuffix(),"mp4");
+
+        File file=new File(filePath);
+        FFmpegFrameGrabber frameGrabber = new FFmpegFrameGrabber(file);
+        String fileName = null;
+        Frame captured_frame = null;
+        FFmpegFrameRecorder recorder = null;
+        try {
+            fileName = file.getAbsolutePath().replace(fileAttribute.getSuffix(),"mp4");
+            File desFile=new File(fileName);
+            //判断一下防止穿透缓存
+            if(desFile.exists()){
+                return fileName;
+            }
+
+            frameGrabber.start();
+            recorder = new FFmpegFrameRecorder(fileName, frameGrabber.getImageWidth(), frameGrabber.getImageHeight(), frameGrabber.getAudioChannels());
+            recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); //avcodec.AV_CODEC_ID_H264  //AV_CODEC_ID_MPEG4
+            recorder.setFormat("mp4");
+            recorder.setFrameRate(frameGrabber.getFrameRate());
+            //recorder.setSampleFormat(frameGrabber.getSampleFormat()); //
+            recorder.setSampleRate(frameGrabber.getSampleRate());
 
+            recorder.setAudioChannels(frameGrabber.getAudioChannels());
+            recorder.setFrameRate(frameGrabber.getFrameRate());
+            recorder.start();
+            while ((captured_frame = frameGrabber.grabFrame()) != null) {
+                try {
+                    recorder.setTimestamp(frameGrabber.getTimestamp());
+                    recorder.record(captured_frame);
+                } catch (Exception e) {
+                }
+            }
+            recorder.stop();
+            recorder.release();
+            frameGrabber.stop();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        //是否删除源文件
+        //file.delete();
+        return convertFileName;
+    }
 }