递归setTimeout随机停止

时间:2019-03-11 17:10:25

标签: javascript node.js stream settimeout hls

我正在构建一个模块,允许用户从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随机触发失败的原因,并希望得到我的任何帮助或想法。

谢谢您的帮助。

0 个答案:

没有答案