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