From 7690c679d59cf806fb9fa972f824ee657d0a0643 Mon Sep 17 00:00:00 2001
From: lipengjava <lipbb@qq.com>
Date: Tue, 8 Aug 2017 09:15:53 +0800
Subject: [PATCH] first commit

---
 .gitignore                                          |   1 +
 README.md                                           |   9 +++++++++
 pom.xml                                             |  90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/main/java/MyApplication.java                    |  27 +++++++++++++++++++++++++++
 src/main/java/com/sanmang/service/VideoService.java |  33 +++++++++++++++++++++++++++++++++
 src/main/java/com/sanmang/util/MyVideoUtils.java    | 224 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/main/resources/log4j.properties                 |  12 ++++++++++++
 src/main/resources/video-config.properties          |  16 ++++++++++++++++
 src/test/java/VideoTest.java                        |  20 ++++++++++++++++++++
 video.iml                                           |  21 +++++++++++++++++++++
 10 files changed, 453 insertions(+), 0 deletions(-)
 create mode 100644 .gitignore
 create mode 100644 README.md
 create mode 100644 pom.xml
 create mode 100644 src/main/java/MyApplication.java
 create mode 100644 src/main/java/com/sanmang/service/VideoService.java
 create mode 100644 src/main/java/com/sanmang/util/MyVideoUtils.java
 create mode 100644 src/main/resources/log4j.properties
 create mode 100644 src/main/resources/video-config.properties
 create mode 100644 src/test/java/VideoTest.java
 create mode 100644 video.iml

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2e63c91
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+## 视频截图
+---
+
+1. 基本功能:
+    * 生成首帧图片
+    * 每隔 n 秒生成一张截图,并将所有截图合并为一张大图,同时生成访问各图片的 css
+    * 转 HLS (未测试)
+2. 实现方式:
+    * 使用 ffmpeg
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..72a9f45
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,90 @@
+<?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">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.sanmang</groupId>
+    <artifactId>video</artifactId>
+    <version>1.0.0</version>
+
+    <packaging>jar</packaging>
+
+
+    <properties>
+        <!--打包时编码-->
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <!--编译时编码-->
+        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
+        <!--编译级别-->
+        <maven.compiler.source>1.7</maven.compiler.source>
+        <maven.compiler.target>1.7</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <version>1.7.21</version>
+        </dependency>
+        <dependency>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+            <version>1.2.17</version>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.22</version>
+        </dependency>
+
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.12</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>2.4</version>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <!-- 指定Main方法入口的class -->
+                            <mainClass>MyApplication</mainClass>
+                            <!-- 在jar包的MANIFEST.MF文件中生成Class-Path属性 -->
+                            <addClasspath>true</addClasspath>
+                            <!-- Class-Path 前缀 -->
+                            <classpathPrefix>lib/</classpathPrefix>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+            <!-- 复制jar包到lib目录 -->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>copy-dependencies</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>
+                                ${project.build.directory}/lib
+                            </outputDirectory>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/src/main/java/MyApplication.java b/src/main/java/MyApplication.java
new file mode 100644
index 0000000..a96d9c5
--- /dev/null
+++ b/src/main/java/MyApplication.java
@@ -0,0 +1,27 @@
+import com.sanmang.service.VideoService;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class MyApplication {
+    public static void main(String[] args) {
+        if (args.length > 0) {
+            String src = args[0];
+            Path path = Paths.get(src);
+            if (Files.exists(path) || Files.isDirectory(path)) {
+                try {
+                    VideoService service = new VideoService();
+                    service.allInDir(src);
+                } catch (InterruptedException | IOException e) {
+                    e.printStackTrace();
+                }
+            } else {
+                System.out.println("Directory not found.");
+            }
+        } else {
+            System.out.println("Usage: java -jar video.jar yourVideoDir");
+        }
+    }
+}
diff --git a/src/main/java/com/sanmang/service/VideoService.java b/src/main/java/com/sanmang/service/VideoService.java
new file mode 100644
index 0000000..7f74765
--- /dev/null
+++ b/src/main/java/com/sanmang/service/VideoService.java
@@ -0,0 +1,33 @@
+package com.sanmang.service;
+
+import com.sanmang.util.MyVideoUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+/**
+ * 视频处理功能
+ */
+public class VideoService {
+    private Logger logger = LoggerFactory.getLogger(VideoService.class);
+
+    public void allInDir(String dir) throws IOException, InterruptedException {
+        File file = new File(dir);
+        String[] list = file.list(new FilenameFilter() {
+            @Override
+            public boolean accept(File dir, String name) {
+                return name.endsWith(".mp4") || name.endsWith(".flv");
+            }
+        });
+        if (list == null)
+            return;
+        for (String fileName : list) {
+            logger.info("processing file: {}", fileName);
+//            MyVideoUtils.toHLS(dir, fileName);
+            MyVideoUtils.snapshot(dir, fileName);
+        }
+    }
+}
diff --git a/src/main/java/com/sanmang/util/MyVideoUtils.java b/src/main/java/com/sanmang/util/MyVideoUtils.java
new file mode 100644
index 0000000..5c70431
--- /dev/null
+++ b/src/main/java/com/sanmang/util/MyVideoUtils.java
@@ -0,0 +1,224 @@
+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);
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties
new file mode 100644
index 0000000..538578a
--- /dev/null
+++ b/src/main/resources/log4j.properties
@@ -0,0 +1,12 @@
+log4j.rootLogger=INFO,file,stdout
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.Target=System.out
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
+log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
+log4j.appender.file.layout.ConversionPattern=%d{MM-dd HH\:mm\:ss.SSS} %-4r %-5p [%t] %37c %3x - %m%n
+log4j.appender.file.File=${catalina.home}/logs/video.log
+log4j.appender.file.DatePattern='.'yyyy-MM-dd
+log4j.appender.file.Append=false
+log4j.appender.file.Threshold=INFO
+log4j.appender.file.layout=org.apache.log4j.PatternLayout
\ No newline at end of file
diff --git a/src/main/resources/video-config.properties b/src/main/resources/video-config.properties
new file mode 100644
index 0000000..c8870ac
--- /dev/null
+++ b/src/main/resources/video-config.properties
@@ -0,0 +1,16 @@
+# ffmpeg ��ַ
+ffmpeg.path=D:/tools/ffmpeg.exe
+
+# ת HLS
+# ��Ƭʱ������λ��
+hls.segment.time=30
+
+# ��ͼ��ѩ��ͼ��ز�����
+# ʱ��������λ��
+snapshot.interval=6
+# ��ͼ����
+snapshot.width=100
+snapshot.height=56
+
+# ���棨��һ֡��ͼ���Ŀ���
+cover.size=400x225
diff --git a/src/test/java/VideoTest.java b/src/test/java/VideoTest.java
new file mode 100644
index 0000000..ff4936d
--- /dev/null
+++ b/src/test/java/VideoTest.java
@@ -0,0 +1,20 @@
+import com.sanmang.service.VideoService;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * video
+ */
+public class VideoTest {
+    @Test
+    public void allInDir() {
+//        VideoService service = new VideoService();
+//        String dir = "D:/1/1.mp4";
+//        try {
+//            service.allInDir(dir);
+//        } catch (IOException | InterruptedException e) {
+//            e.printStackTrace();
+//        }
+    }
+}
diff --git a/video.iml b/video.iml
new file mode 100644
index 0000000..217202b
--- /dev/null
+++ b/video.iml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
+    <output url="file://$MODULE_DIR$/target/classes" />
+    <output-test url="file://$MODULE_DIR$/target/test-classes" />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+      <excludeFolder url="file://$MODULE_DIR$/target" />
+    </content>
+    <orderEntry type="jdk" jdkName="1.7" jdkType="JavaSDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="Maven: org.slf4j:slf4j-log4j12:1.7.21" level="project" />
+    <orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.21" level="project" />
+    <orderEntry type="library" name="Maven: log4j:log4j:1.2.17" level="project" />
+    <orderEntry type="library" name="Maven: com.alibaba:fastjson:1.2.22" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
+  </component>
+</module>
\ No newline at end of file
--
libgit2 0.24.0