UNPKG

fa-comm

Version:
805 lines (777 loc) 33.6 kB
/* M3U8文件解析、下载等 参考文献:https://www.jianshu.com/p/e97f6555a070 下载ts片段,目前最大使用16进程比较好 */ require('../comm/proto'); const guid = require('../comm/guid'); const _axios = require('../comm/axios'); const _url = require('../comm/url'); const _convert = require('../comm/convert'); const _fs = require('../comm/fs'); const fs = require('fs'); const path = require('path'); const _process = require('../comm/process'); const _requestModule = require('request'); const { execSync } = require("child_process"); const events = require('events'); let request; module.exports = class { constructor(uri, retry = 3) { this.retry = retry; this.uri = uri; // 需要实例化 this.parse = _parse; // 缓存,需要实例化 this.cache = _cache; // 需要实例化 this.saveAs = _saveAs; // this.stringify = _stringify; // this.originMap = _originMap; this._cacheOnceEventEmitter = new events.EventEmitter(); this.cacheTaskProgress = new events.EventEmitter(); } /** * 将一个m3u8地址转换成m3u8对象 * @param {String} uri 一个m3u8地址 * @returns */ static createM3u8 = async function (uri) { return await _parse(uri); } /** * 将m3u8对象转换成字符文件对象 * @param {Object} m3u8Obj m3u8对象 * @param {Boolean} removeHost 是否移除分片域名信息 * @param {String} prefix 移除后替换的分片域名信息(用于保存本地时使用) * @returns */ static stringify(m3u8Obj, removeHost, prefix) { return _stringify(m3u8Obj, removeHost, prefix); } /** * 获得转发对应的真实地址 * @static * @param {String} filepath m3u8文件全路径 * @param {String} url 转发地址 * @returns */ static getOriginUrl(filepath, url) { return _getOriginUrl(filepath, url); } /** * 获得原始地址映射 * @param {String} filepath m3u8所在目录 * @returns */ static originMap(filepath) { return _originMap(filepath); } /** * 从图像或其他文件中提取ts文件 * @static * @param {String | Buffer} filepathOrBuffer 需要提取的实体文件路径或一个文件流 * @param {Boolean} saveas 是否另存为文件 * @returns JSON对象,包含原始文件信息及提取后的ts文件流 */ static extractTs(filepathOrBuffer, saveas = false) { return _extractTs(filepathOrBuffer, saveas); } }; const _read = async function (text, info, uri) { text = text.split('\n'); info.list = []; info.DISCONTINUITY = []; let encrypted = { METHOD: 'NONE' }; for (let i = 0; i < text.length; i++) { if (text[i].startWith("#EXT-X-VERSION:")) { info.VERSION = text[i].replace("#EXT-X-VERSION:", '').replace('\r', ''); } if (text[i].startWith("#EXT-X-TARGETDURATION:")) { info.TARGETDURATION = text[i].replace("#EXT-X-TARGETDURATION:", '').replace('\r', ''); } if (text[i].startWith("#EXT-X-PLAYLIST-TYPE:")) { info.TYPE = text[i].replace("#EXT-X-PLAYLIST-TYPE:", '').replace('\r', ''); } if (text[i].startWith("#EXT-X-MEDIA-SEQUENCE:")) { info.SEQUENCE = text[i].replace("#EXT-X-MEDIA-SEQUENCE:", '').replace('\r', ''); } if (text[i].startWith("#EXT-X-DISCONTINUITY")) { let tmpUrl = text[i - 1]; if (!tmpUrl.startWith('http://') && !tmpUrl.startWith('https://')) { list.URI += uri; } tmpUrl = _url.parse(tmpUrl); tmpUrl = path.basename(tmpUrl.pathname); info.DISCONTINUITY.push(tmpUrl); } if (text[i].startWith("#EXT-X-KEY:")) { encrypted = {}; text[i].replace("#EXT-X-KEY:", '').split(',').map(item => { let tmp = item.split('='); encrypted[tmp[0]] = tmp[1].replace('\r', ''); }); if (encrypted.METHOD != "NONE") { //处理URI if (encrypted.URI) { encrypted.URI = encrypted.URI.replace(/"/g, "").replace(/'/g, ""); if (!encrypted.URI.startWith('http://') && !encrypted.URI.startWith('https://')) { encrypted.URI += uri; } let tmp = _url.parse(encrypted.URI); let _tmp = new _axios(tmp.protocol + '//' + tmp.host); encrypted.KEY = await _tmp.get(tmp.path); if (encrypted.KEY.length == 16) { encrypted.KEY = _convert.to16Text(encrypted.KEY); } } //处理向量 IV=0xaa3dcf6a7acb92ff4fb08d9b3b3d6f51 if (encrypted.IV) { if (encrypted.IV.startWith('0x') && encrypted.IV.length == 34) { encrypted.IV = encrypted.IV.replace('0x', ''); } if (encrypted.IV.length == 16) { encrypted.IV = _convert.to16Text(encrypted.IV); } } else { let _iv = info.SEQUENCE.toString(); //如果未出现,则默认使用媒体片段序列号(即 EXT-X-MEDIA-SEQUENCE)作为其 IV 值,使用大端字节序,往左填充 0 直到序列号满足 16 字节(128 位)。 while (_iv.length < 32) { _iv += '0'; } encrypted.IV = _iv; } } } if (text[i].startWith("#EXTINF:")) { let list = { EXTINF: text[i].replace("#EXTINF:", '').replace(',', '').replace('\r', ''), URI: text[i + 1].replace('\r', '') }; if (!list.URI.startWith('http://') && !list.URI.startWith('https://')) { // list.URI += uri; list.URI = uri + list.URI; } list.ENCRYPTED = encrypted; info.list.push(list); } } return info; } /** * 另存为本地m3u8文件(可用于转发) * @param {*} filepath 本地保存路径 * @param {String} prefix 片段转发请求地址 * @returns */ const _saveAs = async function (filepath, prefix) { if (!filepath.endWith(path.sep)) { filepath += path.sep; } _fs.mkdirSync(filepath); const uri = _url.parse(this.uri); const m3u8Obj = await _parse(this.uri); if (m3u8Obj) { let m3u8FileText = []; if (m3u8Obj.length > 1) { //多个m3u8 m3u8FileText.push("#EXTM3U"); for (index = 0; index < m3u8Obj.length; index++) { const _m3u8FileText = _stringify(m3u8Obj[i], 1, prefix); fs.writeFileSync(`${filepath}${m3u8Obj[index].BANDWIDTH}.m3u8`, _m3u8FileText.remove.join('\n')); m3u8FileText.push(`#EXT-X-STREAM-INF:BANDWIDTH=${m3u8Obj[index].BANDWIDTH}${m3u8Obj[index].RESOLUTION ? `,RESOLUTION=${m3u8Obj[index].RESOLUTION}` : ""}`); m3u8FileText.push(`${filepath}${m3u8Obj[index].BANDWIDTH}.m3u8`); } m3u8FileText.push('#EXT-X-ENDLIST'); fs.writeFileSync(filepath + 'index.m3u8', m3u8FileText.join('\n')); } else { //单个m3u8 m3u8FileText = _stringify(m3u8Obj[0], 1, prefix); fs.writeFileSync(filepath + 'index.m3u8', m3u8FileText.remove.join('\n')); } fs.writeFileSync(filepath + 'm3u8', JSON.stringify({ m3u8Obj, uri }).encrypt()); return filepath + 'index.m3u8'; } else { return null; } } // function _merge(filepath, filename, m3u8Objs) { // for (const m3u8Obj of m3u8Objs) { // var writeStream = fs.createWriteStream(path.join(filepath, filename + '.ts')); // for (const list of m3u8Obj.list) { // let fname = path.join(filepath, filename, 'data', path.basename(list.URI)); // if (fs.existsSync(fname)) { // const readStream = fs.createReadStream(fname); // readStream.pipe(writeStream, false); // } // } // writeStream.end(); // } // } /** * Stream 合并 * @param { String } sourceFiles 源文件目录名 * @param { String } targetFile 目标文件 */ function _merge(filepath, filename, m3u8Obj) { var list = m3u8Obj.list.map(e => path.join(filepath, filename, 'data', path.basename(e.URI))); // const scripts = fs.readdirSync(path.resolve(__dirname, sourceFiles)); // 获取源文件目录下的所有文件 // const fileWriteStream = fs.createWriteStream(path.resolve(__dirname, targetFile)); // 创建一个可写流 var writeStream = fs.createWriteStream(path.join(filepath, filename + '.ts')); streamMergeRecursive(list, writeStream); } /** * Stream 合并的递归调用 * @param { Array } scripts * @param { Stream } fileWriteStream */ function streamMergeRecursive(files = [], fileWriteStream) { // 递归到尾部情况判断 if (!files.length) { return fileWriteStream.end("console.log('Stream 合并完成')"); // 最后关闭可写流,防止内存泄漏 } // const currentFile = path.resolve(__dirname, 'scripts/', scripts.shift()); const currentReadStream = fs.createReadStream(files.shift()); // 获取当前的可读流 currentReadStream.pipe(fileWriteStream, { end: false }); currentReadStream.on('end', function () { streamMergeRecursive(files, fileWriteStream); }); currentReadStream.on('error', function (error) { // 监听错误事件,关闭可写流,防止内存泄漏 console.error(error); fileWriteStream.close(); }); } /** * 缓存m3u8文件 * @param {String} uri m3u8地址 * @param {Number} procNum 同时下载进程数 * @param {String} procNum 缓存路径 */ const _cache = async function (filepath, procNum, merge = true) { let self = this; _fs.mkdirSync(path.join(filepath, 'data')); console.info("正在读取m3u8文件,请稍后..."); const m3u8Obj = await _parse(this.uri); // let originProto = Object.getPrototypeOf(m3u8Obj[0]); // let obj = Object.assign(Object.create(originProto), m3u8Obj[0]); // obj.BANDWIDTH = 1; // m3u8Obj.push(obj); // originProto = Object.getPrototypeOf(m3u8Obj[0]); // obj = Object.assign(Object.create(originProto), m3u8Obj[0]); // obj.BANDWIDTH = 2; // m3u8Obj.push(obj); // originProto = Object.getPrototypeOf(m3u8Obj[0]); // obj = Object.assign(Object.create(originProto), m3u8Obj[0]); // obj.BANDWIDTH = 3; // m3u8Obj.push(obj); if (m3u8Obj) { // if (merge) { // _merge(path.join(filepath, '../'), path.basename(filepath), m3u8Obj[0]); // } if (m3u8Obj.length > 1) { //多个m3u8 let m3u8FileText = ["#EXTM3U"]; let index = 0; self._cacheOnceEventEmitter.on("progress", function (data) { let completed = index / m3u8Obj.length * 100; let nowCompleted = data.progress / m3u8Obj.length; let progress = Math.round((completed + nowCompleted) * 100) / 100; self.cacheTaskProgress.emit("progress", { current: index + 1, total: m3u8Obj.length, detail: data, progress }); if (progress == 100) { if (merge) { for (const o of m3u8Obj) { _merge(path.join(filepath, '../'), path.basename(filepath), o); } } self.cacheTaskProgress.emit("complete", ''); } }); for (index = 0; index < m3u8Obj.length; index++) { let _filepath = filepath + m3u8Obj[index].BANDWIDTH + '/'; _fs.mkdirSync(_filepath); await cacheOnce(m3u8Obj[index], _filepath, procNum, self); m3u8FileText.push(`#EXT-X-STREAM-INF:BANDWIDTH=${m3u8Obj[index].BANDWIDTH}${m3u8Obj[index].RESOLUTION ? `,RESOLUTION=${m3u8Obj[index].RESOLUTION}` : ""}`); m3u8FileText.push(`${m3u8Obj[index].BANDWIDTH}/index.m3u8`); } fs.writeFileSync(filepath + 'index.m3u8', m3u8FileText.join('\n')); } else { //单个m3u8 self._cacheOnceEventEmitter.on("progress", function (data) { self.cacheTaskProgress.emit("progress", { progress: data.progress, current: 1, total: 1, detail: data }); if (data.progress == 100) { if (merge) { _merge(path.join(filepath, '../'), path.basename(filepath), m3u8Obj[0]); } self.cacheTaskProgress.emit("complete", ''); } }); await cacheOnce(m3u8Obj[0], filepath, procNum, self); } } else { self.cacheTaskProgress.emit("error", ''); } } const cacheOnce = async function (m3u8, filepath, procNum, parent) { let m3u8FileText = ["#EXTM3U"]; m3u8.VERSION && m3u8FileText.push(`#EXT-X-VERSION:${m3u8.VERSION}`); m3u8.TARGETDURATION && m3u8FileText.push(`#EXT-X-TARGETDURATION:${m3u8.TARGETDURATION}`); m3u8.TYPE && m3u8FileText.push(`#EXT-X-PLAYLIST-TYPE:${m3u8.TYPE}`); m3u8.SEQUENCE && m3u8FileText.push(`#EXT-X-MEDIA-SEQUENCE:${m3u8.SEQUENCE}`); let j = 0, t = procNum; if (m3u8.list.length < t) { t = m3u8.list.length; } let success = 0, error = 0; // console.log("准备循环下载..."); while (true) { if (j < t) { try { m3u8FileText.push(`#EXTINF:${m3u8.list[j].EXTINF}`); let ts = _url.parse(m3u8.list[j].URI); let fname = path.basename(ts.pathname); m3u8FileText.push('data/' + fname); if (m3u8.DISCONTINUITY && m3u8.DISCONTINUITY.length && m3u8.DISCONTINUITY.find(item => item == fname)) { m3u8FileText.push("#EXT-X-DISCONTINUITY"); } // console.log("准备下载:", j, t, m3u8.list.length); downLoad(filepath + 'data/' + fname, m3u8.list[j].URI).then(function () { // console.log("完成下载:", j, t, m3u8.list.length); success++; if (t < m3u8.list.length) { t++; } let progress = Math.round((((success + error) / m3u8.list.length) * 100 * 0.98) * 100) / 100; parent._cacheOnceEventEmitter.emit("progress", { total: m3u8.list.length, current: success + error, progress }); }).catch(function () { // console.warn("下载失败:", j, t, m3u8.list.length); error++; if (t < m3u8.list.length) { t++; } let progress = Math.round((((success + error) / m3u8.list.length) * 100 * 0.98) * 100) / 100; parent._cacheOnceEventEmitter.emit("progress", { total: m3u8.list.length, current: success + error, progress }); }); } catch (e) { } j++; } if (success + error == m3u8.list.length) { await _process.sleep(1000); break; } await _process.sleep(100); } //解密 // console.log("循环下载完成,准备解密"); for (let j = 0; j < m3u8.list.length; j++) { // console.log("解密", j); if (m3u8.list[j].ENCRYPTED && m3u8.list[j].ENCRYPTED.METHOD != 'NONE') { if (m3u8.list[j].ENCRYPTED.METHOD == 'AES-128') { let ts = _url.parse(m3u8.list[j].URI); let fname = path.basename(ts.pathname); await openssl(filepath + 'data/', fname, m3u8.list[j].ENCRYPTED.IV, m3u8.list[j].ENCRYPTED.KEY, this.retry > 0 ? this.retry : 3); try { fs.unlinkSync(filepath + 'data/' + fname); fs.copyFileSync(filepath + 'data/' + fname + '.UETS', filepath + 'data/' + fname); fs.unlinkSync(filepath + 'data/' + fname + '.UETS'); } catch (e) { } } } let progress = Math.round((((j + 1) / m3u8.list.length) * 100 * 0.02 + 98) * 100) / 100; parent._cacheOnceEventEmitter.emit("progress", { total: m3u8.list.length, current: j + 1, progress }); } m3u8FileText.push("#EXT-X-ENDLIST"); try { fs.writeFileSync(filepath + 'index.m3u8', m3u8FileText.join('\n')); } catch (e) { } } async function openssl(filepath, fname, iv, key, retry, _retry) { _retry = _retry || 0; while (_retry < retry) { try { // execSync(`openssl aes-128-cbc -d -in ${fname} -out ${fname + '.UETS'} -nosalt -iv ${m3u8.list[j].ENCRYPTED.IV} -K ${m3u8.list[j].ENCRYPTED.KEY}`, { if (fs.existsSync(path.join(filepath, fname))) { execSync(`openssl aes-128-cbc -d -in ${fname} -out ${fname + '.UETS'} -nosalt -iv ${iv} -K ${key}`, { cwd: filepath, windowsHide: true }); } return; } catch (e) { _retry++; console.warn(`转换出错,重试${_retry}...`); await _process.sleep(1000); openssl(filepath, fname, iv, key, retry, _retry); } } return; } function downLoad(filename, uri) { return new Promise(function (resolve, reject) { var stream = fs.createWriteStream(filename); _requestModule(uri).pipe(stream).on('close', resolve).on("error", reject); }); }; /** * 将m3u8对象转换成字符文件对象 * @param {Object} m3u8Obj m3u8对象 * @param {Boolean} removeHost 是否移除分片域名信息 * @param {String} prefix 移除后替换的分片域名信息(用于保存本地时使用) * @returns */ function _stringify(m3u8Obj, removeHost, prefix) { let text = [], m3u8Text = []; //起始标签 text.push("#EXTM3U"); m3u8Text.push("#EXTM3U"); //表示 HLS 的协议版本号,该标签与流媒体的兼容性相关。该标签为全局作用域,使能整个 m3u8 文件; //每个 m3u8 文件内最多只能出现一个该标签定义。如果 m3u8 文件不包含该标签,则默认为协议的第一个版本。 m3u8Obj.VERSION && text.push(`#EXT-X-VERSION:${m3u8Obj.VERSION}`); m3u8Obj.VERSION && m3u8Text.push(`#EXT-X-VERSION:${m3u8Obj.VERSION}`); //该标签为必选标签。表示每个视频分段最大的时长(单位秒)。 m3u8Obj.TARGETDURATION && text.push(`#EXT-X-TARGETDURATION:${m3u8Obj.TARGETDURATION}`); m3u8Obj.TARGETDURATION && m3u8Text.push(`#EXT-X-TARGETDURATION:${m3u8Obj.TARGETDURATION}`); //该标签为可选标签。表明流媒体类型。全局生效。 m3u8Obj.TYPE && text.push(`#EXT-X-PLAYLIST-TYPE:${m3u8Obj.TYPE}`); m3u8Obj.TYPE && m3u8Text.push(`#EXT-X-PLAYLIST-TYPE:${m3u8Obj.TYPE}`); //标签必须出现在播放列表第一个切片之前。 //表示播放列表第一个 URL 片段文件的序列号。 //每个媒体片段 URL 都拥有一个唯一的整型序列号。 //每个媒体片段序列号按出现顺序依次加 1。 //如果该标签未指定,则默认序列号从 0 开始。 //媒体片段序列号与片段文件名无关。 m3u8Obj.SEQUENCE && text.push(`#EXT-X-MEDIA-SEQUENCE:${m3u8Obj.SEQUENCE}`); m3u8Obj.SEQUENCE && m3u8Text.push(`#EXT-X-MEDIA-SEQUENCE:${m3u8Obj.SEQUENCE}`); //开始循环每一个切片 let ENCRYPTEDING = false; for (let i = 0; i < m3u8Obj.list.length; i++) { //媒体片段可以进行加密,而该标签可以指定解密方法。 //该标签对所有 媒体片段 和 由标签 EXT-X-MAP 声明的围绕其间的所有 媒体初始化块(Meida Initialization Section) 都起作用, //直到遇到下一个 EXT-X-KEY(若 m3u8 文件只有一个 EXT-X-KEY 标签,则其作用于所有媒体片段)。 //多个 EXT-X-KEY 标签如果最终生成的是同样的秘钥,则他们都可作用于同一个媒体片段。 if (m3u8Obj.list[i].ENCRYPTED.METHOD != "NONE") { //需要加密,并且之前没有标记过加密字段 if (!ENCRYPTEDING) { text.push(`#EXT-X-KEY:METHOD=${m3u8Obj.list[i].ENCRYPTED.METHOD},URI="${m3u8Obj.list[i].ENCRYPTED.URI}"${m3u8Obj.list[i].ENCRYPTED.IV ? `,IV="${m3u8Obj.list[i].ENCRYPTED.IV}"` : ""}`); m3u8Text.push(`#EXT-X-KEY:METHOD=${m3u8Obj.list[i].ENCRYPTED.METHOD},URI="${m3u8Obj.list[i].ENCRYPTED.URI}"${m3u8Obj.list[i].ENCRYPTED.IV ? `,IV="${m3u8Obj.list[i].ENCRYPTED.IV}"` : ""}`); ENCRYPTEDING = true; } } else { if (ENCRYPTEDING) { //之前的片段正在加密,现在无需加缪,需要标记不需要加密的标记,否则忽略 text.push(`#EXT-X-KEY:METHOD=NONE`); m3u8Text.push(`#EXT-X-KEY:METHOD=NONE`); } ENCRYPTEDING = false; } //表示其后 URL 指定的媒体片段时长(单位为秒)。每个 URL 媒体片段之前必须指定该标签。 text.push(`#EXTINF:${m3u8Obj.list[i].EXTINF},`); m3u8Text.push(`#EXTINF:${m3u8Obj.list[i].EXTINF},`); //片段信息 if (removeHost) { const uri = _url.parse(m3u8Obj.list[i].URI); if (prefix) { text.push(`${prefix}${encodeURIComponent(uri.path)}`); } else { text.push(uri.path); } } m3u8Text.push(m3u8Obj.list[i].URI); //该标签表明其前一个切片与下一个切片之间存在中断。 if (m3u8Obj.DISCONTINUITY.find(item => m3u8Obj.list[i].URI.endWith(item))) { text.push("#EXT-X-DISCONTINUITY"); m3u8Text.push("#EXT-X-DISCONTINUITY"); } } //结束标记 text.push('#EXT-X-ENDLIST'); m3u8Text.push('#EXT-X-ENDLIST'); if (!removeHost) { return m3u8Text; } return { unRemove: m3u8Text, remove: text }; } /** * 获得转发对应的真实地址 * @static * @param {String} filepath m3u8文件全路径 * @param {String} url 转发地址 * @returns */ function _getOriginUrl(filepath, url) { if (!filepath.endWith('.m3u8')) { filepath = path.join(filepath, './index.m3u8'); } if (!fs.existsSync(filepath)) { return null; } const m3u8FilePath = path.join(path.dirname(filepath), './m3u8'); if (!fs.existsSync(m3u8FilePath)) { return null; } const m3u8File = JSON.parse(fs.readFileSync(m3u8FilePath).toString().decrypt()); for (let i = 0; i < m3u8File.m3u8Obj.length; i++) { let tmp = m3u8File.m3u8Obj[i].list.find(item => item.URI.indexOf(url) > -1); if (tmp) { if (!tmp.URI.startWith('http://') && !tmp.URI.startWith('https://')) { tmp.URI = `${m3u8File.uri.protocol}//${m3u8File.uri.host}${m3u8File.uri.port ? `:${m3u8File.uri.port}` : ""}${tmp.URI}`; } return tmp.URI; } } return null; } /** * 获得原始地址映射 * @param {String} filepath m3u8所在目录 * @returns */ function _originMap(filepath) { try { if (!filepath.endWith('.m3u8')) { filepath = path.join(filepath, './index.m3u8'); } if (!fs.existsSync(filepath)) { return null; } const m3u8FilePath = path.join(path.dirname(filepath), './m3u8'); if (!fs.existsSync(m3u8FilePath)) { return null; } const m3u8File = JSON.parse(fs.readFileSync(m3u8FilePath).toString().decrypt()); for (let i = 0; i < m3u8File.m3u8Obj.length; i++) { let _filepath = path.join(path.dirname(filepath), `./${m3u8File.m3u8Obj.length == 1 ? "index" : m3u8File.m3u8Obj[i].BANDWIDTH}.m3u8`); const text = fs.readFileSync(_filepath).toString().split('\n'); for (let j = 0; j < m3u8File.m3u8Obj[i].list.length; j++) { let _orginUrl = m3u8File.m3u8Obj[i].list[j].URI; if (!_orginUrl.startWith('http://') && !_orginUrl.startWith('https://')) { _orginUrl = `${m3u8File.uri.protocol}//${m3u8File.uri.host}${m3u8File.uri.port ? `:${m3u8File.uri.port}` : ""}${_orginUrl}`; } const _originUri = _url.parse(_orginUrl); const nowUrl = text.find(item => decodeURIComponent(_url.getParams("url", item)) == _originUri.path); m3u8File.m3u8Obj[i].list[j].URI = nowUrl; m3u8File.m3u8Obj[i].list[j].ORGIN_URI = _orginUrl; m3u8File.m3u8Obj[i].list[j].PATH = _originUri.path; } } return m3u8File.m3u8Obj; } catch (e) { return null; } } /** * 从图像或其他文件中提取ts文件 * @static * @param {String | Buffer} filepathOrBuffer 需要提取的实体文件路径或一个文件流 * @param {Boolean} saveas 是否另存为文件 * @returns JSON对象,包含原始文件信息及提取后的ts文件流 */ function _extractTs(filepathOrBuffer, saveas = false) { let filepath = null, buffer = null; if (typeof filepathOrBuffer == 'object') { buffer = filepathOrBuffer; } else { filepath = filepathOrBuffer; } const hexMap = { "8950": "png", "ffd8": "jpg", "4749": "gif", "4740": "ts" }; function analysis() { return new Promise(function (resolve, reject) { if (filepath) { let i = 0; let index = -1; let origin; const rs = fs.createReadStream(filepath, { highWaterMark: 2 }); rs.on('data', (data) => { i += 2; let hex = ""; for (const buff of data) { hex += buff.toString(16); } if (i == 2) { origin = hexMap[hex] || "unknow"; } if (hex == '4740') { index = i; rs.destroy(); } }); rs.on("err", () => { reject(); }); rs.on("close", () => { resolve({ index, origin }); }); } else { let index = -1; let hex = buffer[0].toString(16) + buffer[1].toString(16); let origin = hexMap[hex] || "unknow"; for (let i = 0; i < buffer.length; i += 2) { hex = buffer[i].toString(16) + buffer[i + 1].toString(16); if (hex == '4740') { index = i; break; } } resolve({ index, origin }); } }); } return new Promise(async function (resolve, reject) { try { const _analysis = await analysis(); let result = { origin: _analysis.origin || "", filepath: '', stream: null }; if (filepath) { if (saveas) { const dirname = path.dirname(filepath); const basename = path.basename(filepath); result.filepath = path.join(dirname, guid.v22 + "_" + basename + ".ts"); const ws = fs.createWriteStream(result.filepath); const rs = fs.createReadStream(filepath, { start: _analysis.index, }); rs.on("data", function (dataChunk) { ws.write(dataChunk); }) rs.on("end", function () { ws.end("", function () { resolve(result); }) }); } else { result.stream = fs.createReadStream(filepath, { start: _analysis.index, }); resolve(result); } } else { const _buf = new Buffer(buffer.length - _analysis.index); buffer.copy(_buf, 0, _analysis.index); if (!saveas) { result.stream = _buf; } else { result.filepath = path.join(__dirname, guid.v22 + ".ts"); fs.writeFileSync(result.filepath, _buf); } resolve(result); } } catch (e) { reject(e); } }); } /** * 将一个m3u8地址转换成m3u8对象 * @param {String} uri 一个m3u8地址,默认当前实例地址 * @returns */ const _parse = async function (uri) { let m3u8Obj = []; let text; try { //转换地址 uri = _url.parse(uri || this.uri); //获取内容 request = new _axios(uri.protocol + '//' + uri.host); text = await request.get(uri.path); } catch (e) { console.warn("请求不到资源1:", e); return null; } if (text && text.startWith('#EXTM3U')) { if (text.indexOf('#EXT-X-STREAM-INF:') > -1) { //是否主播放列表,分片进行解析 text = text.split('\n'); for (let i = 0; i < text.length; i++) { if (text[i].startWith('#EXT-X-STREAM-INF:')) { const info = { BANDWIDTH: '', RESOLUTION: '' }; //解析资源信息 text[i].replace('#EXT-X-STREAM-INF:', '').split(',').map(item => { if (item.startWith('BANDWIDTH')) { info.BANDWIDTH = item.split('=')[1]; } if (item.startWith('RESOLUTION')) { info.RESOLUTION = item.split('=')[1]; } }); let _text = "", result; if (text[i + 1].startWith("http://") || text[i + 1].startWith("https://")) { let tmp = _url.parse(text[i + 1]); let _request = new _axios(tmp.protocol + '//' + tmp.host); try { _text = await _request.get(tmp.path); result = await _read(_text, info, tmp.protocol + '//' + tmp.host); } catch (e) { console.warn("请求不到资源2:", e); return null; } } else { try { _text = await request.get(text[i + 1]); result = await _read(_text, info, uri.protocol + '//' + uri.host); } catch (e) { console.warn("请求不到资源3:", e); return null; } } result && m3u8Obj.push(result); i++; } } } else { //非主播放列表,直接解析 try { m3u8Obj[0] = await _read(text, { BANDWIDTH: '默认', 'RESOLUTION': '默认', }, uri.protocol + '//' + uri.host); } catch (e) { console.warn("请求不到资源4:", e); return null; } } return m3u8Obj; } else { console.warn('非标准M3U8文件'); return null; } }