Java M3U8 视频下载合并
Java实现 M3U8视频下载合并
jdk17
package com.cccin1.project;
import java.io.*;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* M3U8 下载器 - 基于 JDK 17
* 功能:解析 M3U8 文件,下载所有 TS 分片,并合并为单个视频文件。
*/
public class M3u8Downloader {
// HTTP 客户端 (支持 HTTP/2)
private final HttpClient httpClient;
// 下载线程池
private final ExecutorService executorService;
// 用户代理 (模拟浏览器)
private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
public M3u8Downloader() {
this.httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(10))
.build();
// 使用固定大小线程池,可根据需要调整
this.executorService = Executors.newFixedThreadPool(10);
}
/**
* 主要下载方法
*
* @param m3u8Url M3U8 播放列表的 URL
* @param outputDir 输出目录
* @param fileName 输出文件名 (不含扩展名)
* @throws Exception 下载或合并过程中可能抛出异常
*/
public void download(String m3u8Url, String outputDir, String fileName) throws Exception {
Path outputPath = Paths.get(outputDir);
Files.createDirectories(outputPath);
System.out.println("开始解析 M3U8: " + m3u8Url);
List<String> tsUrls = parseM3u8(m3u8Url);
System.out.println("共找到 " + tsUrls.size() + " 个 TS 分片");
if (tsUrls.isEmpty()) {
throw new RuntimeException("未找到任何有效的 TS 分片 URL");
}
// 创建临时目录存放分片
Path tempDir = Files.createTempDirectory("m3u8_temp_");
System.out.println("临时文件目录: " + tempDir);
try {
// 并发下载所有 TS 文件
List<Path> downloadedFiles = downloadTsFiles(tsUrls, tempDir).get();
// 合并所有 TS 文件
Path finalOutput = outputPath.resolve(fileName + ".mp4"); // 也可以是 .ts
mergeTsFiles(downloadedFiles, finalOutput);
System.out.println("✅ 下载并合并完成: " + finalOutput.toAbsolutePath());
} finally {
// 清理临时目录
deleteDirectory(tempDir);
}
}
/**
* 解析 M3U8 文件,提取所有 TS 分片的绝对 URL
*
* @param m3u8Url M3U8 文件 URL
* @return TS 分片 URL 列表
* @throws IOException 网络或解析错误
*/
private List<String> parseM3u8(String m3u8Url) throws IOException {
String content = fetchContent(m3u8Url);
List<String> tsUrls = new ArrayList<>();
// 获取基础 URL (用于处理相对路径)
URI baseUri = URI.create(m3u8Url);
String baseUrl = m3u8Url.substring(0, m3u8Url.lastIndexOf('/') + 1);
// 简单的正则匹配 TS 文件 (更健壮的解析应使用专门的 M3U8 解析库)
Pattern tsPattern = Pattern.compile("^(?!#)(.+?\\.ts.*)$", Pattern.MULTILINE);
Matcher matcher = tsPattern.matcher(content);
while (matcher.find()) {
String tsPath = matcher.group(1).trim();
// 处理相对路径
if (tsPath.startsWith("http://") || tsPath.startsWith("https://")) {
tsUrls.add(tsPath);
} else {
// 拼接成绝对 URL
tsUrls.add(baseUrl + tsPath);
}
}
return tsUrls;
}
/**
* 获取指定 URL 的文本内容
*
* @param url URL
* @return 响应文本
* @throws IOException 网络错误
*/
private String fetchContent(String url) throws IOException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(30))
.header("User-Agent", USER_AGENT)
.GET()
.build();
try {
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode() + " for URL: " + url);
}
return response.body();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("请求被中断: " + url, e);
}
}
/**
* 并发下载所有 TS 文件
*
* @param tsUrls TS 文件 URL 列表
* @param tempDir 临时存储目录
* @return CompletableFuture 包含下载完成的文件路径列表
*/
private CompletableFuture<List<Path>> downloadTsFiles(List<String> tsUrls, Path tempDir) {
List<CompletableFuture<Path>> futures = new ArrayList<>();
for (int i = 0; i < tsUrls.size(); i++) {
String tsUrl = tsUrls.get(i);
// 使用索引作为文件名,保证顺序
Path filePath = tempDir.resolve(String.format("%05d.ts", i));
CompletableFuture<Path> future = CompletableFuture.supplyAsync(() -> {
try {
downloadFile(tsUrl, filePath);
System.out.println("✅ 下载完成: " + tsUrl);
return filePath;
} catch (Exception e) {
System.err.println("❌ 下载失败: " + tsUrl + " | 错误: " + e.getMessage());
// 下载失败返回 null,后续合并时跳过
return null;
}
}, executorService);
futures.add(future);
}
// 将所有 CompletableFuture 组合成一个
CompletableFuture<Void> allDone = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
return allDone.thenApply(v ->
futures.stream()
.map(CompletableFuture::join) // 获取每个 future 的结果
.filter(path -> path != null) // 过滤掉下载失败的 (null)
.sorted() // 按文件名排序 (确保顺序)
.toList()
);
}
/**
* 下载单个文件
*
* @param url 文件 URL
* @param path 本地保存路径
* @throws Exception 下载错误
*/
private void downloadFile(String url, Path path) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(60)) // TS 文件可能较大
.header("User-Agent", USER_AGENT)
.GET()
.build();
HttpResponse<Path> response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(path));
if (response.statusCode() != 200) {
throw new IOException("下载失败: HTTP " + response.statusCode() + " - " + url);
}
}
/**
* 合并多个 TS 文件为一个文件
*
* @param tsFiles 已下载的 TS 文件路径列表 (已排序)
* @param outputFile 输出文件路径
* @throws IOException 文件操作错误
*/
private void mergeTsFiles(List<Path> tsFiles, Path outputFile) throws IOException {
System.out.println("开始合并 " + tsFiles.size() + " 个文件...");
try (FileChannel outChannel = FileChannel.open(outputFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
for (Path tsFile : tsFiles) {
try (FileChannel inChannel = FileChannel.open(tsFile, StandardOpenOption.READ)) {
// 直接传输字节,高效
inChannel.transferTo(0, inChannel.size(), outChannel);
}
}
}
System.out.println("✅ 合并完成: " + outputFile.getFileName());
}
/**
* 递归删除目录及其内容
*
* @param path 目录路径
* @throws IOException 删除失败
*/
private void deleteDirectory(Path path) throws IOException {
if (Files.exists(path)) {
Files.walk(path)
.sorted(java.util.Comparator.reverseOrder())
.forEach(p -> {
try {
Files.delete(p);
} catch (IOException e) {
System.err.println("清理临时文件失败: " + p + " | " + e.getMessage());
}
});
}
}
/**
* 关闭资源
*/
public void close() {
if (executorService != null && !executorService.isShutdown()) {
executorService.shutdown();
}
}
// ==================== 使用示例 ====================
public static void main(String[] args) {
// ⚠️ 请替换为实际的 M3U8 URL
String m3u8Url = "https://example.com/path/to/playlist.m3u8";
String outputDir = "./downloads";
String fileName = "video_output";
M3u8Downloader downloader = new M3u8Downloader();
try {
downloader.download(m3u8Url, outputDir, fileName);
} catch (Exception e) {
System.err.println("下载过程发生错误: " + e.getMessage());
e.printStackTrace();
} finally {
downloader.close();
}
}
}
https://blog.xqlee.com/article/2510171304422374.html
评论