Java M3U8 视频下载合并

资源分享 (10) 2025-10-17 13:04:52

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();
        }
    }
}

评论
User Image
提示:请评论与当前内容相关的回复,广告、推广或无关内容将被删除。

相关文章
Java实现 M3U8视频下载合并jdk17package com.cccin1.project;import java.io.*;import java.ne
前言       最近学习很热衷于学习解析视频,本次解析的是腾讯视频,根据查阅相关资料,目前已经实现腾讯视频真实地址解析,并且能够下载腾讯视频
Xmind 8 pro 3.3.7 绿色免安装版下载信息,你是否还在网络大海中苦苦寻找Xmind 8 pro 绿色版?来看看吧
GD音乐台 - GD Studio's Online Music PlatformMP3下载,MUSIC下载,MP3免费下载,歌曲免费下载
视频压缩软件名称VidCoder视频压缩工具VidCoder简介 VidCoder是一款适用于Windows的开源DVD /蓝光翻录和视频转码应用程序
webstorm2021.3 激活版下载       webstorm2021.3是JetBrains旗下的JavaScript开发者工具,拥有先进而智能的集成开发环境(IDE),主要用于Web...
官方指导Upgrade from 8 to 9 - Proxmox VE实操步骤更新到8.x最新版更新当前系统到PVE8.4.1X(也就是8的最新版)apt u
视频封装/格式常见的封装格式:MP4/MKV....MP4/MKV是你下载的视频文件最常见的种类(文件后缀是.mp4/.mkv)。这些文件其实类似一个包裹,它的
yum install java-1.8.0-openjdk java-1.8.0-openjdk-devel注意必须安装java-1.8.0-openjdk-devel,否则没有javac命令
1.iperf3简介iPerf3是用于主动测试IP网络上最大可用带宽的工具。它支持时序、缓冲区、协议(TCP,UDP,SCTP与IPv4和IPv6)有关的各种参
群晖+爱普生M1108实现无线打印爱普生M1108打印机简介 通过官网给出的信息可以看出,这款打印机本身是不支持无线网络打印的,只有一个usb接口。 将打印机接入群晖打印机通电,然后把打印机的u...
前言距离springfox的swagger2.x 以及3.0.0 长久等待,等来了springdoc的swagger 3 为啥是3是因为支持openapi3.0
概述Ollama官方最新版0.5.7,默认是不支持AMD 780m(代号:gfx1103)集成显卡的,已知方法都是基于一个开源项目ollama-for-amd来
JDK8 从某年某月开始变成个人免费,商用收费了,以至于网上经常搜索到最后一个免费版是jdk那个版本。下面通过Oracle 官方文档来看最后的免费午餐版本是 J
解决方法1、把虚拟机关机。2、打开虚拟机目录,找到.vmx文件并通过记事本打开,我这是USB-918.vmx3、将usb.restrictions.defaultAllow="FALSE"改为u...