我正在构建一个模块,允许用户从m3u8和dash-mpd源捕获实时流。我的模块已经在多个测试中成功工作,并设法捕获了100%的流,正如我期望的那样;但是,我发现有些时候递归setTimeout功能“随机地”未按预期触发,并且该模块似乎只是处于挂起状态。
没有显示任何异常或错误,我甚至在整个脚本中添加了广泛的日志记录,以识别任何潜在的错误,但似乎总是setTimeout函数只是停止在其中触发代码。该项目的结构是一个初始索引脚本,用于捕获目标流类型是什么,然后是一个通用解析器,用于运行可应用于m3u8和dash-mpd流以及更具体的m3u解析器的命令。 / p>
index.js :解析器的初始化以及m3u8播放列表文件内容的初始捕获
const stream = require('stream');
const url = require('url');
const http = require('http');
const https = require('https');
const fs = require('fs');
const m3u = require('./parsers/m3u');
/**
* @param {String} path Path to stream playlist
* @param {Object} options Objects for configuring playlist capture
* @returns {ReadableStream} Readable stream of the playlist
*/
const hlsdl = (path, options) => {
if(path instanceof Object || path == '') throw Error('A path to an M3U or MPD stream was not provided. Please be sure to include a path to continue');
const hlsstream = new stream.PassThrough();
options = options || {};
options.path = path;
options.timeout = (options.timeout) ? options.timeout : 2500;
options.livebuffer = (options.livebuffer) ? options.livebuffer : 20000;
const host = url.parse(path);
let httpLib = null;
let parser = (options.parser) ? getParser(`.${options.parser}`) : getParser(path);
(host.protocol === 'http:') ? httpLib = http : httpLib = https;
if(host.protocol != 'http:' && host.protocol != 'https:' && host.protocol != 'file:') {
throw new Error('No protocol was included in the path provided. Please ensure an http, https, or file protocol is selected.')
}
if(host.protocol === 'file:') {
fs.readFile(host.path, (err, data) => {
if(err) throw Error('The path to the file provided does not exist. Please check again.');
let internalStream = new parser(httpLib, hlsstream, options);
internalStream.write(data);
});
} else {
const downloadPlaylist = (host) => {
httpLib.get(host, (res) => {
let internalStream = new parser(httpLib, hlsstream, options);
let responseBody = '';
if (res.statusCode >= 500 || res.statusCode < 200) throw Error(`The path provided returned a ${res.statusCode} status code. Please ensure the request resource is available for access before continuing.`);
res.on('error', (err) => { console.log(err); downloadPlaylist(host) });
if (res.statusCode === 200) {
console.log('success');
res.on('data', chunk => {
responseBody += chunk;
});
res.on('end', () => {
internalStream.write(responseBody);
});
} else {
res.on('data', chunk => console.log(chunk.toString()));
}
}).on('error', (err) => downloadPlaylist(host));
}
downloadPlaylist(host);
}
return hlsstream;
};
const getParser = (path) => {
if(RegExp('.m3u').test(path)) return m3u;
throw Error('No compatible HLS stream type detected. Please ensure you\'ve provided a direct link to an M3U or MPD stream.');
};
module.exports = hlsdl;
在index.js标识需要使用哪个解析器之后,它将在let internalStream = new parser(httpLib, hlsstream, options);
处创建解析器。我当前正在使用的解析器是m3u解析器。
m3u.js :用于处理m3u / m3u8格式的实时流播放列表的解析器。每个解析器都基于通用解析器
const GenericParser = require('./generic');
const url = require('url');
module.exports = class m3u extends GenericParser {
constructor(httpLib, hlsstream, options) {
super(httpLib, hlsstream, options);
}
_write(chunk) {
let hasNoNewSegments = true;
this.currentSources = 0, this.completed = 0;
const playlist = chunk.toString().trim().split('\n');
this.hlsstream.emit('status', 'Parsing through playlist');
for (let index = 0; index < playlist.length; index++) {
if (this._parse(playlist[index]) == false) break;
if (playlist[index][0] !== '#') {
if (Object.values(this.chunks).every(segment => segment.link == url.resolve(this.path, playlist[index])) == true) {
hasNoNewSegments = false;
}
}
}
if (hasNoNewSegments == true || this.live == true) {
this.refreshAttempts++;
this.hlsstream.emit('status', 'Fetching next playlist.....');
this._fetchPlaylist();
} else {
this.refreshAttempts = 0;
}
if(this.refreshAttempts > 1) {
this.hlsstream.emit('status', '---------------------');
this.hlsstream.emit('status', 'Dumping fetched m3u8 file');
this.hlsstream.emit('status', chunk);
this.hlsstream.emit('status', '---------------------');
}
this.hlsstream.emit('status', 'Playlist parsing complete');
}
_parse(line) {
let value = line;
let info = null;
if(line[0] == '#' && line.indexOf(':') != -1) {
value = line.split(':')[0];
info = line.split(':')[1];
}
switch(value) {
case('#EXT-X-MEDIA-SEQUENCE'):
if(info < this.sequence) return false;
this.sequence = parseInt(info);
break;
case('#EXTINF'):
this.duration += parseFloat(info.split(',')[0]);
break;
case('#EXT-X-ENDLIST'):
this.live = false;
break;
default:
if(value[0] != '#') {
if(!Object.values(this.chunks).some(x => x.link == url.resolve(this.path, value))) {
this.currentSources++;
this._download(url.resolve(this.path, value), this.sources++);
}
}
break;
}
}
};
但是,正如m3u.js解析器说明中所述,每个解析器都使用通用解析器作为其基础。
generic.js :为所有解析器格式提供通用解析基础,包括每20秒下载单个音频片段并捕获播放列表内容的逻辑
const stream = require('stream');
const url = require('url');
module.exports = class GenericParser extends stream.Duplex {
constructor(httpLib, hlsstream, options) {
super();
this.hlsstream = hlsstream
this.httpLib = httpLib;
this.options = options;
this.path = options.path;
this.sources = 0, this.completed = 0, this.currentSources;
this.sequence = 0;
this.totalsize = 0;
this.chunks = {};
this.downloading = false;
this.live = true;
this.refreshAttempts = 0, this.playlistRefresh = null, this.timerSet = false;
this.pipe(hlsstream, {
end: false
});
this.on('end', () => {
this.hlsstream.end();
});
}
_read() {}
_write() {}
_download(link, index) {
this.downloading = true, this.refreshAttempts = 0;
this.hlsstream.emit('status', `Downloading segment ${index}`);
let req = this.httpLib.get(link, (res) => {
let timeout = setTimeout(() => {
req.abort();
this.completed--;
this.hlsstream.emit('issue', `02: Failed to retrieve segment on time. Attempting to fetch segment again. [${index}]`);
this._download(url.resolve(this.path, link), index);
}, this.options.timeout);
if(res.statusCode >= 400 || res.statusCode < 200) {
this.hlsstream.emit('issue', `01B: An error occurred when attempting to retrieve a segment file. Attempting to fetch segment again. [${index}]`);
this._download(url.resolve(this.path, link), index);
}
let body = [];
res.on('data', (chunk) => {
this.totalsize += chunk.length;
body.push(chunk);
});
res.on('error', (err) => {
this.hlsstream.emit('issue', `01C: An error occurred when attempting to retrieve a segment file. Attempting to fetch segment again. [${index}]`);
this._download(url.resolve(this.path, link), index);
});
res.on('end', () => {
clearTimeout(timeout);
this.completed++;
this.hlsstream.emit('status', 'Completed download of segment ' + index);
this.chunks[index] = { link: url.resolve(this.path, link), buffer: Buffer.concat(body) };
if(this.completed === this.currentSources) this._save();
});
});
req.on('error', (err) => {
this.hlsstream.emit('issue', `01A: An error occurred when attempting to retrieve a segment file. Attempting to fetch segment again. [${index}]`);
this._download(url.resolve(this.path, link), index);
});
}
_save() {
this.hlsstream.emit('status', 'Pushing segments to stream');
let index = Object.values(this.chunks).length - this.currentSources;
let length = Object.values(this.chunks).length;
try {
for (index; index < length; index++) {
this.push(this.chunks[index].buffer);
this.hlsstream.emit('segment', {
id: index,
url: url.resolve(this.path, this.chunks[index].link),
size: this.chunks[index].buffer.length,
totalsegments: this.sources - 1
});
delete this.chunks[index].buffer;
if (index == this.sources - 1 && this.live == false) {
this.hlsstream.emit('status', 'Finished pushing segments to stream');
this.downloading = false;
this.push(null);
}
}
} catch(e) {
console.log(e);
this.hlsstream.emit('issue', 'A critical error occurred when writing to stream. Dumping chunk data now:');
this.hlsstream.emit('issue', JSON.stringify(this.chunks));
}
}
_fetchPlaylist() {
if (this.live == true) {
this.hlsstream.emit('status', 'Refresh Attempts: ' + this.refreshAttempts);
if (this.refreshAttempts < 5) {
this.hlsstream.emit('status', 'Setting setTimeout in _fetchPlaylist()...');
this.playlistRefresh = setTimeout(() => {
this.timerSet = false;
this.hlsstream.emit('status', `_fetchPlaylist setTimeout triggered...[${new Date()}]`);
let req = this.httpLib.get(this.path, (res) => {
let responseBody = '';
let timeout = setTimeout(() => {
req.abort();
this.hlsstream.emit('issue', '05: Failed to retrieve playlist on time. Attempting to fetch playlist again.');
this.refreshAttempts++;
this._fetchPlaylist();
}, this.options.timeout);
res.on('error', (err) => {
this.hlsstream.emit('issue', '03A: An error occurred on the response when attempting to fetch the latest playlist: ' + err);
this.refreshAttempts++;
this._fetchPlaylist()
});
if (res.statusCode === 200) {
res.on('data', chunk => {
responseBody += chunk;
});
res.on('end', () => {
clearTimeout(timeout);
this._write(responseBody);
});
} else {
this.hlsstream.emit('issue', '04: Fetching playlist returned an HTTP code other than 200: ' + res.statusCode);
}
});
req.on('error', (err) => {
this.hlsstream.emit('issue', '03B: An error occurred on the request when attempting to fetch the latest playlist: ' + err);
this.refreshAttempts++;
this._fetchPlaylist();
});
}, this.options.livebuffer);
this.timerSet = true;
console.log(this.playlistRefresh);
console.log(this.timerSet);
this.hlsstream.emit('status', 'SetTimeout object: ' + this.playlistRefresh);
this.hlsstream.emit('status', 'Is setTimeout live?: ' + this.timerSet);
} else {
this.hlsstream.emit('status', 'Live stream completed');
this.push(null);
}
}
}
};
现在,问题的根源很明显是setTimeout并未按预期触发其中的匿名函数。 setTimeout背后的逻辑位于 generic.js 中的_fetchPlaylist()
函数中,并在 m3u.js _write()函数中调用>文件。
我找不到setTimeout随机触发失败的原因,并希望得到我的任何帮助或想法。
谢谢您的帮助。