Js-script 下载m3u8视频 1132149908

1,346 阅读2分钟

成品展示.gif

代码实现

/*
 * @Author: zendu 
 * @Date: 2021-09-30 14:19:36 
 * @Desc: download m3u8 ts file and convert to mp4
 * @Usage node  download.js  [ https://test.com/89a0px.m3u8 ]
 */

let link = "http://1257120875.vod2.myqcloud.com/0ef121cdvodtransgzp1257120875/3055695e5285890780828799271/v.f230.m3u8"


let argv = process.argv.slice(2);
if (argv.length > 0 && /^http(s?).*?\.m3u8$/.test(argv[0])) {
    link = argv[0];
}

// npm i axios m3u8-parser single-line-log
const axios = require("axios");
const m3u8Parser = require("m3u8-parser");
const path = require("path");
const fs = require("fs");
const slog = require('single-line-log').stdout;
const { exec } = require("child_process");

// ==================== 主入口
function main() {
    axios.get(link).then(async (response) => {
        outputFileName = link.split('/').pop().replace(/\./g,'_').replace('_m3u8', '.mp4');
        // 1. 获取m3u8文件信息
        let data = response.data;
        // 2. 解析文件,形成序列化的列表
        let segments = generateM3u8Segments(data);
        // 3. 循环下载每个文件
        let flag = await downloadM3u8S(segments, link);
        if (!flag) return;
        // 4. 合并ts文件到mp4
        await convertTStoMp4(segments, outputFileName);
        // 5. 清除ts文件
        await clearFile(segments);

        console.log(`output: ${outputFileName}`);
    });
}


function generateM3u8Segments(data) {
    console.log('序列化m3u8');
    const parser = new m3u8Parser.Parser();
    parser.push(data);
    parser.end();
    const parsedManifest = parser.manifest;
    segments = parsedManifest.segments;
    return segments;
}

function Progress() {
    this.pb = new ProgressBar('下载进度', 0);
}
Progress.prototype.render = (completed, total) => {
    this.pb.render({ completed: num, total: total });
}


/* 
    @segments: 
        {
            duration: 1.416667,
            uri: 'v.f230.ts?start=0&end=282375&type=mpegts',
            timeline: 0
        },
    @m3u8Link: url
    return false | true
*/
let nowDownloadNumber = 0;
let pb = new ProgressBar('下载进度', 0);
function downloadM3u8S(segments, m3u8Link, downloadDir = 'video') {
    return new Promise((resolve, reject) => {
        let originURL = m3u8Link.replace(m3u8Link.split('/').pop(), '');
        // 创建下载目录
        if (!fs.existsSync(downloadDir)) {
            fs.mkdirSync(downloadDir);
        }

        segments.forEach(async (v, i) => {
            try {
                // 构造ts文件的下载链接
                v.url = originURL + v.uri;
                v.name = path.join(downloadDir, i + 1 + '.ts');
                v.index = i + 1;
                v.length = segments.length;
                // 下载每一个ts文件
                await downloadAndSaveSingleM3u8(v);
                nowDownloadNumber++;
                pb.render({ completed: nowDownloadNumber, total: segments.length });
                // console.log(`下载:${nowDownloadNumber}/${segments.length}`);
                if (nowDownloadNumber === segments.length) {
                    resolve(true); // 下载完成
                }
            } catch (e) {
                console.log('2', 'm3u8文件下载异常, Function:downloadM3u8S');
                reject(false);
            }
        });
    })
}

async function downloadAndSaveSingleM3u8(segmentItem) {
    return new Promise((resolve, reject) => {
        axios({
            url: segmentItem.url,
            method: 'get',
            timeout: 2000,
            headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36' },
            responseType: 'arraybuffer' // 下载文件时需要将返回数据改为arraybuffer类型
        }).then(response => {

            let data = response.data;
            let status = response.status;
            if (status != 200) throw Error("request single ts file error");
            fs.writeFile(segmentItem.name, data, () => { });
            resolve(true);
        }).catch(error => {
            reject(false);
        })
    })
}

function convertTStoMp4(segments, outputname) {
    return new Promise((resolve, reject) => {
        try {
            let fileString = "concat:" + segments.map((v, i) => v.name).join("|");
            console.log("");
            if (fs.existsSync(outputname)) {
                console.log(`\n${outputname} has existed, covert to bak_${outputname}`);
                outputname = "_bak_" + outputname;
            }

            cmd = `ffmpeg -i "${fileString}" -acodec copy -vcodec copy -absf aac_adtstoasc ${outputname}`;

            exec(cmd, (err, data) => {
                if (err) console.log("3 convert fail");
                console.log("ts convert to mp4");
                resolve(true);
            });
        } catch (error) {
            reject(false);
        }

    })
}

function clearFile(segments) {
    return new Promise((resolve, reject) => {
        let cmd = `rm -f  ${segments.map((v, i) => v.name).join(" ")}`;
        exec(cmd, (err, data) => {
            if (err) console.log("5 clearfile error!");
            console.log("clear temp file");
            resolve(true);
        });
    })
}









// 下载进度条
function ProgressBar(description, bar_length) {
    // 两个基本参数(属性)
    this.description = description || 'Progress';       // 命令行开头的文字信息
    this.length = bar_length || 25;                     // 进度条的长度(单位:字符),默认设为 25

    // 刷新进度条图案、文字的方法
    this.render = function (opts) {
        var percent = (opts.completed / opts.total).toFixed(4);    // 计算进度(子任务的 完成数 除以 总数)
        var cell_num = Math.floor(percent * this.length);             // 计算需要多少个 █ 符号来拼凑图案

        // 拼接黑色条
        var cell = '';
        for (var i = 0; i < cell_num; i++) {
            cell += '█';
        }

        // 拼接灰色条
        var empty = '';
        for (var i = 0; i < this.length - cell_num; i++) {
            empty += ' ';
        }

        // 拼接最终文本
        var cmdText = this.description + ': ' + (100 * percent).toFixed(2) + '% ' + cell + empty + ' ' + opts.completed + '/' + opts.total;

        // 在单行输出文本
        slog(cmdText);
    };
}


main();