├── .gitignore ├── Makefile ├── README.md ├── hlsprobe ├── hlsproberc-sample └── sequence-diagram.plantuml /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | lint: 2 | pylint-2.7 hlsprobe 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HLS Probe Utility 2 | ================= 3 | 4 | Utility to detect errors in HTTP Live Streams (Apple HLS). 5 | It may be used regular monitoring tool and mediaserver stress testing. 6 | Features are: 7 | 8 | * parse M3U8-playlists (variant and single-bitrate playlists supported) 9 | * detect bad playlists format (empty playlists, incorrect chunk durations) 10 | * check HTTP response statuses and webserver timeouts 11 | 12 | Planned features: 13 | 14 | * probe chunks with `mediainfo` utility (from libav) 15 | 16 | This utility can't be used for HLS playback. 17 | 18 | This Python version are maintained but development moved to version on Go language: 19 | https://github.com/grafov/hlsprobe2 20 | 21 | Install 22 | ------- 23 | 24 | First install dependencies: 25 | 26 | `pip install m3u8` 27 | 28 | `pip install PyYAML` 29 | 30 | Get repo, cd to it and copy hlsproberc-sample to ~/.hlsproberc. Edit it for your needs. 31 | 32 | `./hlsprobe` 33 | 34 | Similar projects 35 | ---------------- 36 | 37 | * https://code.google.com/p/hls-player 38 | * https://github.com/brookemckim/hlspider 39 | 40 | Project status 41 | -------------- 42 | 43 | [![Is maintained?](http://stillmaintained.com/grafov/hlsprobe.png)](http://stillmaintained.com/grafov/hlsprobe) 44 | -------------------------------------------------------------------------------- /hlsprobe: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- mode:python; coding:utf-8 -*- 3 | # 4 | # ॐ तारे तुत्तारे तुरे स्व 5 | """ 6 | HLS Prober for 451 Fahrenheit mediaserver. 7 | Author: Alexander I.Grafov (Axel) 8 | This utility licensed under GPL v3. 9 | 10 | Uses https://pypi.python.org/pypi/m3u8 for M3U8 playlists parsing. 11 | Scripted in Python2 because m3u8 lib still don't work under Python3. 12 | """ 13 | __version__ = "0.4" 14 | 15 | import sys 16 | import os 17 | import random 18 | import time 19 | import signal 20 | import logging 21 | import urllib2 22 | import smtplib 23 | import m3u8 24 | import yaml 25 | import optparse 26 | import urlparse 27 | import socket 28 | from email.mime.text import MIMEText 29 | from multiprocessing import Process, Queue 30 | from subprocess import check_output #, call, Popen, PIPE 31 | from pprint import pprint 32 | 33 | CONFIG = "~/.hlsproberc" 34 | 35 | 36 | class CupertinoProbe: 37 | """ Parse variant and stream M3U8-playlists. Parser uses python `m3u8` library. 38 | Get playlist URLs from the task queue. 39 | """ 40 | def __init__(self, conf, playlistq, chunkq, bugq, pno, log): 41 | """ Initialized with `conf` config, `playlistq` playlist queue, 42 | `chunkq` chunk queje, `bugq` bug queue, `pno` prober instance number 43 | and `log` logger object. 44 | """ 45 | self.conf = conf 46 | self.playlistq = playlistq 47 | self.chunkq = chunkq 48 | self.bugq = bugq 49 | self.err = ErrContainer(bugq) 50 | self.pno = pno 51 | self.log = log 52 | 53 | def __call__(self): 54 | """ Read and parse a playlist.""" 55 | while True: 56 | try: 57 | group, name, uri = self.playlistq.get(True) 58 | try: 59 | resp = urllib2.urlopen(uri, timeout=self.conf["timeout"]["read"]) 60 | except urllib2.URLError: 61 | self.log.warn("Failed to open (%s) %s stream" % (group, uri)) 62 | self.err("open", "stream", group, name, uri) 63 | continue 64 | except socket.timeout: 65 | self.log.warn("Timeout occurs on (%s) %s stream" % (group, uri)) 66 | self.err("timeout", "stream", group, name, uri) 67 | continue 68 | # Check for client error or server error 69 | status = resp.getcode() 70 | if status >= 400: 71 | self.log.warn("Bad status %s for (%s) %s stream" % (status, group, uri)) 72 | self.err("status", "stream", group, name, uri, status) 73 | continue 74 | rawdata = resp.read() 75 | resp.close() 76 | try: 77 | playlist = m3u8.loads(rawdata) 78 | baseuri = base_uri(uri) 79 | except: 80 | self.log.warn("Failed to parse (%s) %s stream" % (group, uri)) 81 | self.err("parsing", "stream", group, name, uri) 82 | continue 83 | self.log.debug("Load playlist from %s %s" % (group, uri)) 84 | time.sleep(self.conf["sleep"]["playlist-open"]) 85 | if playlist.is_variant: 86 | if not playlist.playlists: 87 | self.err("empty", "variant", group, name, uri) 88 | # Load stream playlists and put them back to playlist queue 89 | for stream in playlist.playlists: 90 | while self.playlistq.full(): 91 | self.log.debug("Playlist queue is full (%d). Probably you need to increase number of stream probers. Check `workers/stream-probers` value." % self.playlistq.qsize()) 92 | time.sleep(3) 93 | self.playlistq.put((group, name, "%s/%s" % (baseuri, stream.uri))) 94 | else: 95 | if not playlist.segments: 96 | self.err("empty", "chunklist", group, name, uri) 97 | # Load URLs to media chunks and put them to chunk queue 98 | for seg in playlist.segments: 99 | while self.chunkq.full(): 100 | self.log.debug("Chunk queue is full (%d). Probably you need to increase number of media probers. Check `workers/media-probers` value." % self.chunkq.qsize()) 101 | time.sleep(3) 102 | self.chunkq.put((time.time(), group, name, "%s/%s" % (baseuri, seg.uri))) 103 | # for economy we are probe only one chunk in the chunklist 104 | if self.conf["mode"]["one-segment"]: 105 | break 106 | except KeyboardInterrupt: 107 | self.log.info("Finalize cupertino prober %s." % self.pno) 108 | 109 | 110 | class MediaProbe: 111 | """ Get and analyze media chunks. 112 | """ 113 | def __init__(self, conf, chunkq, bugq, pno, log): 114 | """ Initialized with `conf` configuration object, 115 | `chunkq` chunk queue, `bugq` bug queue, 116 | `pno` prober instance number and `log` logger object. 117 | """ 118 | self.conf = conf 119 | self.chunkq = chunkq 120 | self.pno = pno 121 | self.log = log 122 | self.loaded = [] # cache list of already loaded chunks 123 | self.err = ErrContainer(bugq) 124 | 125 | def __call__(self): 126 | while True: 127 | try: 128 | stamp, group, name, uri = self.chunkq.get(True) 129 | if uri in self.loaded: 130 | self.log.debug("Chunk %s for %s %s already loaded." % (uri, group, name)) 131 | continue 132 | if time.time() >= stamp + self.conf["timeout"]["target-duration"]: 133 | self.log.info("Media probing is very slow. %s (%s) was skiped." % (uri, group)) 134 | continue 135 | try: 136 | resp = urllib2.urlopen(uri, timeout=self.conf["timeout"]["read"]) 137 | status = resp.getcode() 138 | if status >= 400: 139 | self.log.warn("Bad status %s for (%s) %s stream" % (status, group, uri)) 140 | self.err("status", "stream", group, name, uri, status) 141 | continue 142 | data = resp.read() 143 | except urllib2.URLError: 144 | self.log.error("Error on read %s of %s %s" % (uri, group, name)) 145 | self.err("status", "chunk", group, name, uri) 146 | except socket.timeout: 147 | self.log.error("Timeout occurs on read %s of %s %s" % (uri, group, name)) 148 | self.err("timeout", "chunk", group, name, uri) 149 | self.log.debug("Probed chunk %s (len %d)" % (uri, len(data))) 150 | if len(self.loaded) > 96: 151 | self.loaded = self.loaded[32:] 152 | except KeyboardInterrupt: 153 | self.log.info("Finalize media prober %s." % self.pno) 154 | 155 | def analyze(self): 156 | """ TODO Analyze with Mediainfo. 157 | """ 158 | pass 159 | 160 | 161 | class Source: 162 | """ Reads config data and puts urls to task queue. 163 | """ 164 | def __init__(self, conf, playlistq, log): 165 | """ Initialized with `conf` config dictionary 166 | `playlistq` playlist queue, and `log` logger object. 167 | """ 168 | self.conf = conf 169 | self.playlistq = playlistq 170 | self.streams = conf["streams"] 171 | self.log = log 172 | self.streamlist = [] 173 | self._walk(conf["streams"]) 174 | log.debug("%d stream links loaded." % len(self.streamlist)) 175 | 176 | def _walk(self, streams, parent=""): 177 | """ Recursive walk of `streams` configuration tree. 178 | """ 179 | if type(streams) == dict: 180 | for key in streams.keys(): 181 | if parent: 182 | parent += "/%s" % key 183 | else: 184 | parent = key 185 | self._walk(streams[key], parent) 186 | elif type(streams) == list: 187 | for val in streams: 188 | if type(val) == str and val.startswith("http"): 189 | self.streamlist.append((parent, val)) 190 | elif type(val) is dict: 191 | self._walk(val, parent) 192 | elif type(streams) == str: 193 | self.streamlist.append((parent, streams)) 194 | 195 | def __call__(self): 196 | """ Read the channel list and put tasks to playlist queue. 197 | """ 198 | while True: 199 | try: 200 | if self.playlistq.full(): 201 | self.log.debug("Playlist queue is full (%d). Probably you need to increase number of stream probers. Check `workers/stream-probers` value." % self.playlistq.qsize()) 202 | time.sleep(3) 203 | continue 204 | group, stream = random.choice(self.streamlist) 205 | self.playlistq.put((group, stream, stream)) 206 | time.sleep(self.conf["sleep"]["streams-list"]) 207 | except KeyboardInterrupt: 208 | self.log.info("Exit source parser.") 209 | 210 | def flat(self): 211 | return self.streamlist 212 | 213 | 214 | class ErrContainer: 215 | """ Container for transferring error messages between processes. 216 | """ 217 | def __init__(self, bugq): 218 | """ Initialized with `bugq` bug queue and `where` place where error appear. 219 | """ 220 | self.stamp = 0 221 | self.bugq = bugq 222 | self.kind = "other" 223 | self.objtype = "" 224 | self.group = "" 225 | self.stream = "" 226 | self.uri = "" 227 | 228 | def __call__(self, kind="other", objtype="", group="", stream="", uri="", status=200): 229 | self.stamp = time.time() 230 | self.kind = kind 231 | self.objtype = objtype 232 | self.group = group 233 | self.stream = stream 234 | self.uri = uri 235 | self.status = status 236 | self.msg = {"stamp": self.stamp, "kind": kind, "objtype": objtype, 237 | "group": group, "stream": stream, "uri": uri, "status": status} 238 | self.bugq.put(self.msg) 239 | 240 | def get(self, msg): 241 | self.stamp = msg["stamp"] 242 | self.kind = msg["kind"] 243 | self.objtype = msg["objtype"] 244 | self.group = msg["group"] 245 | self.stream = msg["stream"] 246 | self.uri = msg["uri"] 247 | 248 | 249 | class ProblemAnalyzer: 250 | """ Gather and analyze problems with streams. Log it or send mail on critical incidents. 251 | Probers generate errors of types: timeout, status, empty, media. 252 | """ 253 | def __init__(self, conf, bugq, playlistq, chunkq, log): 254 | """ Initialized with `bugq` bug queue and `log` logger object. 255 | """ 256 | self.conf = conf 257 | self.bugq = bugq 258 | self.playlistq = playlistq 259 | self.log = log 260 | self.notify = Notify(conf, log) 261 | self.err = ErrContainer(bugq) 262 | self.playlistq = playlistq 263 | self.chunkq = chunkq 264 | self.errors = {} # key is (group, stream) and value is (stamp, kind, objtype, uri) 265 | for val in Source(conf, playlistq, log).flat(): 266 | self.errors[val] = [] 267 | self.msgerrs = {} 268 | self.last_sent = 0 269 | 270 | def __call__(self): 271 | nospam = 0 272 | while True: 273 | try: 274 | curtime = time.time() 275 | try: 276 | err = self.bugq.get(timeout=3) 277 | except: 278 | pass 279 | else: 280 | r = open("/tmp/report", "w") # XXX 281 | pprint(self.errors, r) 282 | r.close() 283 | self.err.get(err) 284 | for idx, err in enumerate(self.errors[(self.err.group, self.err.stream)]): 285 | # Remove old errors 286 | if not err["confirmed"] and curtime > err["stamp"] + self.conf["timeout"]["keep-error"]: 287 | del self.errors[(self.err.group, self.err.stream)][idx] 288 | continue 289 | # This error appeared more than once 290 | if err["kind"] == self.err.kind and err["objtype"] == self.err.objtype and err["uri"] == self.err.uri: 291 | err["count"] += 1 292 | err["reported"] = False 293 | err["confirmed"] = True 294 | self.errors[(self.err.group, self.err.stream)][idx] = err 295 | break 296 | else: 297 | # This is a new error 298 | self.errors[(self.err.group, self.err.stream)].append({"stamp": self.err.stamp, "kind": self.err.kind, "objtype": self.err.objtype, "uri": self.err.uri, "count": 0, "reported": False, "confirmed": False}) 299 | # Double check this error 300 | if err["objtype"] == "playlist": 301 | self.playlistq.put((self.err.group, self.err.stream, self.err.uri)) 302 | elif err["objtype"] == "chunk": 303 | self.chunkq.put((time.time(), self.err.group, self.err.stream, self.err.uri)) 304 | # Now make report 305 | for case in self.errors: 306 | for idx, err in enumerate(self.errors[case]): 307 | # Notify if error has not yet reported 308 | if not err["reported"] and err["confirmed"]: 309 | err["reported"] = True 310 | self.errors[case][idx] = err 311 | if case in self.msgerrs: 312 | self.msgerrs[case].update({err["uri"]: err}) 313 | else: 314 | self.msgerrs[case] = {err["uri"]: err} 315 | r = open("/tmp/msgerrs", "w") # XXX 316 | pprint(self.msgerrs, r) 317 | r.close() 318 | # Send notify 319 | if self.msgerrs and (curtime > self.last_sent + self.conf["timeout"]["spam"]): # XXX 320 | msg = "Dear human,\n\n" 321 | for case in self.msgerrs: 322 | msg += "The stream %s" % case[1] 323 | if self.err.group: 324 | msg += " of the group %s" % case[0] 325 | msg += ":\n" 326 | for uri in self.msgerrs[case]: 327 | kind = self.msgerrs[case][uri]["kind"] 328 | if kind == "status": 329 | kind = "bad status %s" % self.msgerrs[case][uri]["status"] 330 | elif kind == "empty": 331 | kind = "empty body" 332 | if self.msgerrs[case][uri]["count"] > 1: 333 | msg += "- [%s] %s still persists for" % (time.ctime(self.msgerrs[case][uri]["stamp"]), kind) 334 | else: 335 | msg += "- [%s] %s detected for" % (time.ctime(self.msgerrs[case][uri]["stamp"]), kind) 336 | msg += " %s\n" % uri 337 | msg = msg + "\n-- \nFor your service,\nHLS prober,\nthe robot." 338 | self.notify.send(msg) 339 | self.last_sent = curtime 340 | except KeyboardInterrupt: 341 | self.log.info("Exit problem analyzer.") 342 | 343 | 344 | class Notify: 345 | """ Send emails if problems detected. 346 | """ 347 | def __init__(self, conf, log): 348 | """ Initialized with mail configuration. 349 | """ 350 | self.subject = conf["notify"]["subject"] 351 | self.author = conf["notify"]["author"] 352 | self.to = conf["notify"]["addresses"] 353 | self.log = log 354 | self.send("Dear human,\nHLS probe v%s started at %s." % (__version__, time.ctime()), "HLS probe started on $HOSTNAME") 355 | 356 | def subjparse(self, subject): 357 | return check_output('bash -c "echo %s"' % subject, shell=True).strip() 358 | 359 | def send(self, text, subject=""): 360 | self.msg = MIMEText(text) 361 | if not subject: 362 | subject = self.subject 363 | self.msg['Subject'] = self.subjparse(subject) 364 | self.msg['From'] = self.author 365 | self.msg['To'] = ", ".join(self.to.split(';')) 366 | self.msg['X-Mailer'] = "HLS Probe v.%s" % __version__ 367 | #print self.msg.as_string() 368 | mail = smtplib.SMTP("localhost") 369 | #mail.set_debuglevel(9) 370 | try: 371 | mail.sendmail(self.author, self.to.split(';'), self.msg.as_string()) 372 | self.log.info("Mail delivered.") 373 | except: # SMTPRecipientsRefused: 374 | self.log.warn("Can't delivery notification!") 375 | mail.quit() 376 | 377 | 378 | class Config: 379 | """ YAML configuration. 380 | """ 381 | def __init__(self, conf, log): 382 | """ Initialized with config dictionary and logger object. 383 | """ 384 | self.conf = conf 385 | try: 386 | # TODO apply defaults 387 | self.cfg = yaml.load(open(os.path.expanduser(conf)).read()) 388 | log.debug("Loaded configuration from the %s." % os.path.expanduser(conf)) 389 | except IOError: 390 | log.fatal("Can't open config file. Exit.") 391 | exit(1) 392 | except yaml.scanner.ScannerError, err: 393 | log.fatal("Error parsing config: %s" % err) 394 | exit(1) 395 | 396 | def __call__(self): 397 | return self.cfg 398 | 399 | 400 | class FlowController: 401 | """ Manage all probe-tasks over workers. 402 | """ 403 | def __init__(self, confile, log): 404 | self.log = log 405 | self.slots = [] 406 | conf = Config(confile, log)() 407 | # Playlistq tasks are lists: (group, stream, uri) 408 | playlistq = Queue(conf["workers"]["stream-probers"]*90) 409 | # Chunkq tasks are lists: (timestamp, group, stream, uri) 410 | chunkq = Queue(conf["workers"]["media-probers"]*240) 411 | # Bugq tasks are dicts: {"stamp", "kind", "objtype", "group", "stream", "uri"} 412 | bugq = Queue(conf["workers"]["stream-probers"]*8 + conf["workers"]["media-probers"]*8) 413 | for i in range(1, conf["workers"]["stream-probers"]): 414 | self.slots.append(Process(target=CupertinoProbe(conf, playlistq, chunkq, bugq, i, log))) 415 | log.debug("%d stream probers forked." % conf["workers"]["stream-probers"]) 416 | for i in range(1, conf["workers"]["stream-probers"]): 417 | self.slots.append(Process(target=MediaProbe(conf, chunkq, bugq, i, log))) 418 | log.debug("%d media probers forked." % conf["workers"]["media-probers"]) 419 | self.slots.append(Process(target=Source(conf, playlistq, log))) 420 | self.slots.append(Process(target=ProblemAnalyzer(conf, bugq, playlistq, chunkq, log))) 421 | 422 | def run(self): 423 | for slot in self.slots: 424 | slot.start() 425 | try: 426 | signal.pause() 427 | except KeyboardInterrupt: 428 | for slot in self.slots: 429 | self.log.info("Exit subprocess %s" % slot.pid) 430 | time.sleep(0.01) 431 | slot.terminate() 432 | self.log.info("Control process %s was interrupted by operator." % os.getpid()) 433 | 434 | 435 | class Logger: 436 | """ Customizable logging to STDERR. 437 | """ 438 | def __init__(self, verbose): 439 | if verbose: 440 | level = logging.DEBUG 441 | else: 442 | level = logging.WARN 443 | log = logging.getLogger("hlsprobe") 444 | hdlr = logging.StreamHandler(sys.stderr) 445 | formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s") 446 | hdlr.setFormatter(formatter) 447 | log.addHandler(hdlr) 448 | log.setLevel(level) 449 | self.log = log 450 | self.last = "" 451 | 452 | def _check_doubles(self, msg): 453 | if msg == self.last: 454 | return False 455 | else: 456 | self.last = msg 457 | return True 458 | 459 | def debug(self, msg): 460 | if self._check_doubles(msg): 461 | self.log.info(msg) 462 | 463 | def info(self, msg): 464 | if self._check_doubles(msg): 465 | self.log.info(msg) 466 | 467 | def warn(self, msg): 468 | if self._check_doubles(msg): 469 | self.log.warn(msg) 470 | 471 | def error(self, msg): 472 | if self._check_doubles(msg): 473 | self.log.error(msg) 474 | 475 | def fatal(self, msg): 476 | if self._check_doubles(msg): 477 | self.log.fatal(msg) 478 | 479 | 480 | def base_uri(uri): 481 | parsed_url = urlparse.urlparse(uri) 482 | prefix = parsed_url.scheme + '://' + parsed_url.netloc 483 | basepath = os.path.normpath(parsed_url.path + '/..') 484 | return urlparse.urljoin(prefix, basepath) 485 | 486 | 487 | def main(): 488 | """ Workflow: 489 | Source -[playlist url]-> TaskQueue ->>> CupertinoProbe -[chunk url]-> TaskQueue -> MediaProbe [bool] 490 | """ 491 | cli = optparse.OptionParser() 492 | cli.add_option("--show-config", action="store_true", dest="show_config", help="Show parsed config (for debug purposes).") 493 | cli.add_option("-c", "--config", dest="conf", help="Use alternative configuration file.") 494 | cli.add_option("-v", "--verbose", dest="verbose", action="store_true", help="Show more probing details (for debug purposes).") 495 | opt, _ = cli.parse_args() 496 | if opt.conf: 497 | confile = opt.conf 498 | else: 499 | confile = CONFIG 500 | log = Logger(opt.verbose) 501 | if opt.show_config: 502 | pprint(Config(confile, log)()) 503 | exit() 504 | log.info("HLS Probe v%s started" % __version__) 505 | flowc = FlowController(confile, log) 506 | flowc.run() 507 | 508 | 509 | if __name__ == "__main__": 510 | main() 511 | 512 | # TODO сообщать о проблемах конфигурации в очередь ошибок 513 | -------------------------------------------------------------------------------- /hlsproberc-sample: -------------------------------------------------------------------------------- 1 | # mode:yaml 2 | # Sample config for the `hlsprobe` 3 | # 4 | # Configuration file in YAML format (http://yaml.org). 5 | # In current version of hlsprobe all sections of config file must be exist. 6 | 7 | # Define streams define URIs to streams 8 | # 9 | streams: 10 | # Just place here URIs to variant playlists or chunklists 11 | - http://example.com/playlist1.m3u8 12 | - http://example.com/playlist2.m3u8 13 | # You may group links (for example group channels by servers). 14 | # Groups in this list is optional, in simple case you just place links to playlists without group hierarchy. 15 | - server1: # Group names appears in error messages of hlsprobe. 16 | - http://srv1.example.com/playlist1.m3u8 17 | - http://example.com/playlist2.m3u8 18 | - server2: 19 | - http://srv1.example.com/playlist1.m3u8 20 | - http://example.com/playlist2.m3u8 21 | - subgroup: # Groups may be nested to any level. 22 | - http://srv1.example.com/playlist1.m3u8 23 | - http://example.com/playlist2.m3u8 24 | # Define number of workers (independent proceses which parse and analyze playlists). 25 | workers: 26 | stream-probers: 8 27 | media-probers: 32 28 | notify: 29 | report-threshold: 2 30 | addresses: admin@example.com, admin2@example.com 31 | author: hlsprobe@example.com 32 | subject: HLS probe on $HOSTNAME alert 33 | sleep: 34 | playlist-open: 0 35 | streams-list: 2 36 | timeout: 37 | target-duration: 150 38 | read: 12 39 | keep-error: 10 40 | spam: 120 41 | mode: 42 | one-segment: true 43 | -------------------------------------------------------------------------------- /sequence-diagram.plantuml: -------------------------------------------------------------------------------- 1 | Проблемы: 2 | 3 | 1 нет ответа сервера (timeout) 4 | 2 есть ответ сервера - ресурс не найден или запрещён (40x) 5 | 3 вариантный плейлист пустой (некорректный smil) 6 | 4 плейлист битрейта пустой 7 | 5 чанк не проходит mediainfo 8 | 6 сбой (любая из ошибок выше) для всей группы ресурсов [определить группы] 9 | 7 mediainfo канала не совпадает с заданным 10 | 8 битрейты не совпадают с заданными 11 | 12 | @startuml 13 | 14 | title HLS Probe Workflow 15 | 16 | participant FlowController 17 | participant Source 18 | participant Playlists <> 19 | participant CupertinoProbe 20 | participant Chunks <> 21 | participant MediaProbe 22 | participant Errors <> 23 | participant ProblemAnalyzer 24 | 25 | == Initialization == 26 | 27 | activate FlowController 28 | 29 | FlowController-->Playlists: create queue 30 | activate Playlists 31 | FlowController-->Chunks: create queue 32 | activate Chunks 33 | FlowController-->Errors: create queue 34 | activate Errors 35 | 36 | FlowController-->Source: spawn process 37 | activate Source 38 | FlowController-->CupertinoProbe: spawn process 39 | activate CupertinoProbe 40 | FlowController-->MediaProbe: spawn process 41 | activate MediaProbe 42 | FlowController-->ProblemAnalyzer: spawn process 43 | activate ProblemAnalyzer 44 | 45 | == Main flow == 46 | 47 | Source->Source: choice (random or sequental) stream URI 48 | Source->Playlists: put URI 49 | CupertinoProbe<-Playlists: get URI 50 | 51 | 52 | == End of work == 53 | 54 | deactivate Source 55 | FlowController-->ProblemAnalyzer: end process 56 | deactivate ProblemAnalyzer 57 | deactivate CupertinoProbe 58 | deactivate MediaProbe 59 | deactivate Chunks 60 | deactivate Playlists 61 | deactivate Errors 62 | deactivate FlowController 63 | 64 | @enduml --------------------------------------------------------------------------------