文章目录
前言
由于最近打算换手机,UC里边好多的小电影。咳咳(▰˘◡˘▰),不是,是学习资料打算转移一下,发现好多都是m3u8的视频,都是零零碎碎的片段组成,影响学习效率,所以有了这篇文章
一、m3u8文件
关于m3u8文件,就不具体介绍了(写了也是抄网上的),有兴趣的小伙伴可以看看这篇文章
这里就简单的引用一段话:
M3U8文件是指UTF-8编码格式的M3U文件(M3U使用Latin-1字符集编码)。M3U文件是一个记录索引的纯文本文件,打开它时播放软件并不是播放它,而是根据它的索引找到对应的音视频文件的网络地址进行在线播放
m3u8的文件格式,大体如下:
二、神奇的工具
接下来介绍下这次的重点,就是FFmpeg这个工具,官网的介绍如下:
A complete, cross-platform solution to record, convert and stream audio and video(一个完整的跨平台解决方案,用于录制、转换和流式传输音频和视频)
FFmpeg功能有很多,也很强大,我们这里只需要用到两个功能就行了,一个是根据m3u8网址下载视频,还有就是根据ts文件列表合成mp4文件
三、思路整理
1.安装FFmpeg
这个就直接去FFmpeg官网去下载就行了。找到对应的版本解压并配置环境变量,具体参考这边
2.获取m3u8文件
当只有一个m3u8文件的时候,只需把对应的文件路径写进去就行了,但是往往我们有很多资料(咳咳)。所以需要指定文件夹,获取到文件夹下所有的m3u8文件
3.解析m3u8文件
获取到m3u8文件之后,就需要提取m3u8的文件信息(主要是路径),之后再把Android的路径转化成Windows下的路径,如下:
这里借用一个open-m3u8.jar去解析,具体用法可以看open-m3u8.jar的git介绍
4.重写m3u8文件
4.1生成文件列表
调用FFmpeg之前,需要指定一个文件列表给FFmpeg作为参数进行合并,这个方法无法解决加密过的视频文件,可以自己进行解密,不过有点麻烦。文件格式大体如下:
大体就是读取m3u8文件列表,转成这种格式的文件
4.2重写视频文件路径
这个方式利用FFmpeg进行处理,不论是否加密过,都可以进行合并。只需要把原先的安卓文件路径替换成当前的路径即可
5.调用FFmpeg
生成列表文件之后,就需要调用FFmpeg命令将文件合成,这个也简单,只需要调用java自带的Runtime就行了
5.1生成文件列表
FFmpeg命令如下
ffmpeg -f concat -safe 0 -i 生成的列表文件 -c copy 视频名称
5.2重写视频文件路径
FFmpeg命令如下
ffmpeg -allowed_extensions ALL -protocol_whitelist "file,http,https,rtp,udp,tcp,tls,crypto" -i m3u8文件路径 -c copy 输出视频路径
6.其他
上边使用合并ts的方式实现,当然还可以直接通过网络下载,命令如下:
ffmpeg -allowed_extensions ALL -protocol_whitelist "file,http,https,rtp,udp,tcp,tls,crypto" -i m3u8文件路径 -c copy 输出视频路径
m3u8文件可以在ts目录下找,如下:
这里很明显是http协议,而不是file协议
四、代码实现
maven依赖如下:
<?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.hqd</groupId>
<artifactId>simple-utils</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<open-m3u8.version>0.2.4</open-m3u8.version>
<commons-beanutils.version>1.7.0</commons-beanutils.version>
<commons-lang3.version>3.4</commons-lang3.version>
<commons-collections.version>3.2.2</commons-collections.version>
<commons-io.version>2.4</commons-io.version>
<junit.version>4.12</junit.version>
</properties>
<dependencies>
<!-- m3u8解析工具 -->
<dependency>
<groupId>com.iheartradio.m3u8</groupId>
<artifactId>open-m3u8</artifactId>
<version>${open-m3u8.version}</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>${commons-beanutils.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>${commons-collections.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<!-- 测试模块jar包-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
java代码如下:
package com.hqd.test;
import com.hqd.utils.file.SimpleFileUtils;
import com.hqd.utils.file.filter.SuffixNameFilter;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.TrackData;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.FileExistsException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.*;
import java.util.List;
public class FileUtilsTest {
private final static String KEY_TAG = "#EXT-X-KEY:";
private final static String SUFFIX_KEY = ".key";
public static String SUFFIX_NAME_MP4 = ".mp4";
public static String SUFFIX_NAME_TS = ".ts";
public static String SUFFIX_NAME_TXT = ".txt";
public static String SUFFIX_NAME_M3U8 = ".m3u8";
/**
* E:\Ffmpeg\bin\ffmpeg.exe -allowed_extensions ALL -protocol_whitelist "file,http,crypto,tcp" -i G:\FFOutput\fileList.txt -c copy G:\FFOutput\test.mp4 包含key的解析命令
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
mergeTSFiles("E:\\Ffmpeg\\bin\\ffmpeg", new File("G:\\FFOutput"), false);
}
public static void mergeTSFiles(String ffmpegPath, File rootPath) throws IOException {
mergeTSFiles(ffmpegPath, rootPath, true);
}
/**
* 修复key缺失引号问题
*
* @param m3u8
* @throws IOException
*/
private static void repairM3U8Format(File m3u8) throws IOException {
if (SimpleFileUtils.isDir(m3u8)) {
return;
}
List<String> contentLines = FileUtils.readLines(m3u8);
for (int i = 0; i < contentLines.size(); i++) {
String content = contentLines.get(i);
int index = content.indexOf(KEY_TAG);
if (index != -1) {
if (!content.endsWith("\"")) {
contentLines.set(i, content.replaceFirst(SUFFIX_KEY, SUFFIX_KEY + "\""));
}
break;
}
}
FileUtils.writeLines(m3u8, contentLines);
}
public static void mergeTSFiles(String ffmpegPath, File rootPath, boolean isAddSuffix) throws IOException {
if (SimpleFileUtils.isFile(rootPath)) {
return;
}
if (StringUtils.isBlank(ffmpegPath)) {
ffmpegPath = "ffmpeg";
}
List<File> m3u8Files = SimpleFileUtils.searchFile(rootPath, SUFFIX_NAME_M3U8, false, false, true);
for (File f : m3u8Files) {
try {
File copyFile = new File(f.getParent(), String.format("%s_copy%s", SimpleFileUtils.getFileName(f), SUFFIX_NAME_M3U8));
FileUtils.copyFile(f, copyFile);
repairM3U8Format(copyFile);
Playlist playlist = new PlaylistParser(new FileInputStream(copyFile), Format.EXT_M3U, Encoding.UTF_8).parse();
List<TrackData> tracks = playlist.getMediaPlaylist().getTracks();
String tsPath = getTsPath(tracks);
String tsRootPath = getTsAbsolutePath(tracks);
if (StringUtils.isBlank(tsPath) || StringUtils.isBlank(tsRootPath)) {
continue;
}
File tsFullPath = new File(rootPath, tsPath);
if (SimpleFileUtils.isFile(tsFullPath)) {
throw new FileNotFoundException(tsFullPath.toString());
}
String tsFullPathStr = tsFullPath.getAbsolutePath();
if (isAddSuffix) {
SimpleFileUtils.addDirFilesSuffix(tsFullPath, SUFFIX_NAME_TS);
}
String content = FileUtils.readFileToString(copyFile);
content = content.replace(tsRootPath, "file:" + tsFullPath.toString().replace("\\", "/"));
FileUtils.write(copyFile, content);
String[] cmd = {"cmd", "/C",
String.format("start %s -allowed_extensions ALL -protocol_whitelist \"file,http,https,rtp,udp,tcp,tls,crypto\" -i %s -c copy %s", ffmpegPath,
"\"" + copyFile.getAbsolutePath() + "\"",
"\"" + tsFullPathStr.substring(0, tsFullPathStr.lastIndexOf('.')) + SUFFIX_NAME_MP4 + "\"")
};
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
throw new IOException(e);
}
}
}
/**
* 合并所有m3u8,无法处理加密过的视频
*
* @param ffmpegPath
* @param rootPath
* @param isAddSuffix
* @throws IOException
*/
@Deprecated
public static void mergeAllTSFiles(String ffmpegPath, File rootPath, boolean isAddSuffix) throws IOException {
if (SimpleFileUtils.isFile(rootPath)) {
return;
}
if (StringUtils.isBlank(ffmpegPath)) {
ffmpegPath = "ffmpeg";
}
List<File> m3u8Files = SimpleFileUtils.searchFile(rootPath, SUFFIX_NAME_M3U8, false, false, true);
for (File f : m3u8Files) {
PlaylistParser parser = new PlaylistParser(new FileInputStream(f), Format.EXT_M3U, Encoding.UTF_8);
try {
Playlist playlist = parser.parse();
List<TrackData> tracks = playlist.getMediaPlaylist().getTracks();
String tsPath = getTsPath(tracks);
if (StringUtils.isBlank(tsPath)) {
continue;
}
File tsFullPath = new File(rootPath, tsPath);
if (SimpleFileUtils.isFile(tsFullPath)) {
throw new FileNotFoundException(tsFullPath.toString());
}
String tsFullPathStr = tsFullPath.getAbsolutePath();
if (isAddSuffix) {
SimpleFileUtils.addDirFilesSuffix(tsFullPath, SUFFIX_NAME_TS);
}
String listFilePath = writeFileListText(rootPath, tsPath, tracks, isAddSuffix);
String[] cmd = {"cmd", "/C",
String.format("start %s -f concat -safe 0 -i %s -c copy %s -y", ffmpegPath,
"\"" + listFilePath + "\"",
"\"" + tsFullPathStr.substring(0, tsFullPathStr.lastIndexOf('.')) + SUFFIX_NAME_MP4 + "\"")
};
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
throw new IOException(e);
}
}
}
/**
* 获取ts文件路径名
*
* @param trackDataList
* @return
*/
private static String getTsPath(List<TrackData> trackDataList) {
if (CollectionUtils.isNotEmpty(trackDataList)) {
String uri = trackDataList.get(0).getUri();
if (StringUtils.isNotBlank(uri) && uri.startsWith("file://")) {
int end = uri.lastIndexOf('/');
if (end != -1) {
int start = uri.substring(0, end).lastIndexOf('/');
if (start != -1) {
return uri.substring(start + 1, end);
}
}
}
}
return null;
}
/**
* 获取ts全路径
*
* @param trackDataList
* @return
*/
private static String getTsAbsolutePath(List<TrackData> trackDataList) {
if (CollectionUtils.isNotEmpty(trackDataList)) {
String uri = trackDataList.get(0).getUri();
if (StringUtils.isNotBlank(uri) && uri.startsWith("file://")) {
int end = uri.lastIndexOf('/');
if (end != -1) {
return uri.substring(0, end);
} else {
return uri;
}
}
}
return null;
}
private static String writeFileListText(File savePath, String name, List<TrackData> trackDataList, boolean isContainsSuffix) throws IOException {
if (StringUtils.isBlank(name)) {
return null;
}
if (CollectionUtils.isNotEmpty(trackDataList)) {
FileUtils.forceMkdir(savePath);
File listFileText = new File(savePath, name.substring(0, name.lastIndexOf('.')) + "_list" + SUFFIX_NAME_TXT);
if (listFileText.exists()) {
throw new IOException(String.format("文件'%s'已存在", listFileText));
}
StringBuilder sb = new StringBuilder();
File tsPath = new File(savePath, name);
if (tsPath.exists()) {
trackDataList.stream().forEach((trackData) -> {
String uri = trackData.getUri();
if (StringUtils.isNotBlank(uri) && uri.startsWith("file://")) {
int index = uri.lastIndexOf('/');
if (index != -1) {
sb.append("file ")
.append("'")
.append(tsPath.getAbsolutePath() + File.separator + uri.substring(index + 1));
if (isContainsSuffix) {
sb.append(SUFFIX_NAME_TS);
}
sb.append("'").append(System.lineSeparator());
}
}
});
try (BufferedWriter bw = new BufferedWriter(new FileWriter(listFileText))) {
bw.write(sb.toString());
} catch (IOException e) {
throw e;
}
return listFileText.getAbsolutePath();
}
}
return null;
}
/**
* 合并ts文件
*
* @param ffmpegPath ffmpeg路径
* @param listFile 列表文件路径
* @param savePath 保存路径
* @param videoName 生成视频名称
* @throws IOException
*/
public static void mergeTSFiles(String ffmpegPath, File listFile, File savePath, String videoName) throws IOException {
if (SimpleFileUtils.isDir(listFile)) {
return;
}
if (savePath.isFile()) {
return;
}
if (!savePath.exists()) {
savePath.mkdirs();
}
File mp4 = new File(savePath, videoName + SUFFIX_NAME_MP4);
if (mp4.exists()) {
throw new FileExistsException(String.format("文件'%s'已存在", mp4));
}
String[] cmd = {"cmd", "/C",
String.format("start %s -f concat -safe 0 -i %s -c copy %s -y",
ffmpegPath, listFile.getAbsolutePath(),
mp4.getAbsolutePath())
};
Runtime.getRuntime().exec(cmd);
}
/**
* 生成ts合并列表
*
* @param file 目标文件夹
* @param suffix 后缀名
* @throws IOException
*/
public static String writeFileListText(File file, String savePath, String suffix) throws IOException {
if (StringUtils.isBlank(suffix) || SimpleFileUtils.isFile(file)) {
return null;
}
File path = new File(savePath);
if (!path.exists()) {
path.mkdirs();
}
File fileList = new File(path, file.getName() + "_list" + SUFFIX_NAME_TXT);
if (fileList.exists()) {
throw new FileExistsException(String.format("文件'%s'已存在", fileList));
}
File[] files = file.listFiles(new SuffixNameFilter(suffix));
try (BufferedWriter bw = new BufferedWriter(new FileWriter(fileList))) {
for (File f : files) {
bw.write("file '" + f.getPath() + "'");
bw.newLine();
}
}
return fileList.getPath();
}
}
总结
这里只是简单做个合并ts的小工具,如果有错误的地方欢迎大家指出(〃‘▽’〃)