constructor()

in source/stale-playlist-detector/playlist.js [64:199]


    constructor(options) {
        this.options = options;
        this.duration_multiplier = options.duration_multiplier ? Number.parseFloat(options.duration_multiplier) : 1.5;
        this.last_sample = {
            "timeseconds": 0,
            "hash": "",
            "media_sequence": 0,
            "duration": 0
        };
        this.change_durations = [];
        this.change_durations_max = 25;
        this.min_duration = 0;
        this.max_duration = 0;
        this.median_duration = 0;
        this.mean_duration = 0;
        let classobject = this;
        this.fsm = new machina.Fsm({
            initialize: function(options) {},
            namespace: options.url,
            initialState: "uninitialized",
            states: {
                uninitialized: {
                    "start": function() {
                        this.deferUntilTransition();
                        this.transition("validate");
                    }
                },
                "validate": {
                    _onEnter: function() {
                        // check that we have minimal configuration to run
                        if (!classobject.options.url) {
                            logger.error("options.url is missing");
                            this.transition("configuration-problem");
                        } else {
                            this.transition("configure")
                        }
                    }
                },
                "configure": {
                    _onEnter: function() {
                        this.transition("refresh");
                    }
                },
                "refresh": {
                    _onEnter: async function() {
                        try {
                            let data = await http_get(classobject.options.url);
                            let parser = new m3u8_parser.Parser();
                            parser.push(data.body);
                            parser.end();
                            let now = epoch();
                            // get the segment duration
                            // let duration = (parser.manifest && parser.manifest.targetDuration) ? Number.parseFloat(parser.manifest.targetDuration) : 0;
                            let duration = lowest_duration(parser.manifest);
                            logger.info(`specified duration is ${duration}s [${classobject.options.url}]`);
                            let hex = playlist_hash(data.body);
                            logger.info(`playist hash is ${hex} [${classobject.options.url}]`);
                            let media_sequence = Number.parseInt(parser.manifest.mediaSequence);
                            logger.info(`media sequence is ${media_sequence} [${classobject.options.url}]`);
                            this.transition("check", {
                                "timeseconds": now,
                                "hash": hex,
                                "media_sequence": media_sequence,
                                "duration": duration
                            });
                        } catch (error) {
                            logger.error(error);
                            this.transition("check", classobject.last_sample);
                        }
                    }
                },
                "check": {
                    _onEnter: async function(input) {
                        // compare current sample to last changed sample
                        if (classobject.changed(input)) {
                            // changed
                            let change_duration_seconds = input.timeseconds - classobject.last_sample.timeseconds;
                            classobject.last_sample = input;
                            logger.info(`changed after ${change_duration_seconds}s [${classobject.options.url}]`);
                            classobject.change_durations.push(change_duration_seconds);
                            while (classobject.change_durations.length > classobject.change_durations_max) {
                                classobject.change_durations.shift();
                            }
                            // logger.info(JSON.stringify(classobject.change_durations));
                            classobject.p90_duration = jstat.percentile(classobject.change_durations, 0.90).toFixed(1);
                            classobject.mean_duration = jstat.mean(classobject.change_durations).toFixed(1);
                            classobject.median_duration = jstat.median(classobject.change_durations).toFixed(1);
                            classobject.min_duration = jstat.min(classobject.change_durations).toFixed(1);
                            classobject.max_duration = jstat.max(classobject.change_durations).toFixed(1);
                            logger.info(`p90 duration is ${classobject.p90_duration}s from ${classobject.change_durations.length} samples [${classobject.options.url}]`);
                            logger.info(`mean duration is ${classobject.mean_duration}s from ${classobject.change_durations.length} samples [${classobject.options.url}]`);
                            logger.info(`median duration is ${classobject.median_duration}s from ${classobject.change_durations.length} samples [${classobject.options.url}]`);
                            logger.info(`min duration is ${classobject.min_duration}s from ${classobject.change_durations.length} samples [${classobject.options.url}]`);
                            logger.info(`max duration is ${classobject.max_duration}s from ${classobject.change_durations.length} samples [${classobject.options.url}]`);
                            this.transition("fresh");
                        } else {
                            // not changed
                            let max_duration = Number.parseInt(classobject.last_sample.duration * classobject.options.detector_options.duration_multiplier);
                            logger.info(`maximum allowed duration is ${max_duration}s [${classobject.options.url}]`);
                            let expected_update = classobject.last_sample.timeseconds + max_duration;
                            logger.info(`change due by ${new Date(expected_update * 1000).toTimeString()} ${expected_update} [${classobject.options.url}]`);
                            if (input.timeseconds > expected_update) {
                                this.transition("stale");
                            } else {
                                this.transition("fresh");
                            }
                        }
                    }
                },
                "stale": {
                    _onEnter: function() {
                        logger.error(`stale playlist [${classobject.options.url}]`);
                        this.emit("stale", classobject);
                    }
                },
                "fresh": {
                    _onEnter: function() {
                        logger.info(`fresh playlist [${classobject.options.url}]`);
                        this.emit("fresh", classobject);
                    }
                },
                "configuration-problem": {
                    _onEnter: function() {
                        logger.error("unable to proceed");
                        // stop here until start message
                    }
                }
            },
            "start": function() {
                this.handle("start");
            }
        });
        this.fsm.on("transition", function(event, data) {
            logger.info(JSON.stringify(event), JSON.stringify(data));
        });
    }