关于m3u8的一个小工具


前言

    由于最近打算换手机,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协议


四、代码实现

git地址

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的小工具,如果有错误的地方欢迎大家指出(〃‘▽’〃)

作为一名AI语言模型,我无法进行实战操作。但我可以为您提供相关的思路和技术方案: 1. RTSP转RTMP RTSP是实时流传输协议,而RTMP是实时消息传输协议。它们都是用于实时传输音视频流的协议,但是它们的传输方式不同。RTSP是基于TCP的协议,而RTMP是基于TCP或UDP的协议。 要将RTSP流转换为RTMP流,您需要使用一个转换器。其中一个常用的转换器是FFmpeg。FFmpeg是一个开源的多媒体框架,可以处理各种音视频格式,并且支持将RTSP流转换为RTMP流。 以下是使用FFmpeg将RTSP流转换为RTMP流的命令: ffmpeg -i rtsp://xxx.xxx.xxx.xxx:554/xxx -c copy -f flv rtmp://xxx.xxx.xxx.xxx/live/xxx 其中,rtsp://xxx.xxx.xxx.xxx:554/xxx是RTSP流的地址,rtmp://xxx.xxx.xxx.xxx/live/xxx是RTMP流的地址。 2. RTSP转M3U8 M3U8是一种基于HTTP的流协议,它是一种多媒体播放列表,用于指定流媒体文件的播放顺序和位置。要将RTSP流转换为M3U8流,您需要使用一个转换器,如FFmpeg。 以下是使用FFmpeg将RTSP流转换为M3U8流的命令: ffmpeg -i rtsp://xxx.xxx.xxx.xxx:554/xxx -c:v libx264 -c:a aac -hls_time 10 -hls_list_size 6 -hls_wrap 10 -start_number 1 /var/www/html/live/xxx.m3u8 其中,rtsp://xxx.xxx.xxx.xxx:554/xxx是RTSP流的地址,/var/www/html/live/xxx.m3u8M3U8流的地址。 3. 集成到Java应用程序中 要将上述转换器集成到Java应用程序中,您可以使用Java的ProcessBuilder类来执行命令行命令。以下是一个示例代码: String command = "ffmpeg -i rtsp://xxx.xxx.xxx.xxx:554/xxx -c copy -f flv rtmp://xxx.xxx.xxx.xxx/live/xxx"; ProcessBuilder processBuilder = new ProcessBuilder(command.split(" ")); processBuilder.redirectErrorStream(true); Process process = processBuilder.start(); InputStream inputStream = process.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); String line; while ((line = bufferedReader.readLine()) != null) { System.out.println(line); } process.waitFor(); 要将RTSP转换为M3U8,您可以使用类似的代码,只需更改命令即可。 以上是一个基本的思路和技术方案,具体实现还需要根据您的具体需求进行调整。希望能对您有所帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穷儒公羊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值