Prechádzať zdrojové kódy

添加 media 模块

reghao 3 rokov pred
rodič
commit
027e0d5c6f

+ 43 - 0
media/pom.xml

@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>jutil</artifactId>
+        <groupId>cn.reghao.jutil</groupId>
+        <version>1.0.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>media</artifactId>
+
+    <properties>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.reghao.jutil</groupId>
+            <artifactId>jdk</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+        </dependency>
+
+        <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>
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>ffmpeg</artifactId>
+            <version>4.2.1-1.5.2</version>
+            <classifier>linux-x86_64</classifier>
+        </dependency>
+    </dependencies>
+</project>

+ 156 - 0
media/src/main/java/cn/reghao/jutil/media/image/ImageOps.java

@@ -0,0 +1,156 @@
+package cn.reghao.jutil.media.image;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * 对图像的操作
+ *
+ * @author reghao
+ * @date 2021-08-04 16:26:13
+ */
+public class ImageOps {
+    public static byte[] convert(File srcFile, String destFormat) {
+        String srcFormat = getFormat(srcFile);
+        if (srcFormat != null && srcFormat.equals(destFormat)) {
+            return new byte[0];
+        }
+
+        try (ImageInputStream iis = ImageIO.createImageInputStream(srcFile)) {
+            BufferedImage image = ImageIO.read(iis);
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            ImageOutputStream ios = ImageIO.createImageOutputStream(baos);
+            ImageIO.write(image, destFormat, ios);
+            return baos.toByteArray();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return new byte[0];
+    }
+
+    public static String getFormat(File file) {
+        try (ImageInputStream iis = ImageIO.createImageInputStream(file);) {
+            Iterator<ImageReader> iterator = ImageIO.getImageReaders(iis);
+            if ((iterator.hasNext())) {
+                ImageReader ir = iterator.next();
+                return ir.getFormatName();
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+
+    public static Size info(File file) throws IOException {
+        BufferedImage bi = ImageIO.read(file);
+        return new Size(bi.getWidth(), bi.getHeight());
+    }
+
+    public static BufferedImage merge(List<BufferedImage> bufferedImages, boolean isVertical) {
+        int size = bufferedImages.size();
+        int[][] imageArray = new int[size][];
+        for (int i = 0; i < size; i++) {
+            int width = bufferedImages.get(i).getWidth();
+            int height = bufferedImages.get(i).getHeight();
+            imageArray[i] = new int[width*height];
+            imageArray[i] = bufferedImages.get(i).getRGB(0, 0, width, height, imageArray[i], 0, width);
+        }
+
+        int newHeight = 0, newWidth = 0;
+        for (BufferedImage bufferedImage : bufferedImages) {
+            if (!isVertical) {
+                // 横向拼接,height 不变,width 增加
+                newHeight = Math.max(newHeight, bufferedImage.getHeight());
+                newWidth += bufferedImage.getWidth();
+            } else {
+                // 纵向拼接,width 不变,height 增加
+                newWidth = Math.max(newWidth, bufferedImage.getWidth());
+                newHeight += bufferedImage.getHeight();
+            }
+        }
+
+        BufferedImage newImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
+        int width = 0, height = 0;
+        for (int i = 0; i < size; i++) {
+            if (!isVertical) {
+                // 横向拼接
+                newImage.setRGB(width, 0, bufferedImages.get(i).getWidth(), newHeight, imageArray[i], 0, bufferedImages.get(i).getWidth());
+                width += bufferedImages.get(i).getWidth();
+            } else {
+                // 纵向拼接
+                newImage.setRGB(0, height, newWidth, bufferedImages.get(i).getHeight(), imageArray[i], 0, newWidth);
+                height += bufferedImages.get(i).getHeight();
+            }
+        }
+
+        return newImage;
+    }
+
+    /**
+     * 缩小图片
+     *
+     * @param
+     * @return
+     * @date 2021-08-18 下午1:45
+     */
+    public static BufferedImage resize(BufferedImage srcImage, int size) {
+        int width = srcImage.getWidth()/size;
+        int height = srcImage.getHeight()/size;
+
+        BufferedImage newImage = new BufferedImage(width, height, srcImage.getType());
+        Graphics g = newImage.getGraphics();
+        g.drawImage(srcImage, 0, 0, width, height, null);
+        g.dispose();
+        return newImage;
+    }
+
+    public static BufferedImage resize(String imagePath, int size) throws IOException {
+        BufferedImage srcImage = ImageIO.read(new File(imagePath));
+        int width = srcImage.getWidth()/size;
+        int height = srcImage.getHeight()/size;
+
+        BufferedImage newImage = new BufferedImage(width, height, srcImage.getType());
+        Graphics g = newImage.getGraphics();
+        g.drawImage(srcImage, 0, 0, width, height, null);
+        g.dispose();
+        return newImage;
+    }
+
+    public static void saveImage(BufferedImage bufferedImage, String filePath) throws IOException {
+        ImageIO.write(bufferedImage, "jpg", new File(filePath));
+    }
+
+    public static void saveImage(ByteArrayOutputStream baos, String filePath) throws IOException {
+        BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(baos.toByteArray()));
+        ImageIO.write(bufferedImage, "jpg", new File(filePath));
+    }
+
+    public static class Size {
+        private final int width;
+        private final int height;
+
+        public Size(int width, int height) {
+            this.width = width;
+            this.height = height;
+        }
+
+        public int getWidth() {
+            return width;
+        }
+
+        public int getHeight() {
+            return height;
+        }
+    }
+}

+ 39 - 0
media/src/main/java/cn/reghao/jutil/media/video/FFmpegWrapper.java

@@ -0,0 +1,39 @@
+package cn.reghao.jutil.media.video;
+
+import cn.reghao.jutil.jdk.shell.ShellExecutor;
+import cn.reghao.jutil.jdk.shell.ShellResult;
+
+/**
+ * @author reghao
+ * @date 2022-03-04 11:04:32
+ */
+public class FFmpegWrapper {
+    static ShellExecutor shellExecutor = new ShellExecutor();
+
+    public static void mergeToMp4(String dir, String videoId, String videoFilePath, String audioFilePath) throws Exception {
+        String mp4FilePath = String.format("%s/%s.mp4", dir, videoId);
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("ffmpeg -i ").append(audioFilePath).append(" ")
+                .append("-i ").append(videoFilePath).append(" ")
+                .append("-codec copy ").append(mp4FilePath);
+        ShellResult shellResult = shellExecutor.exec(dir, sb.toString().split("\\s+"));
+        if (!shellResult.isSuccess()) {
+            throw new Exception("合并成 mp4 文件异常: " + shellResult.getResult());
+        }
+    }
+
+    public static void generateDash(String dir, String video, String audio) throws Exception {
+        StringBuilder sb = new StringBuilder();
+        sb.append("MP4Box -dash 5000 -rap -frag-rap -profile dashavc264:onDemand -frag 5000 ")
+                .append(video).append(" ").append(audio).append(" ")
+                .append("-out index.mpd");
+        ShellResult shellResult = shellExecutor.exec(dir, sb.toString().split("\\s+"));
+        if (!shellResult.isSuccess()) {
+            throw new Exception("生成 dash 异常: " + shellResult.getResult());
+        }
+    }
+
+    public static void generateM3u8() {
+    }
+}

+ 178 - 0
media/src/main/java/cn/reghao/jutil/media/video/VideoOps.java

@@ -0,0 +1,178 @@
+package cn.reghao.jutil.media.video;
+
+import cn.reghao.jutil.media.image.ImageOps;
+import org.bytedeco.ffmpeg.avformat.AVFormatContext;
+import org.bytedeco.ffmpeg.avutil.AVDictionary;
+import org.bytedeco.ffmpeg.avutil.AVDictionaryEntry;
+import org.bytedeco.ffmpeg.global.avformat;
+import org.bytedeco.ffmpeg.global.avutil;
+import org.bytedeco.javacv.FFmpegFrameGrabber;
+import org.bytedeco.javacv.Frame;
+import org.bytedeco.javacv.Java2DFrameConverter;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * @author reghao
+ * @date 2021-08-04 09:51:30
+ */
+public class VideoOps {
+    static {
+        avutil.av_log_set_level(avutil.AV_LOG_QUIET);
+    }
+
+    private static final Java2DFrameConverter frameConverter = new Java2DFrameConverter();
+
+    /**
+     * 视频元数据
+     *
+     * @param
+     * @return
+     * @date 2023-02-06 5:55 AM
+     */
+    public static Map<String, String> getMetadata(String filePath) throws IOException {
+        VideoOps.videoProps(new File(filePath));
+        Map<String, String> metadata = new HashMap<>();
+        AVFormatContext fmt_ctx = new AVFormatContext(null);
+        AVDictionaryEntry tag = new AVDictionaryEntry(null);
+        try {
+            int ret = avformat.avformat_open_input(fmt_ctx, filePath, null, null);
+            if (ret < 0) {
+                throw new IOException(ret + ":avformat_open_input error");
+            }
+
+            ret = avformat.avformat_find_stream_info(fmt_ctx, (AVDictionary) null);
+            if (ret < 0) {
+                throw new IOException(ret + ":avformat_find_stream_info error");
+            }
+
+            // To iterate through all the dictionary entries, you can set the matching key
+            // to the null string "" and set the AV_DICT_IGNORE_SUFFIX flag.
+            // ::av_dict_iterate::
+            while (Objects.nonNull(tag = avutil.av_dict_get(fmt_ctx.metadata(), "", tag, avutil.AV_DICT_IGNORE_SUFFIX))) {
+                metadata.put(tag.key().getString(), tag.value().getString());
+            }
+        } finally {
+            avformat.avformat_close_input(fmt_ctx);
+        }
+        return metadata;
+    }
+
+    /**
+     * 视频属性
+     *
+     * @param
+     * @return
+     * @date 2021-08-13 上午9:08
+     */
+    public static Map<String, Object> videoProps(File file) throws IOException {
+        FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(file);
+        grabber.start();
+        // 截取第一帧
+        Frame frame = grabber.grabImage();
+        BufferedImage bufferedImage = frameConverter.getBufferedImage(frame);
+
+        Map<String, Object> map = new HashMap<>();
+        int width = bufferedImage.getWidth();
+        map.put("width", width);
+        int height = bufferedImage.getHeight();
+        map.put("height", height);
+        double fps = grabber.getFrameRate();
+        map.put("fps", fps);
+        int frameLength = grabber.getLengthInFrames();
+        map.put("frameLength", frameLength);
+        int length = frameLength / (int) fps;
+        map.put("length", length);
+        long duration = grabber.getLengthInTime() / (1000 * 1000);
+        map.put("duration", duration);
+        String format = grabber.getFormat();
+        map.put("format", format);
+        // 视频旋转的角度
+        String rotate = grabber.getVideoMetadata("rotate");
+        map.put("rotate", rotate);
+        grabber.stop();
+        return map;
+    }
+
+    /**
+     * 取出指定位置的帧生成视频封面缩略图
+     *
+     * @param
+     * @return
+     * @date 2021-08-18 上午11:12
+     */
+    public static ByteArrayOutputStream thumbnailCover(File file) throws IOException {
+        FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(file);
+        grabber.start();
+        int frameLength = grabber.getLengthInFrames();
+        int interval = frameLength/16;
+        List<Integer> frames = new ArrayList<>();
+        for (int i = 1; i < frameLength; i += interval) {
+            frames.add(i);
+        }
+
+        if (frames.size() > 16) {
+            frames = frames.subList(0, 16);
+        }
+
+        List<List<BufferedImage>> lists = new ArrayList<>();
+        List<BufferedImage> list = new ArrayList<>();
+        Frame frame;
+        for (int i : frames) {
+            grabber.setFrameNumber(i);
+            frame = grabber.grabImage();
+            if (list.size() == 4) {
+                lists.add(new ArrayList<>(list));
+                list.clear();
+            }
+
+            if (frame != null) {
+                // TODO 在截取的帧上添加视频中所处的时间水印
+                list.add(ImageOps.resize(frameConverter.getBufferedImage(frame), 4));
+            }
+        }
+        grabber.stop();
+        if (list.size() == 4) {
+            lists.add(new ArrayList<>(list));
+            list.clear();
+        }
+
+        List<BufferedImage> list1 = new ArrayList<>();
+        for (List<BufferedImage> tmp : lists) {
+            list1.add(ImageOps.merge(tmp, false));
+        }
+
+        BufferedImage resultImage = ImageOps.merge(list1, true);
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ImageIO.write(resultImage, "jpg", baos);
+        return baos;
+    }
+
+    public static ByteArrayOutputStream videoCover(File file) throws IOException {
+        FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(file);
+        grabber.start();
+        int frameNumber = grabber.getLengthInFrames()/100;
+        grabber.setFrameNumber(frameNumber);
+        // 截取帧总长度的 1/100 位置处作为视频封面
+        Frame frame = grabber.grabImage();
+        BufferedImage bufferedImage = frameConverter.getBufferedImage(frame);
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ImageIO.write(bufferedImage, "jpg", baos);
+        return baos;
+    }
+
+    public static void videoCover(File file, String savedFilePath) throws IOException {
+        FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(file);
+        grabber.start();
+        // 截取第一帧作为视频封面
+        Frame frame = grabber.grabImage();
+        BufferedImage bufferedImage = frameConverter.getBufferedImage(frame);
+        ImageOps.saveImage(bufferedImage, savedFilePath);
+    }
+}

+ 14 - 0
media/src/test/java/ImageTest.java

@@ -0,0 +1,14 @@
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.util.Iterator;
+
+/**
+ * @author reghao
+ * @date 2023-02-21 21:09:37
+ */
+public class ImageTest {
+}

+ 1 - 0
pom.xml

@@ -13,6 +13,7 @@
         <module>tool</module>
         <module>redis</module>
         <module>web</module>
+        <module>media</module>
     </modules>
 
     <properties>