lipengjava

first commit

  1 +target/
  1 +## 视频截图
  2 +---
  3 +
  4 +1. 基本功能:
  5 + * 生成首帧图片
  6 + * 每隔 n 秒生成一张截图,并将所有截图合并为一张大图,同时生成访问各图片的 css
  7 + * 转 HLS (未测试)
  8 +2. 实现方式:
  9 + * 使用 ffmpeg
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <modelVersion>4.0.0</modelVersion>
  6 +
  7 + <groupId>com.sanmang</groupId>
  8 + <artifactId>video</artifactId>
  9 + <version>1.0.0</version>
  10 +
  11 + <packaging>jar</packaging>
  12 +
  13 +
  14 + <properties>
  15 + <!--打包时编码-->
  16 + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  17 + <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  18 + <!--编译时编码-->
  19 + <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
  20 + <!--编译级别-->
  21 + <maven.compiler.source>1.7</maven.compiler.source>
  22 + <maven.compiler.target>1.7</maven.compiler.target>
  23 + </properties>
  24 +
  25 + <dependencies>
  26 + <dependency>
  27 + <groupId>org.slf4j</groupId>
  28 + <artifactId>slf4j-log4j12</artifactId>
  29 + <version>1.7.21</version>
  30 + </dependency>
  31 + <dependency>
  32 + <groupId>log4j</groupId>
  33 + <artifactId>log4j</artifactId>
  34 + <version>1.2.17</version>
  35 + </dependency>
  36 + <dependency>
  37 + <groupId>com.alibaba</groupId>
  38 + <artifactId>fastjson</artifactId>
  39 + <version>1.2.22</version>
  40 + </dependency>
  41 +
  42 + <dependency>
  43 + <groupId>junit</groupId>
  44 + <artifactId>junit</artifactId>
  45 + <version>4.12</version>
  46 + <scope>test</scope>
  47 + </dependency>
  48 + </dependencies>
  49 + <build>
  50 + <plugins>
  51 + <plugin>
  52 + <groupId>org.apache.maven.plugins</groupId>
  53 + <artifactId>maven-jar-plugin</artifactId>
  54 + <version>2.4</version>
  55 + <configuration>
  56 + <archive>
  57 + <manifest>
  58 + <!-- 指定Main方法入口的class -->
  59 + <mainClass>MyApplication</mainClass>
  60 + <!-- 在jar包的MANIFEST.MF文件中生成Class-Path属性 -->
  61 + <addClasspath>true</addClasspath>
  62 + <!-- Class-Path 前缀 -->
  63 + <classpathPrefix>lib/</classpathPrefix>
  64 + </manifest>
  65 + </archive>
  66 + </configuration>
  67 + </plugin>
  68 + <!-- 复制jar包到lib目录 -->
  69 + <plugin>
  70 + <groupId>org.apache.maven.plugins</groupId>
  71 + <artifactId>maven-dependency-plugin</artifactId>
  72 + <executions>
  73 + <execution>
  74 + <id>copy</id>
  75 + <phase>package</phase>
  76 + <goals>
  77 + <goal>copy-dependencies</goal>
  78 + </goals>
  79 + <configuration>
  80 + <outputDirectory>
  81 + ${project.build.directory}/lib
  82 + </outputDirectory>
  83 + </configuration>
  84 + </execution>
  85 + </executions>
  86 + </plugin>
  87 + </plugins>
  88 + </build>
  89 +
  90 +</project>
  1 +import com.sanmang.service.VideoService;
  2 +
  3 +import java.io.IOException;
  4 +import java.nio.file.Files;
  5 +import java.nio.file.Path;
  6 +import java.nio.file.Paths;
  7 +
  8 +public class MyApplication {
  9 + public static void main(String[] args) {
  10 + if (args.length > 0) {
  11 + String src = args[0];
  12 + Path path = Paths.get(src);
  13 + if (Files.exists(path) || Files.isDirectory(path)) {
  14 + try {
  15 + VideoService service = new VideoService();
  16 + service.allInDir(src);
  17 + } catch (InterruptedException | IOException e) {
  18 + e.printStackTrace();
  19 + }
  20 + } else {
  21 + System.out.println("Directory not found.");
  22 + }
  23 + } else {
  24 + System.out.println("Usage: java -jar video.jar yourVideoDir");
  25 + }
  26 + }
  27 +}
  1 +package com.sanmang.service;
  2 +
  3 +import com.sanmang.util.MyVideoUtils;
  4 +import org.slf4j.Logger;
  5 +import org.slf4j.LoggerFactory;
  6 +
  7 +import java.io.File;
  8 +import java.io.FilenameFilter;
  9 +import java.io.IOException;
  10 +
  11 +/**
  12 + * 视频处理功能
  13 + */
  14 +public class VideoService {
  15 + private Logger logger = LoggerFactory.getLogger(VideoService.class);
  16 +
  17 + public void allInDir(String dir) throws IOException, InterruptedException {
  18 + File file = new File(dir);
  19 + String[] list = file.list(new FilenameFilter() {
  20 + @Override
  21 + public boolean accept(File dir, String name) {
  22 + return name.endsWith(".mp4") || name.endsWith(".flv");
  23 + }
  24 + });
  25 + if (list == null)
  26 + return;
  27 + for (String fileName : list) {
  28 + logger.info("processing file: {}", fileName);
  29 +// MyVideoUtils.toHLS(dir, fileName);
  30 + MyVideoUtils.snapshot(dir, fileName);
  31 + }
  32 + }
  33 +}
  1 +package com.sanmang.util;
  2 +
  3 +import com.alibaba.fastjson.JSON;
  4 +import org.slf4j.Logger;
  5 +import org.slf4j.LoggerFactory;
  6 +
  7 +import javax.imageio.ImageIO;
  8 +import java.awt.image.BufferedImage;
  9 +import java.io.BufferedReader;
  10 +import java.io.File;
  11 +import java.io.IOException;
  12 +import java.io.InputStreamReader;
  13 +import java.nio.file.Files;
  14 +import java.nio.file.Path;
  15 +import java.nio.file.Paths;
  16 +import java.util.HashMap;
  17 +import java.util.Map;
  18 +import java.util.PropertyResourceBundle;
  19 +import java.util.ResourceBundle;
  20 +import java.util.regex.Matcher;
  21 +import java.util.regex.Pattern;
  22 +
  23 +/**
  24 + * 视频工具
  25 + */
  26 +public class MyVideoUtils {
  27 + private static final Logger logger = LoggerFactory.getLogger(MyVideoUtils.class);
  28 + /**
  29 + * 小截图的宽
  30 + */
  31 + private static int width = 100;
  32 + /**
  33 + * 小截图的高
  34 + */
  35 + private static int height = 56;
  36 + /**
  37 + * 小截图的间隔,秒
  38 + */
  39 + private static int DELTA = 6;
  40 +
  41 + private static String ffmpegPath;
  42 + /**
  43 + * 封面宽高
  44 + */
  45 + private static String coverSize = "400x225";
  46 +
  47 + /**
  48 + * HLS 视频每段的时长
  49 + */
  50 + private static String segmentTime = "10";
  51 +
  52 + static {
  53 + ResourceBundle bundle = PropertyResourceBundle.getBundle("video-config");
  54 + ffmpegPath = bundle.getString("ffmpeg.path");
  55 + width = Integer.parseInt(bundle.getString("snapshot.width"));
  56 + height = Integer.parseInt(bundle.getString("snapshot.height"));
  57 + DELTA = Integer.parseInt(bundle.getString("snapshot.interval"));
  58 + coverSize = bundle.getString("cover.size");
  59 + segmentTime = bundle.getString("hls.segment.time");
  60 + }
  61 +
  62 + /**
  63 + * 将视频转为 HLS 格式 (m3u8)
  64 + * @param dir 放视频的路径
  65 + * @param videoFileName 视频文件名称(不包含路径)
  66 + * @throws IOException IOException
  67 + */
  68 + public static void toHLS(String dir, String videoFileName) throws IOException {
  69 + String prefix = getFilePrefix(videoFileName);
  70 + Path path = Paths.get(dir, prefix);
  71 + Files.createDirectories(path);
  72 +
  73 + logger.info("convert video to HLS format.");
  74 + ProcessBuilder builder = new ProcessBuilder(ffmpegPath, "-y",
  75 + "-i", Paths.get(dir, videoFileName).toString(),
  76 + "-codec:v", "libx264", "-codec:a", "mp3", "-map", "0",
  77 + "-f", "ssegment", "-segment_format", "mpegts",
  78 + "-segment_list", path.resolve(prefix + ".m3u8").toString(),
  79 + "-segment_time", segmentTime,
  80 + path.resolve(prefix + ".%03d.ts").toString());
  81 +
  82 + builder.redirectErrorStream(true);
  83 +
  84 + Process process = builder.start();
  85 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "utf-8"))) {
  86 + String line;
  87 + while ((line = reader.readLine()) != null) {
  88 + logger.debug(line);
  89 + }
  90 + }
  91 + }
  92 +
  93 + /**
  94 + * 首帧截图。每隔6帧截图合并一张图,并生成对应的css
  95 + */
  96 + public static Map<String, Object> snapshot(String dir, String videoFileName) throws IOException, InterruptedException {
  97 + Map<String, Object> map = new HashMap<>();
  98 +
  99 + String prefix = getFilePrefix(videoFileName);
  100 + Path path = Paths.get(dir, prefix);
  101 + Files.createDirectories(path);
  102 +
  103 + generateCover(dir, videoFileName, map);
  104 +
  105 + generateMoreImageAndCSS(dir, videoFileName, map);
  106 +
  107 + Path jsonPath = path.resolve(prefix + ".json");
  108 + Files.write(jsonPath, JSON.toJSONBytes(map));
  109 +
  110 + return map;
  111 + }
  112 +
  113 + /**
  114 + * 每隔 DELTA 秒截一张图,合并为一张图,同时生成对应的 css 文件
  115 + */
  116 + private static void generateMoreImageAndCSS(String dir, String fileName, Map<String, Object> map) throws IOException, InterruptedException {
  117 + String prefix = getFilePrefix(fileName);
  118 + Path path = Paths.get(dir, prefix);
  119 + Files.createDirectories(path);
  120 +
  121 + String cssFileName = prefix + ".css";
  122 + String combineImageName = prefix + ".combine.jpg";
  123 + String combineImagePath = path.resolve(combineImageName).toString();
  124 + int seconds = (Integer) map.get("seconds");
  125 + logger.info("generate images & css");
  126 + StringBuilder cssBuilder = new StringBuilder();
  127 + cssBuilder.append(".icon {display: inline-block; ")
  128 + .append("width: ").append(width).append("px; height: ").append(height).append("px; ")
  129 + .append("background-image: url(").append(combineImageName).append(");}\n");
  130 +
  131 + int numIcons = (seconds - 1) / DELTA + 1;
  132 + map.put("numIcons", numIcons);
  133 + int countPerRow = 20;
  134 + BufferedImage combine = new BufferedImage(width * countPerRow,
  135 + (int) (height * Math.ceil(1.0 * numIcons / countPerRow)),
  136 + BufferedImage.TYPE_INT_RGB);
  137 +
  138 + for (int i = 0; i < numIcons; i++) {
  139 + logger.info("image: {}/{}", i + 1, numIcons);
  140 + String imgFilePath = path.resolve(prefix + "." + i + ".jpg").toString();
  141 +
  142 + // 截图
  143 + ProcessBuilder builder = new ProcessBuilder(ffmpegPath, "-ss", "" + (i * DELTA),
  144 + "-i", Paths.get(dir, fileName).toString(),
  145 + "-f", "image2", "-s", width + "x" + height,
  146 + "-t", "0.001", "-y", imgFilePath);
  147 + builder.redirectErrorStream(true);
  148 +
  149 + Process process = builder.start();
  150 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "utf-8"))) {
  151 + String line;
  152 + while ((line = reader.readLine()) != null) {
  153 + logger.debug(line);
  154 + }
  155 + }
  156 + process.waitFor(); // 保证图片已经生成才能进行后续的合并图片。会增加执行时间
  157 +
  158 + // 合并到总图片中
  159 + File imageFile = new File(imgFilePath);
  160 + BufferedImage image = ImageIO.read(imageFile);
  161 + int x = width * (i % countPerRow);
  162 + int y = height * (i / countPerRow);
  163 + combine.getGraphics().drawImage(image, x, y, null);
  164 +
  165 + // css
  166 + cssBuilder.append(".icon-").append(i).append("{background-position: ").append(-x).append("px ")
  167 + .append(-y).append("px;}\n");
  168 +
  169 + // delete temp image
  170 + Files.delete(Paths.get(imgFilePath));
  171 + }
  172 +
  173 + // 保存合并后的图片
  174 + logger.info("save combine image: {}", combineImageName);
  175 + ImageIO.write(combine, "JPG", new File(combineImagePath));
  176 +
  177 +
  178 + // 保存 css
  179 + logger.info("generate css: {}", cssFileName);
  180 + Files.write(path.resolve(cssFileName), cssBuilder.toString().getBytes("utf-8"));
  181 +
  182 + map.put("css", cssFileName);
  183 + map.put("combine", combineImageName);
  184 + }
  185 +
  186 + private static String getFilePrefix(String fileName) {
  187 + return fileName.substring(0, fileName.lastIndexOf("."));
  188 + }
  189 +
  190 + /**
  191 + * 第一页截图,以及得到总时长
  192 + */
  193 + private static void generateCover(String dir, String fileName, Map<String, Object> map) throws IOException {
  194 + String prefix = getFilePrefix(fileName);
  195 + String imageName = prefix + ".jpg";
  196 + Path path = Paths.get(dir, prefix);
  197 + Files.createDirectories(path);
  198 + String imageFullPath = path.resolve(imageName).toString();
  199 +
  200 + ProcessBuilder builder = new ProcessBuilder(ffmpegPath, "-y",
  201 + "-i", Paths.get(dir, fileName).toString(),
  202 + "-f", "image2", "-s", coverSize,
  203 + "-t", "0.001", imageFullPath);
  204 + builder.redirectErrorStream(true);
  205 + logger.info("generate cover: {}", imageName);
  206 + Process process = builder.start();
  207 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "utf-8"))) {
  208 + String line;
  209 + Pattern pattern = Pattern.compile("\\s+Duration: (.+?),.+");
  210 + while ((line = reader.readLine()) != null) {
  211 + Matcher matcher = pattern.matcher(line);
  212 + if (matcher.matches()) {
  213 + String timeStr = matcher.group(1);
  214 + map.put("duration", timeStr);
  215 + String[] split = timeStr.split(":|\\.");
  216 + int seconds = Integer.parseInt(split[0]) * 60 * 60
  217 + + Integer.parseInt(split[1]) * 60
  218 + + Integer.parseInt(split[2]);
  219 + map.put("seconds", seconds);
  220 + }
  221 + }
  222 + }
  223 + }
  224 +}
  1 +log4j.rootLogger=INFO,file,stdout
  2 +log4j.appender.stdout=org.apache.log4j.ConsoleAppender
  3 +log4j.appender.stdout.Target=System.out
  4 +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
  5 +log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
  6 +log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
  7 +log4j.appender.file.layout.ConversionPattern=%d{MM-dd HH\:mm\:ss.SSS} %-4r %-5p [%t] %37c %3x - %m%n
  8 +log4j.appender.file.File=${catalina.home}/logs/video.log
  9 +log4j.appender.file.DatePattern='.'yyyy-MM-dd
  10 +log4j.appender.file.Append=false
  11 +log4j.appender.file.Threshold=INFO
  12 +log4j.appender.file.layout=org.apache.log4j.PatternLayout
  1 +# ffmpeg 地址
  2 +ffmpeg.path=D:/tools/ffmpeg.exe
  3 +
  4 +# 转 HLS
  5 +# 分片时长,单位秒
  6 +hls.segment.time=30
  7 +
  8 +# 截图(雪碧图相关参数)
  9 +# 时间间隔,单位秒
  10 +snapshot.interval=6
  11 +# 截图宽高
  12 +snapshot.width=100
  13 +snapshot.height=56
  14 +
  15 +# 封面(第一帧截图)的宽高
  16 +cover.size=400x225
  1 +import com.sanmang.service.VideoService;
  2 +import org.junit.Test;
  3 +
  4 +import java.io.IOException;
  5 +
  6 +/**
  7 + * video
  8 + */
  9 +public class VideoTest {
  10 + @Test
  11 + public void allInDir() {
  12 +// VideoService service = new VideoService();
  13 +// String dir = "D:/1/1.mp4";
  14 +// try {
  15 +// service.allInDir(dir);
  16 +// } catch (IOException | InterruptedException e) {
  17 +// e.printStackTrace();
  18 +// }
  19 + }
  20 +}
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
  3 + <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
  4 + <output url="file://$MODULE_DIR$/target/classes" />
  5 + <output-test url="file://$MODULE_DIR$/target/test-classes" />
  6 + <content url="file://$MODULE_DIR$">
  7 + <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
  8 + <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
  9 + <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
  10 + <excludeFolder url="file://$MODULE_DIR$/target" />
  11 + </content>
  12 + <orderEntry type="jdk" jdkName="1.7" jdkType="JavaSDK" />
  13 + <orderEntry type="sourceFolder" forTests="false" />
  14 + <orderEntry type="library" name="Maven: org.slf4j:slf4j-log4j12:1.7.21" level="project" />
  15 + <orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.21" level="project" />
  16 + <orderEntry type="library" name="Maven: log4j:log4j:1.2.17" level="project" />
  17 + <orderEntry type="library" name="Maven: com.alibaba:fastjson:1.2.22" level="project" />
  18 + <orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" />
  19 + <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
  20 + </component>
  21 +</module>