MyVideoUtils.java 8.4 KB
package com.sanmang.util;

import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 视频工具
 */
public class MyVideoUtils {
    private static final Logger logger = LoggerFactory.getLogger(MyVideoUtils.class);
    /**
     * 小截图的宽
     */
    private static int width = 100;
    /**
     * 小截图的高
     */
    private static int height = 56;
    /**
     * 小截图的间隔,秒
     */
    private static int DELTA = 6;

    private static String ffmpegPath;
    /**
     * 封面宽高
     */
    private static String coverSize = "400x225";

    /**
     * HLS 视频每段的时长
     */
    private static String segmentTime = "10";

    static {
        ResourceBundle bundle = PropertyResourceBundle.getBundle("video-config");
        ffmpegPath = bundle.getString("ffmpeg.path");
        width = Integer.parseInt(bundle.getString("snapshot.width"));
        height = Integer.parseInt(bundle.getString("snapshot.height"));
        DELTA = Integer.parseInt(bundle.getString("snapshot.interval"));
        coverSize = bundle.getString("cover.size");
        segmentTime = bundle.getString("hls.segment.time");
    }

    /**
     * 将视频转为 HLS 格式 (m3u8)
     * @param dir 放视频的路径
     * @param videoFileName 视频文件名称(不包含路径)
     * @throws IOException IOException
     */
    public static void toHLS(String dir, String videoFileName) throws IOException {
        String prefix = getFilePrefix(videoFileName);
        Path path = Paths.get(dir, prefix);
        Files.createDirectories(path);

        logger.info("convert video to HLS format.");
        ProcessBuilder builder = new ProcessBuilder(ffmpegPath, "-y",
                "-i", Paths.get(dir, videoFileName).toString(),
                "-codec:v", "libx264", "-codec:a", "mp3", "-map", "0",
                "-f", "ssegment", "-segment_format", "mpegts",
                "-segment_list", path.resolve(prefix + ".m3u8").toString(),
                "-segment_time", segmentTime,
                path.resolve(prefix + ".%03d.ts").toString());

        builder.redirectErrorStream(true);

        Process process = builder.start();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "utf-8"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                logger.debug(line);
            }
        }
    }

    /**
     * 首帧截图。每隔6帧截图合并一张图,并生成对应的css
     */
    public static Map<String, Object> snapshot(String dir, String videoFileName) throws IOException, InterruptedException {
        Map<String, Object> map = new HashMap<>();

        String prefix = getFilePrefix(videoFileName);
        Path path = Paths.get(dir, prefix);
        Files.createDirectories(path);

        generateCover(dir, videoFileName, map);

        generateMoreImageAndCSS(dir, videoFileName, map);

        Path jsonPath = path.resolve(prefix + ".json");
        Files.write(jsonPath, JSON.toJSONBytes(map));

        return map;
    }

    /**
     * 每隔 DELTA 秒截一张图,合并为一张图,同时生成对应的 css 文件
     */
    private static void generateMoreImageAndCSS(String dir, String fileName, Map<String, Object> map) throws IOException, InterruptedException {
        String prefix = getFilePrefix(fileName);
        Path path = Paths.get(dir, prefix);
        Files.createDirectories(path);

        String cssFileName = prefix + ".css";
        String combineImageName = prefix + ".combine.jpg";
        String combineImagePath = path.resolve(combineImageName).toString();
        int seconds = (Integer) map.get("seconds");
        logger.info("generate images & css");
        StringBuilder cssBuilder = new StringBuilder();
        cssBuilder.append(".icon {display: inline-block; ")
                .append("width: ").append(width).append("px; height: ").append(height).append("px; ")
                .append("background-image: url(").append(combineImageName).append(");}\n");

        int numIcons = (seconds - 1) / DELTA + 1;
        map.put("numIcons", numIcons);
        int countPerRow = 20;
        BufferedImage combine = new BufferedImage(width * countPerRow,
                (int) (height * Math.ceil(1.0 * numIcons / countPerRow)),
                BufferedImage.TYPE_INT_RGB);

        for (int i = 0; i < numIcons; i++) {
            logger.info("image: {}/{}", i + 1, numIcons);
            String imgFilePath = path.resolve(prefix + "." + i + ".jpg").toString();

            // 截图
            ProcessBuilder builder = new ProcessBuilder(ffmpegPath, "-ss", "" + (i * DELTA),
                    "-i", Paths.get(dir, fileName).toString(),
                    "-f", "image2", "-s", width + "x" + height,
                    "-t", "0.001", "-y", imgFilePath);
            builder.redirectErrorStream(true);

            Process process = builder.start();
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "utf-8"))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    logger.debug(line);
                }
            }
            process.waitFor(); // 保证图片已经生成才能进行后续的合并图片。会增加执行时间

            // 合并到总图片中
            File imageFile = new File(imgFilePath);
            BufferedImage image = ImageIO.read(imageFile);
            int x = width * (i % countPerRow);
            int y = height * (i / countPerRow);
            combine.getGraphics().drawImage(image, x, y, null);

            // css
            cssBuilder.append(".icon-").append(i).append("{background-position: ").append(-x).append("px ")
                    .append(-y).append("px;}\n");

            // delete temp image
            Files.delete(Paths.get(imgFilePath));
        }

        // 保存合并后的图片
        logger.info("save combine image: {}", combineImageName);
        ImageIO.write(combine, "JPG", new File(combineImagePath));


        // 保存 css
        logger.info("generate css: {}", cssFileName);
        Files.write(path.resolve(cssFileName), cssBuilder.toString().getBytes("utf-8"));

        map.put("css", cssFileName);
        map.put("combine", combineImageName);
    }

    private static String getFilePrefix(String fileName) {
        return fileName.substring(0, fileName.lastIndexOf("."));
    }

    /**
     * 第一页截图,以及得到总时长
     */
    private static void generateCover(String dir, String fileName, Map<String, Object> map) throws IOException {
        String prefix = getFilePrefix(fileName);
        String imageName = prefix + ".jpg";
        Path path = Paths.get(dir, prefix);
        Files.createDirectories(path);
        String imageFullPath = path.resolve(imageName).toString();

        ProcessBuilder builder = new ProcessBuilder(ffmpegPath, "-y",
                "-i", Paths.get(dir, fileName).toString(),
                "-f", "image2", "-s", coverSize,
                "-t", "0.001", imageFullPath);
        builder.redirectErrorStream(true);
        logger.info("generate cover: {}", imageName);
        Process process = builder.start();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "utf-8"))) {
            String line;
            Pattern pattern = Pattern.compile("\\s+Duration: (.+?),.+");
            while ((line = reader.readLine()) != null) {
                Matcher matcher = pattern.matcher(line);
                if (matcher.matches()) {
                    String timeStr = matcher.group(1);
                    map.put("duration", timeStr);
                    String[] split = timeStr.split(":|\\.");
                    int seconds = Integer.parseInt(split[0]) * 60 * 60
                            + Integer.parseInt(split[1]) * 60
                            + Integer.parseInt(split[2]);
                    map.put("seconds", seconds);
                }
            }
        }
    }
}