Enigma2 plugin to to play various online streams (mostly Latvian).

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. # -*- Mode: Python -*-
  2. # vi:si:et:sw=4:sts=4:ts=4
  3. #
  4. # Copyright (C) 2009-2010 Fluendo, S.L. (www.fluendo.com).
  5. # Copyright (C) 2009-2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
  6. # Copyright (C) 2014 Juan Font Alonso <juanfontalonso@gmail.com>
  7. # This file may be distributed and/or modified under the terms of
  8. # the GNU General Public License version 2 as published by
  9. # the Free Software Foundation.
  10. # This file is distributed without any warranty; without even the implied
  11. # warranty of merchantability or fitness for a particular purpose.
  12. # See "LICENSE" in the source distribution for more information.
  13. import logging
  14. import re
  15. import urllib2
  16. class M3U8(object):
  17. def __init__(self, url=None):
  18. self.url = url
  19. self._programs = [] # main list of programs & bandwidth
  20. self._files = {} # the current program playlist
  21. self._first_sequence = None # the first sequence to start fetching
  22. self._last_sequence = None # the last sequence, to compute reload delay
  23. self._reload_delay = None # the initial reload delay
  24. self._update_tries = None # the number consecutive reload tries
  25. self._last_content = None
  26. self._endlist = False # wether the list ended and should not be refreshed
  27. self._encryption_method = None
  28. self._key_url = None
  29. self._key = None
  30. def endlist(self):
  31. return self._endlist
  32. def has_programs(self):
  33. return len(self._programs) != 0
  34. def get_program_playlist(self, program_id=None, bitrate=None):
  35. # return the (uri, dict) of the best matching playlist
  36. if not self.has_programs():
  37. raise
  38. _, best = min((abs(int(x['BANDWIDTH']) - bitrate), x)
  39. for x in self._programs)
  40. return best['uri'], best
  41. def reload_delay(self):
  42. # return the time between request updates, in seconds
  43. if self._endlist or not self._last_sequence:
  44. raise
  45. if self._update_tries == 0:
  46. ld = self._files[self._last_sequence]['duration']
  47. self._reload_delay = min(self.target_duration * 3, ld)
  48. d = self._reload_delay
  49. elif self._update_tries == 1:
  50. d = self._reload_delay * 0.5
  51. elif self._update_tries == 2:
  52. d = self._reload_delay * 1.5
  53. else:
  54. d = self._reload_delay * 3.0
  55. logging.debug('Reload delay is %r' % d)
  56. return int(d)
  57. def has_files(self):
  58. return len(self._files) != 0
  59. def iter_files(self):
  60. # return an iter on the playlist media files
  61. if not self.has_files():
  62. return
  63. if not self._endlist:
  64. current = max(self._first_sequence, self._last_sequence - 3)
  65. else:
  66. # treat differently on-demand playlists?
  67. current = self._first_sequence
  68. while True:
  69. try:
  70. f = self._files[current]
  71. current += 1
  72. yield f
  73. if (f.has_key('endlist')):
  74. break
  75. except:
  76. yield None
  77. def update(self, content):
  78. # update this "constructed" playlist,
  79. # return wether it has actually been updated
  80. if self._last_content and content == self._last_content:
  81. logging.info("Content didn't change")
  82. self._update_tries += 1
  83. return False
  84. self._update_tries = 0
  85. self._last_content = content
  86. def get_lines_iter(c):
  87. c = c.decode("utf-8-sig")
  88. for l in c.split('\n'):
  89. if l.startswith('#EXT'):
  90. yield l
  91. elif l.startswith('#'):
  92. pass
  93. else:
  94. yield l
  95. self._lines = get_lines_iter(content)
  96. first_line = self._lines.next()
  97. if not first_line.startswith('#EXTM3U'):
  98. logging.error('Invalid first line: %r' % first_line)
  99. raise
  100. self.target_duration = None
  101. discontinuity = False
  102. allow_cache = None
  103. i = 0
  104. new_files = []
  105. for l in self._lines:
  106. if l.startswith('#EXT-X-STREAM-INF'):
  107. def to_dict(l):
  108. i = re.findall('(?:[\w-]*="[\w\.\,]*")|(?:[\w-]*=[\w]*)', l)
  109. d = {v.split('=')[0]: v.split('=')[1].replace('"','') for v in i}
  110. return d
  111. d = to_dict(l[18:])
  112. print "stream info: " + str(d)
  113. d['uri'] = self._lines.next()
  114. self._add_playlist(d)
  115. elif l.startswith('#EXT-X-TARGETDURATION'):
  116. self.target_duration = int(l[22:])
  117. elif l.startswith('#EXT-X-MEDIA-SEQUENCE'):
  118. self.media_sequence = int(l[22:])
  119. i = self.media_sequence
  120. elif l.startswith('#EXT-X-DISCONTINUITY'):
  121. discontinuity = True
  122. elif l.startswith('#EXT-X-PROGRAM-DATE-TIME'):
  123. print l
  124. elif l.startswith('#EXT-X-ALLOW-CACHE'):
  125. allow_cache = l[19:]
  126. elif l.startswith('#EXT-X-KEY'):
  127. self._encryption_method = l.split(',')[0][18:]
  128. self._key_url = l.split(',')[1][5:-1]
  129. response = urllib2.urlopen(self._key_url)
  130. self._key = response.read()
  131. response.close()
  132. elif l.startswith('#EXTINF'):
  133. v = l[8:].split(',')
  134. d = dict(file=self._lines.next().strip(),
  135. title=v[1].strip(),
  136. duration=float(v[0]),
  137. sequence=i,
  138. discontinuity=discontinuity,
  139. allow_cache=allow_cache)
  140. discontinuity = False
  141. i += 1
  142. new = self._set_file(i, d)
  143. if i > self._last_sequence:
  144. self._last_sequence = i
  145. if new:
  146. new_files.append(d)
  147. elif l.startswith('#EXT-X-ENDLIST'):
  148. if i > 0:
  149. self._files[i]['endlist'] = True
  150. self._endlist = True
  151. elif l.startswith('#EXT-X-VERSION'):
  152. pass
  153. elif len(l.strip()) != 0:
  154. print l
  155. if not self.has_programs() and not self.target_duration:
  156. logging.error("Invalid HLS stream: no programs & no duration")
  157. raise
  158. if len(new_files):
  159. logging.debug("got new files in playlist: %r", new_files)
  160. return True
  161. def _add_playlist(self, d):
  162. self._programs.append(d)
  163. def _set_file(self, sequence, d):
  164. new = False
  165. if not self._files.has_key(sequence):
  166. new = True
  167. if not self._first_sequence:
  168. self._first_sequence = sequence
  169. elif sequence < self._first_sequence:
  170. self._first_sequence = sequence
  171. self._files[sequence] = d
  172. return new
  173. def __repr__(self):
  174. return "M3U8 %r %r" % (self._programs, self._files)