使用Java实现M3U8视频文件合并的完整指南
一、M3U8文件格式简介
M3U8是一种基于HTTP Live Streaming (HLS)协议的播放列表文件格式,常用于视频点播和直播领域。它本质上是M3U播放列表的UTF-8编码版本,主要包含以下内容:
- 文件头信息:
#EXTM3U
标识 - 版本信息:
#EXT-X-VERSION
- 媒体序列信息:
#EXT-X-MEDIA-SEQUENCE
- 目标时长:
#EXT-X-TARGETDURATION
- 分片列表:
#EXTINF
标签指示的TS文件片段
二、M3U8合并的基本原理
合并M3U8文件主要涉及以下几个步骤:
- 解析M3U8索引文件,获取所有TS分片URL
- 按顺序下载所有TS分片文件
- 将TS文件按正确顺序合并为完整视频文件
- (可选)转换为MP4等其他格式
三、Java实现M3U8合并的完整代码
1. 添加必要的依赖
首先,在pom.xml
中添加以下依赖:
<dependencies><!-- HTTP客户端 --><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.13</version></dependency><!-- 文件操作 --><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.11.0</version></dependency><!-- 视频处理 --><dependency><groupId>org.bytedeco</groupId><artifactId>javacv-platform</artifactId><version>1.5.7</version></dependency>
</dependencies>
2. M3U8解析器实现
import org.apache.commons.io.FileUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;public class M3U8Merger {// 解析M3U8文件,获取所有TS片段URLpublic static List<String> parseM3U8(String m3u8Url) throws IOException, URISyntaxException {List<String> tsUrls = new ArrayList<>();// 获取M3U8文件内容String m3u8Content = downloadFileAsString(m3u8Url);// 解析TS片段Pattern pattern = Pattern.compile("(?m)^[^#].*\\.ts$");Matcher matcher = pattern.matcher(m3u8Content);// 构建完整的TS URLURI baseUri = new URI(m3u8Url).resolve(".");while (matcher.find()) {String tsPath = matcher.group();URI tsUri = baseUri.resolve(tsPath);tsUrls.add(tsUri.toString());}return tsUrls;}// 下载文件内容为字符串private static String downloadFileAsString(String fileUrl) throws IOException {try (CloseableHttpClient httpClient = HttpClients.createDefault()) {HttpGet httpGet = new HttpGet(fileUrl);try (CloseableHttpResponse response = httpClient.execute(httpGet);InputStream inputStream = response.getEntity().getContent();BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {StringBuilder content = new StringBuilder();String line;while ((line = reader.readLine()) != null) {content.append(line).append("\n");}return content.toString();}}}// 下载单个TS文件private static void downloadTsFile(String tsUrl, String outputPath) throws IOException {try (CloseableHttpClient httpClient = HttpClients.createDefault()) {HttpGet httpGet = new HttpGet(tsUrl);try (CloseableHttpResponse response = httpClient.execute(httpGet);InputStream inputStream = response.getEntity().getContent()) {FileUtils.copyInputStreamToFile(inputStream, new File(outputPath));}}}// 合并所有TS文件public static void mergeTsFiles(List<String> tsUrls, String outputDir, String outputFilename) throws IOException {// 确保输出目录存在Files.createDirectories(Paths.get(outputDir));// 临时目录存放下载的TS文件String tempDir = outputDir + File.separator + "temp_ts_files";Files.createDirectories(Paths.get(tempDir));// 下载所有TS文件List<String> tsFilePaths = new ArrayList<>();for (int i = 0; i < tsUrls.size(); i++) {String tsUrl = tsUrls.get(i);String tsPath = tempDir + File.separator + "segment_" + i + ".ts";downloadTsFile(tsUrl, tsPath);tsFilePaths.add(tsPath);}// 合并TS文件String mergedTsPath = outputDir + File.separator + "merged.ts";try (FileOutputStream fos = new FileOutputStream(mergedTsPath);BufferedOutputStream mergingStream = new BufferedOutputStream(fos)) {for (String tsFilePath : tsFilePaths) {Files.copy(Paths.get(tsFilePath), mergingStream);}}// 转换为MP4格式(可选)String finalOutputPath = outputDir + File.separator + outputFilename;convertToMp4(mergedTsPath, finalOutputPath);// 清理临时文件FileUtils.deleteDirectory(new File(tempDir));Files.deleteIfExists(Paths.get(mergedTsPath));}// 将TS文件转换为MP4(需要FFmpeg支持)private static void convertToMp4(String inputPath, String outputPath) throws IOException {try {ProcessBuilder pb = new ProcessBuilder("ffmpeg", "-i", inputPath, "-c", "copy", outputPath);pb.redirectErrorStream(true);Process process = pb.start();process.waitFor();} catch (Exception e) {throw new IOException("FFmpeg转换失败,请确保已安装FFmpeg并添加到系统PATH", e);}}public static void main(String[] args) {try {// 示例使用String m3u8Url = "https://example.com/video/playlist.m3u8";String outputDir = "D:/videos";String outputFilename = "merged_video.mp4";System.out.println("开始解析M3U8文件...");List<String> tsUrls = parseM3U8(m3u8Url);System.out.println("找到 " + tsUrls.size() + " 个TS片段");System.out.println("开始下载并合并TS文件...");mergeTsFiles(tsUrls, outputDir, outputFilename);System.out.println("视频合并完成,保存为: " + outputDir + File.separator + outputFilename);} catch (Exception e) {e.printStackTrace();}}
}
四、代码解析与关键点说明
1. M3U8解析逻辑
- 使用正则表达式
(?m)^[^#].*\.ts$
匹配不以#开头的.ts文件行 - 正确处理相对路径,通过URI.resolve()方法构建完整的TS文件URL
2. 文件下载处理
- 使用Apache HttpClient进行HTTP请求
- 使用commons-io的FileUtils简化文件操作
- 采用流式下载避免内存溢出
3. 文件合并策略
- 先下载所有TS片段到临时目录
- 按顺序将TS文件二进制合并
- 使用FFmpeg将合并后的TS转换为MP4格式
4. 异常处理
- 处理网络请求异常
- 处理文件IO异常
- 处理FFmpeg转换异常
五、高级功能扩展
1. 多线程下载加速
// 使用ExecutorService实现多线程下载
ExecutorService executor = Executors.newFixedThreadPool(8);
List<Future<?>> futures = new ArrayList<>();for (int i = 0; i < tsUrls.size(); i++) {final int index = i;futures.add(executor.submit(() -> {String tsUrl = tsUrls.get(index);String tsPath = tempDir + File.separator + "segment_" + index + ".ts";downloadTsFile(tsUrl, tsPath);}));
}// 等待所有下载完成
for (Future<?> future : futures) {future.get();
}
executor.shutdown();
2. 断点续传支持
// 检查本地已下载的文件
File tsFile = new File(tsPath);
if (tsFile.exists() && tsFile.length() > 0) {System.out.println("文件已存在,跳过下载: " + tsPath);return;
}// 使用Range头实现断点续传
HttpGet httpGet = new HttpGet(tsUrl);
if (tsFile.exists()) {long downloadedLength = tsFile.length();httpGet.setHeader("Range", "bytes=" + downloadedLength + "-");
}try (CloseableHttpResponse response = httpClient.execute(httpGet);InputStream inputStream = response.getEntity().getContent();FileOutputStream fos = new FileOutputStream(tsFile, true)) {byte[] buffer = new byte[8192];int bytesRead;while ((bytesRead = inputStream.read(buffer)) != -1) {fos.write(buffer, 0, bytesRead);}
}
3. 加密TS文件的处理
如果M3U8中的TS文件是加密的,需要处理AES加密:
// 解析加密key
Pattern keyPattern = Pattern.compile("#EXT-X-KEY:METHOD=AES-128,URI=\"(.+?)\"");
Matcher keyMatcher = keyPattern.matcher(m3u8Content);
String keyUrl = null;
if (keyMatcher.find()) {keyUrl = keyMatcher.group(1);
}// 下载解密key
byte[] key = null;
if (keyUrl != null) {try (CloseableHttpClient httpClient = HttpClients.createDefault()) {HttpGet httpGet = new HttpGet(keyUrl);try (CloseableHttpResponse response = httpClient.execute(httpGet);InputStream inputStream = response.getEntity().getContent()) {ByteArrayOutputStream buffer = new ByteArrayOutputStream();byte[] data = new byte[1024];int nRead;while ((nRead = inputStream.read(data, 0, data.length)) != -1) {buffer.write(data, 0, nRead);}key = buffer.toByteArray();}}
}// 解密TS文件
if (key != null) {Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");SecretKeySpec keySpec = new SecretKeySpec(key, "AES");IvParameterSpec ivSpec = new IvParameterSpec(new byte[16]); // 通常IV是16个0cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);byte[] decryptedData = cipher.doFinal(Files.readAllBytes(Paths.get(tsPath)));Files.write(Paths.get(tsPath), decryptedData);
}
六、常见问题及解决方案
1. 网络请求失败
- 问题:部分TS文件下载失败
- 解决:实现重试机制,设置合理的超时时间
// 带重试的下载方法
private static void downloadWithRetry(String url, String outputPath, int maxRetries) throws IOException {int retryCount = 0;while (retryCount < maxRetries) {try {downloadTsFile(url, outputPath);return;} catch (IOException e) {retryCount++;if (retryCount == maxRetries) {throw e;}try {Thread.sleep(1000 * retryCount); // 指数退避} catch (InterruptedException ie) {Thread.currentThread().interrupt();throw new IOException("下载被中断", ie);}}}
}
2. 合并后的视频不同步
- 问题:音视频不同步或时间戳错误
- 解决:使用FFmpeg重新封装而非简单二进制合并
// 使用FFmpeg合并TS文件(更可靠的方法)
private static void mergeWithFFmpeg(List<String> tsFilePaths, String outputPath) throws IOException {// 创建文件列表File listFile = File.createTempFile("ffmpeg-list", ".txt");try (BufferedWriter writer = new BufferedWriter(new FileWriter(listFile))) {for (String tsPath : tsFilePaths) {writer.write("file '" + tsPath + "'");writer.newLine();}}// 执行FFmpeg命令try {ProcessBuilder pb = new ProcessBuilder("ffmpeg", "-f", "concat", "-safe", "0", "-i", listFile.getAbsolutePath(), "-c", "copy", outputPath);pb.redirectErrorStream(true);Process process = pb.start();process.waitFor();} catch (Exception e) {throw new IOException("FFmpeg合并失败", e);} finally {listFile.delete();}
}
3. 内存不足问题
- 问题:处理大文件时内存溢出
- 解决:使用流式处理而非全量加载到内存
// 流式合并文件
private static void streamMergeFiles(List<String> inputFiles, String outputFile) throws IOException {try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(outputFile))) {byte[] buffer = new byte[8192];for (String inputFile : inputFiles) {try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(inputFile))) {int bytesRead;while ((bytesRead = in.read(buffer)) != -1) {out.write(buffer, 0, bytesRead);}}}}
}
七、总结
本文详细介绍了如何使用Java实现M3U8视频文件的合并,包括:
- M3U8文件解析与TS片段URL提取
- 多线程下载加速实现
- TS文件合并与格式转换
- 加密视频的解密处理
- 常见问题的解决方案
通过这个完整的解决方案,你可以轻松地将分片的M3U8视频合并为完整的MP4文件。根据实际需求,你可以进一步扩展功能,如添加进度显示、支持更多视频格式转换等。