前言
前段时间发现了一个很喜欢的视频,可惜网站不让下载,简单看了一下视频是被切片成m4s
格式的流文件,初步想法是将所有的流文件下载下来然后使用ffmpeg
合并成一个完整的mp4
,于是写了一段脚本来实现一下,电脑没有配python
环境,所以使用JavaScript
实现,合并功能需要安装ffmpeg
,没有的小伙伴自行安装哦
前置知识
m4s
文件(复制百度)
M4S
文件是使用MPEG-DASH 流技术
通过 Internet 流式传输的一小段视频。它包含二进制数据形式的视频片段。接收应用程序(通常是网络浏览器或媒体播放器)按接收顺序播放这些片段。第一个 M4S 段由它包含的初始化数据标识。在summary
中,m4s
文件是完整文件的单个小媒体片段。M4S
文件基于ISO
基础媒体文件 (ISOBMFF
) 格式。大文件的这些小片段可以通过HTTP
独立下载。因此,如果您有一个大的MP4
电影文件,则可以使用MPEG-DASH
(HTTP
上的动态自适应流式传输)技术将其分段为M4S
分段文件,从而对其进行流式传输。如果将此大型电影文件作为M4S
下载到光盘,则会下载多个M4S
文件。如果将所有这些.m4s
段连接起来,就会生成一个完整的可播放文件。除非文件的第一个初始化段也可用,否则媒体播放器无法播放文件。
思路整理
- 找到目标
m4s
文件的接口,观察接口规律,拼接URL
批量下载 - 然后将文件写入本地,再遍历目录生成
ffmpeg
合并用的文化列表目录 - 然后调用
ffmpeg
终端命令合并 - 最后清理临时文件
开始实现
首先观察到目标m4s
文件的url
格式都是https://xxxxxx/1080.mp4/seg-1-v1-a1.m4s
/ https://xxxxxx/1080.mp4/seg-2-v1-a1.m4s
等等,猜测只是通过目标的序号来管理分片,那考虑使用循环来批量下载,先写几个函数来处理基本的功能,例如下载文件 / 生成临时目录 / 本地写入 / 清理临时文件等
请求函数
const fetchData = async (url) => {
try {
let response = await fetch(url, {
method: 'GET',
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60"
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
let m4sData = await response.blob();
if (m4sData instanceof Blob) {
console.log(m4sData.size); // 打印 Blob 对象的大小
} else {
console.log("m4sData.data 不是一个 Blob 对象");
}
return m4sData;
} catch (error) {
console.log("get_m4sData下载失败");
console.log(error);
}
};
本地写入的函数
const writeFile = async (fileName, file) => {
fs.writeFile(fileName, file, (err) => {
if (err) {
console.log("写入失败:", err);
return
}
console.log(`${fileName}写入成功`);
})
}
生成临时目录的函数
const generateFileList = () => {
// 获取 assets 目录下所有目标文件
const files = fs.readdirSync(folderPath)
.filter(file => file.endsWith('.ts'))
.sort((a, b) => {
// 提取文件名中的数字部分进行比较
const numA = parseInt(a.match(/seg-(\d+)-v1-a1\.ts/)[1], 10);
const numB = parseInt(b.match(/seg-(\d+)-v1-a1\.ts/)[1], 10);
return numA - numB;
});
// 生成文件列表内容(使用 Unix 路径分隔符)
const listContent = files
.map(file => `file '${path.join(file).replace(/\\/g, '/')}'`)
.join('\n');
// 写入文件列表
const listPath = path.join(folderPath, 'list.txt');
fs.writeFileSync(listPath, listContent);
console.log('文件列表已生成:', listPath);
return listPath;
};
合并视频的函数
const mergeSegments = () => {
const listPath = path.join(folderPath, 'list.txt').replace(/\\/g, '/');
const outputFile = './mergeVideo/merged_video.mp4';
console.log(listPath);
// 检查文件列表是否存在
if (!fs.existsSync(listPath)) {
console.error('错误:文件列表未生成');
process.exit(1);
}
execSync(
`ffmpeg -f concat -safe 0 -i "${listPath}" -c copy "${outputFile}"`,
{ stdio: 'inherit' }
);
console.log('合并完成:', outputFile);
};
移除临时文件和善后优化的函数
// 删除 assets 目录下的所有文件
const deleteAllFilesInAssets = () => {
const folderPath = path.join('./assets');
const files = fs.readdirSync(folderPath);
files.forEach(file => {
const filePath = path.join(folderPath, file);
fs.unlinkSync(filePath);
});
console.log('assets 目录下的所有文件已删除');
};
// 随机改名
const renameMergedVideo = () => {
const oldPath = path.join('./mergeVideo', 'merged_video.mp4');
const videoFileName = generateRandomString()
const newPath = path.join('./mergeVideo', `video_${videoFileName}.mp4`);
if (fs.existsSync(oldPath)) {
fs.renameSync(oldPath, newPath);
console.log(`文件已重命名为video_${videoFileName}.mp4 `);
} else {
console.log('文件 merged_video.mp4 不存在');
}
};
// 生成一个随机的8位数字加大小写字母的字符串
const generateRandomString = () => {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let results = '';
const length = 8;
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
results += characters.charAt(randomIndex);
}
return results;
};
接下来就可以编写我们的main
函数了,只需要挨个调用上面的辅助函数即可
export const main = async (Number_of_data_segments,BASE_URL,rootPath = folderPath) => {
for (let i = 1; i <= Number_of_data_segments; i++) {
let url = `seg-${i}-v1-a1.ts`
console.log(`正在下载第${i}个数据段,标识为${url}`);
let m4s = await fetchData(BASE_URL + url)
console.log(`第${i}个数据段下载完成`);
await writeFile(`${rootPath}/${url}`, Buffer.from(await m4s.arrayBuffer()))
}
console.log(`下载完成`);
console.log(`生成目录映射`);
generateFileList();
console.log(`合并数据段`);
mergeSegments();
deleteAllFilesInAssets()
renameMergedVideo()
}
总结
最后试了一下,效果还是蛮不错的,这些都是最终合成的视频
这只是个简单的脚本,很多地方都可以优化,例如可以通过网络状态来判断分片数量,就不再需要手动去查看分片数量了,这些地方有兴趣的小伙伴可以自行尝试