lipengjava

first commit

target/
... ...
## 视频截图
---
1. 基本功能:
* 生成首帧图片
* 每隔 n 秒生成一张截图,并将所有截图合并为一张大图,同时生成访问各图片的 css
* 转 HLS (未测试)
2. 实现方式:
* 使用 ffmpeg
\ No newline at end of file
... ...
<?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
... ...
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");
}
}
}
... ...
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);
}
}
}
... ...
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);
}
}
}
}
}
... ...
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
... ...
# ffmpeg 地址
ffmpeg.path=D:/tools/ffmpeg.exe
# 转 HLS
# 分片时长,单位秒
hls.segment.time=30
# 截图(雪碧图相关参数)
# 时间间隔,单位秒
snapshot.interval=6
# 截图宽高
snapshot.width=100
snapshot.height=56
# 封面(第一帧截图)的宽高
cover.size=400x225
... ...
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();
// }
}
}
... ...
<?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
... ...