【JavaScript爬虫记录】记录一下使用JavaScript爬取m4s流视频过程(内含ffmpeg合并)

前言

前段时间发现了一个很喜欢的视频,可惜网站不让下载,简单看了一下视频是被切片成m4s格式的流文件,初步想法是将所有的流文件下载下来然后使用ffmpeg合并成一个完整的mp4,于是写了一段脚本来实现一下,电脑没有配python环境,所以使用JavaScript实现,合并功能需要安装ffmpeg,没有的小伙伴自行安装哦

前置知识
  1. m4s文件(复制百度)

    M4S 文件是使用 MPEG-DASH 流技术通过 Internet 流式传输的一小段视频。它包含二进制数据形式的视频片段。接收应用程序(通常是网络浏览器或媒体播放器)按接收顺序播放这些片段。第一个 M4S 段由它包含的初始化数据标识。在 summary 中,m4s文件是完整文件的单个小媒体片段。 M4S 文件基于 ISO 基础媒体文件 (ISOBMFF) 格式。大文件的这些小片段可以通过 HTTP 独立下载。因此,如果您有一个大的 MP4 电影文件,则可以使用 MPEG-DASHHTTP 上的动态自适应流式传输)技术将其分段为 M4S 分段文件,从而对其进行流式传输。如果将此大型电影文件作为 M4S 下载到光盘,则会下载多个 M4S 文件。如果将所有这些 .m4s 段连接起来,就会生成一个完整的可播放文件。除非文件的第一个初始化段也可用,否则媒体播放器无法播放文件。


思路整理
  1. 找到目标m4s文件的接口,观察接口规律,拼接URL批量下载
  2. 然后将文件写入本地,再遍历目录生成ffmpeg合并用的文化列表目录
  3. 然后调用ffmpeg终端命令合并
  4. 最后清理临时文件

开始实现

首先观察到目标m4s文件的url格式都是https://xxxxxx/1080.mp4/seg-1-v1-a1.m4s / https://xxxxxx/1080.mp4/seg-2-v1-a1.m4s等等,猜测只是通过目标的序号来管理分片,那考虑使用循环来批量下载,先写几个函数来处理基本的功能,例如下载文件 / 生成临时目录 / 本地写入 / 清理临时文件等

请求函数

javascript 复制代码
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);
    }
};

本地写入的函数

javascript 复制代码
const writeFile = async (fileName, file) => {
    fs.writeFile(fileName, file, (err) => {
        if (err) {
            console.log("写入失败:", err);
            return
        }
        console.log(`${fileName}写入成功`);
    })
}

生成临时目录的函数

javascript 复制代码
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;
};

合并视频的函数

javascript 复制代码
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);
  };

移除临时文件和善后优化的函数

javascript 复制代码
// 删除 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函数了,只需要挨个调用上面的辅助函数即可

javascript 复制代码
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()
}

总结

最后试了一下,效果还是蛮不错的,这些都是最终合成的视频

这只是个简单的脚本,很多地方都可以优化,例如可以通过网络状态来判断分片数量,就不再需要手动去查看分片数量了,这些地方有兴趣的小伙伴可以自行尝试

相关推荐
前端菜鸟日常14 分钟前
vue2和vue3的按需引入的详细对比通俗易懂
javascript·vue.js·ecmascript
程楠楠&M1 小时前
uni-app(位置1)
前端·javascript·uni-app·node.js
破z晓1 小时前
uniapp 整合openlayers 编辑图形文件并上传到服务器
前端·javascript·uni-app
祈澈菇凉2 小时前
除了Axios,如何用fetch处理403错误?
前端·javascript·vue.js
JSON_L2 小时前
Vue Axios
前端·javascript·vue.js
狐凄2 小时前
练习题:47
开发语言·前端·javascript
没有晚不了安2 小时前
1.16作业
java·前端·javascript
我谈山美,我说你媚2 小时前
vue2自定义useVModel函数
java·前端·javascript
jsPao2 小时前
JavaScript 语言基础之标签语句
开发语言·javascript·ecmascript
Et2nity3 小时前
tiptap md 编辑器实用场景开发
前端·javascript·编辑器·markdown