目录
一、前言
目前大多数在线视频网站,很多都是使用.m3u8格式,这样,及时我们把视频下载到本地后,得到的依然是零散的ts格式的文件,如何把这些文件合并生成一个MP4文件呢,在这里分享一个最近撸码解决的办法。
二、需求来源
最近在参加一个在线网课,和目前大多数在线视频网站一样,使用的是.m3u8格式,.m3u8格式的具体实现原理和详细的介绍这里就不进行说明了,大致的原理就是把原来一整个大的视频文件,拆分成多个ts格式的小的视频文件,然后再按顺序加载到用户浏览器中播放,这一点可以再浏览器的控制台的网络请求中检测到详细:
为了方便学习,我把在线课程的视频下载到了本地,打开下载的内容是这样的,得到一个.m3u8文件,和一大堆ts文件,现在为了方便文件的管理和播放,我需要把这些ts文件合并成一个MP4文件,又不想去下写乱七八糟的工具,怎么办呢?
这时候很多人第一想到的是大名鼎鼎的ffmpeg,没错,我也想到了ffmpeg,着多简单呀,不就是合并ts文件嘛,一行命令就可以了。
注意:本文中的ffmpeg命令,需要先安装ffmpeg,安装的具体步骤可以参考文末的附录说明。
ffmpeg -i "concat:1.ts|2.ts|3.ts" -c copy output.mp4
# 或者把ts文件写到一个txt文件(input_list.txt)中,做一个文件清单,然后用这个命令
# file '1.ts'
# file '2.ts'
# file '3.ts'
ffmpeg -f concat -i input_list.txt -c copy output.mp4
但是,现在有4、5百个ts文件,我总不能一个一个写吧……于是,你就想到了这个命令,然后开开心心的打开mp4文件,然后你很可能会发现,MP4的视频很可能并没有按照ts的实际顺序来合并,造成视频内容乱了。
# 把folder文件夹下的所有ts文件做合并
ffmpeg -safe 0 -i "concat:folder/*.ts" -c copy output.mp4
三、解决方案
我们如何保证TS文件的顺序不出错呢?打开.m3u8文件,看看里面的内容,里面每一行的注释这里就不详细介绍了,如果想做进一步的了解,请自行问度娘吧。.m3u8文件中的ts文件的顺序,就是视频播放时的加载顺序,我们按照里面的顺序进行合并,肯定就不会错了。
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:18
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:17.4508,
index/0.ts
#EXTINF:8.34167,
index/1.ts
#EXTINF:8.34168,
index/2.ts
#EXTINF:13.7471,
index/3.ts
#EXTINF:8.34168,
index/4.ts
#EXTINF:8.34168,
index/5.ts
#EXTINF:12.012,
index/6.ts
……
现在解决方案已经很明朗了,我们需要使用下面的命令来合并生成MP4文件:
ffmpeg -f concat -i input_list.txt -c copy output.mp4
现在关键的问题是要生成 input_list.txt 文件,实现逻辑是读取.m3u8文件,按照原来的顺序把每一行的index/*.ts,加上“file”前缀,变成file 'index/*.ts',然后写入到新的txt文件。
要实现这个可以用两种方法:
1、batch/powershell命令
编写命令脚本,实现对.m3u8文件每一行的遍历,然后创建新的ts文件清单文件,这个做法要求对命令的语法什么的要求比较高,而且其中的逻辑控制,可能无法满足最终的需求,所以我放弃了这个方法(主要是对命令确实不熟(>_<) )
# 使用for /f循环读取xxx.m3u8文件,对每一行进行处理
for /f ["options"] %%i in ('xxx.m3u8') do (
# 每一行进行处理的逻辑
)
2、徒手撸个应用
考虑到是windows操作系统中,所以就用C#写个简单的程序来跑吧,既然是用到代码,那可操作性就很高了,实现的逻辑步骤主要有:
(1)扫描当前目录,查找.m3u8文件,如果找到,则解析当前目录的.m3u8文件。
(2)如果当前目录没有.m3u8文件,则遍历当前的子目录,查找子目录的.m3u8文件进行解析。
(3)解析.m3u8文件,每个.m3u8文件生成一个对应的临时ts文件清单,然后根据文件清单合并生成一个MP4文件。
#主要的代码
(1)从.m3u8文件中提取ts文件List,这里直接去除以#符号大头的行,具体逻辑可以根据实际情况继续完善。
/// <summary>
/// 根据m3u8文件路径,获取ts文件路径序列
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
private static List<string> GetTsFileByM3u8(string filePath)
{
List<string> list = new List<string>();
using (StreamReader reader = new StreamReader(filePath))
{
string line;
// 逐行读取文件内容
while ((line = reader.ReadLine()) != null)
{
if (!line.StartsWith("#"))
{
list.Add(line);
}
}
}
return list;
}
(2)启动ffmpeg,根据临时生成的ts文件描述文件,合并输出MP4文件。
/// <summary>
/// 运行ffmpeg命令,合并生成MP4文案
/// </summary>
/// <param name="fileListPath">ts文件清单说明txt文件</param>
/// <param name="outputFile">合并输出文件</param>
private static void ExecuFFMpeg(string fileListPath, string outputFile)
{
if (!File.Exists(fileListPath))
{
//throw new Exception(String.Format("{0}文件不存在!", fileListPath));
Console.WriteLine(String.Format("缓存文件不存在:{0}", fileListPath));
}
else
{
outputFile = GetFileName(outputFile);
List<string> ffmpegArguments = new List<string>
{
"-f", "concat",
"-safe", "0",
"-i", fileListPath,
"-c", "copy",
outputFile
};
try
{
// 合并FFmpeg命令和参数
string ffmpegCommand = "ffmpeg";
string ffmpegCmdLine = string.Join(" ", ffmpegArguments);
Console.WriteLine("启动ffmpeg进程,开始合并,请稍后……");
//// 创建并配置Process对象
Process ffmpegProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = ffmpegCommand,
Arguments = ffmpegCmdLine,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
DateTime now = DateTime.Now;
// 启动FFmpeg进程并等待其退出
ffmpegProcess.Start();
string ffmpegOutput = ffmpegProcess.StandardOutput.ReadToEnd();
string ffmpegError = ffmpegProcess.StandardError.ReadToEnd();
ffmpegProcess.WaitForExit();
DateTime end = DateTime.Now;
// 输出FFmpeg的结果
Console.WriteLine("【FFmpeg Output】:");
Console.WriteLine(ffmpegOutput);
if (!string.IsNullOrEmpty(ffmpegError))
{
Console.WriteLine("【FFmpeg Error】:");
Console.WriteLine(ffmpegError);
}
//outputFile
Console.WriteLine(String.Format("合并完成,已生成{0}文件", outputFile));
Console.WriteLine(String.Format("耗时 {0} 秒", (end - now).TotalSeconds));
}
catch (Exception err)
{
throw err;
}
finally
{
// 清理临时文件
// 可以在程序结束时删除临时文件,或者根据需要保留
// File.Delete(tempFileListPath);
}
}
}
代码有点长,所以不在文章里贴出来了,完整代码已经上传,具体可以下载下来参考参考,主要的地方都做了注释,如果要使用,可以根据自己的需要进行修改后运行。
#运行效果
编译输出后,生成FFMpegTsMerged.exe运行程序,然后拷贝到.m3u8文件同级目录,运行看看效果,运行后生成第一章的1.mp4文件和temp_filelist_1_index.txt临时文件。(index中是.m3u8中所包含的所有ts文件)
把FFMpegTsMerged.exe拷贝到上一级目录,运行看看效果,sec1-sec4分别对应第一到第四节课的视频。
运行程序:
四、结束语
基本达到要求了,代码里还有好多异常判断拉之类的逻辑要处理,不过也不想完善了,目前已经帮我把视频生成了就可以了。
附录:安装ffmpeg
1、下载
官方下载地址:Download FFmpeg
下载:Builds - CODEX FFMPEG @ gyan.dev
选ffmpeg-release-full-shared.7z下载,可能有点慢,耐心等待,下载完成后解压出来。
注意:不要下错了去下个源码回来。
2、安装
解释也没有什么所谓的安装了,就是把ffmpeg加到环境变量Path中,确保ffmpeg可以直接运行就可以了。
3、测试命令
运行 ffmpeg -version 命令,如果没有提示命令不存在或者报错,着说明配置正常。