[Python-modules-commits] [python-m3u8] 01/04: Import python-m3u8_0.3.0.orig.tar.gz
Ondřej Nový
onovy at moszumanska.debian.org
Fri Oct 7 20:44:24 UTC 2016
This is an automated email from the git hooks/post-receive script.
onovy pushed a commit to branch master
in repository python-m3u8.
commit e5a1810286287c1e2db6aedb4c5a46c2b8f8e141
Author: Ondřej Nový <onovy at debian.org>
Date: Fri Oct 7 22:03:04 2016 +0200
Import python-m3u8_0.3.0.orig.tar.gz
---
.gitignore | 4 +
README.rst | 70 +++++++++++++++--
m3u8/__init__.py | 16 +++-
m3u8/mixins.py | 54 ++++++++++++++
m3u8/model.py | 200 +++++++++++++++++++++++++------------------------
m3u8/parser.py | 99 +++++++++++++++++++-----
m3u8/protocol.py | 2 +
setup.py | 5 +-
tests/playlists.py | 181 +++++++++++++++++++++++++++++++++++++++++++-
tests/test_loader.py | 54 ++++++++------
tests/test_model.py | 207 ++++++++++++++++++++++++++++++++++++++++-----------
tests/test_parser.py | 70 ++++++++++++++---
12 files changed, 758 insertions(+), 204 deletions(-)
diff --git a/.gitignore b/.gitignore
index 432207e..d48b86f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,5 +3,9 @@
tests/server.stdout
dist/
build/
+bin/
+include/
+lib/
+local/
.coverage
.cache
diff --git a/README.rst b/README.rst
index 4977d33..31c75a6 100644
--- a/README.rst
+++ b/README.rst
@@ -34,20 +34,76 @@ directly from a string:
m3u8_obj = m3u8.loads('#EXTM3U8 ... etc ... ')
-Encryption key
---------------
-The segments may be encrypted, in this case the ``key`` attribute will
-be an object with all the attributes from `#EXT-X-KEY`_:
+Encryption keys
+---------------
+
+The segments may be or not encrypted. The ``keys`` attribute list will
+be an list with all the different keys as described with `#EXT-X-KEY`_:
+
+Each key has the next properties:
- ``method``: ex.: "AES-128"
- ``uri``: the key uri, ex.: "http://videoserver.com/key.bin"
- ``iv``: the initialization vector, if available. Otherwise ``None``.
-If no ``#EXT-X-KEY`` is found, the ``key`` attribute will be ``None``.
+If no ``#EXT-X-KEY`` is found, the ``keys`` list will have a unique element ``None``. Multiple keys are supported.
+
+If unencrypted and encrypted segments are mixed in the M3U8 file, then the list will contain a ``None`` element, with one
+or more keys afterwards.
+
+To traverse the list of keys available:
+
+::
+
+ import m3u8
+
+ m3u8_obj = m3u8.loads('#EXTM3U8 ... etc ...')
+ len(m3u8_obj.keys) => returns the number of keys available in the list (normally 1)
+ for key in m3u8_obj.keys:
+ if key: # First one could be None
+ key.uri
+ key.method
+ key.iv
+
+
+Getting segments encrypted with one key
+---------------------------------------
+
+There are cases where listing segments for a given key is important. It's possible to
+retrieve the list of segments encrypted with one key via ``by_key`` method in the
+``segments`` list.
+
+Example of getting the segments with no encryption:
+
+::
+
+ import m3u8
+
+ m3u8_obj = m3u8.loads('#EXTM3U8 ... etc ...')
+ segmk1 = m3u8_obj.segments.by_key(None)
+
+ # Get the list of segments encrypted using last key
+ segm = m3u8_obj.segments.by_key( m3u8_obj.keys[-1] )
+
+
+With this method, is now possible also to change the key from some of the segments programatically:
+
+
+::
+
+ import m3u8
+
+ m3u8_obj = m3u8.loads('#EXTM3U8 ... etc ...')
+
+ # Create a new Key and replace it
+ new_key = m3u8.Key("AES-128", "/encrypted/newkey.bin", None, iv="0xf123ad23f22e441098aa87ee")
+ for segment in m3u8_obj.segments.by_key( m3u8_obj.keys[-1] ):
+ segm.key = new_key
+ # Remember to sync the key from the list as well
+ m3u8_obj.keys[-1] = new_key
+
-Multiple keys are not supported yet (and has a low priority), follow
-`issue 1`_ for updates.
Variant playlists (variable bitrates)
-------------------------------------
diff --git a/m3u8/__init__.py b/m3u8/__init__.py
index 0f364fb..7393089 100644
--- a/m3u8/__init__.py
+++ b/m3u8/__init__.py
@@ -25,6 +25,7 @@ from m3u8.parser import parse, is_url, ParseError
__all__ = ('M3U8', 'Playlist', 'IFramePlaylist', 'Media',
'Segment', 'loads', 'load', 'parse', 'ParseError')
+
def loads(content):
'''
Given a string with a m3u8 content, returns a M3U8 object.
@@ -32,7 +33,8 @@ def loads(content):
'''
return M3U8(content)
-def load(uri, timeout = None):
+
+def load(uri, timeout=None):
'''
Retrieves the content from a given URI and returns a M3U8 object.
Raises ValueError if invalid content or IOError if request fails.
@@ -44,7 +46,9 @@ def load(uri, timeout = None):
return _load_from_file(uri)
# Support for python3 inspired by https://github.com/szemtiv/m3u8/
-def _load_from_uri(uri, timeout = None):
+
+
+def _load_from_uri(uri, timeout=None):
resource = urlopen(uri, timeout=timeout)
base_uri = _parsed_url(_url_for(uri))
if PYTHON_MAJOR_VERSION < (3,):
@@ -53,24 +57,28 @@ def _load_from_uri(uri, timeout = None):
content = _read_python3x(resource)
return M3U8(content, base_uri=base_uri)
+
def _url_for(uri):
return urlopen(uri).geturl()
+
def _parsed_url(url):
parsed_url = url_parser.urlparse(url)
prefix = parsed_url.scheme + '://' + parsed_url.netloc
base_path = posixpath.normpath(parsed_url.path + '/..')
return url_parser.urljoin(prefix, base_path)
+
def _read_python2x(resource):
return resource.read().strip()
+
def _read_python3x(resource):
- return resource.read().decode(resource.headers.get_content_charset(failobj="utf-8"))
+ return resource.read().decode(resource.headers.get_content_charset(failobj="utf-8"))
+
def _load_from_file(uri):
with open(uri) as fileobj:
raw_content = fileobj.read().strip()
base_uri = os.path.dirname(uri)
return M3U8(raw_content, base_uri=base_uri)
-
diff --git a/m3u8/mixins.py b/m3u8/mixins.py
new file mode 100644
index 0000000..e2576f5
--- /dev/null
+++ b/m3u8/mixins.py
@@ -0,0 +1,54 @@
+
+import os
+from m3u8.parser import is_url
+
+try:
+ import urlparse as url_parser
+except ImportError:
+ import urllib.parse as url_parser
+
+
+def _urijoin(base_uri, path):
+ if is_url(base_uri):
+ return url_parser.urljoin(base_uri, path)
+ else:
+ return os.path.normpath(os.path.join(base_uri, path.strip('/')))
+
+
+class BasePathMixin(object):
+
+ @property
+ def absolute_uri(self):
+ if self.uri is None:
+ return None
+ if is_url(self.uri):
+ return self.uri
+ else:
+ if self.base_uri is None:
+ raise ValueError('There can not be `absolute_uri` with no `base_uri` set')
+ return _urijoin(self.base_uri, self.uri)
+
+ @property
+ def base_path(self):
+ return os.path.dirname(self.uri)
+
+ @base_path.setter
+ def base_path(self, newbase_path):
+ if not self.base_path:
+ self.uri = "%s/%s" % (newbase_path, self.uri)
+ self.uri = self.uri.replace(self.base_path, newbase_path)
+
+
+class GroupedBasePathMixin(object):
+
+ def _set_base_uri(self, new_base_uri):
+ for item in self:
+ item.base_uri = new_base_uri
+
+ base_uri = property(None, _set_base_uri)
+
+ def _set_base_path(self, newbase_path):
+ for item in self:
+ item.base_path = newbase_path
+
+ base_path = property(None, _set_base_path)
diff --git a/m3u8/model.py b/m3u8/model.py
index 8f653e9..591faa7 100644
--- a/m3u8/model.py
+++ b/m3u8/model.py
@@ -8,12 +8,8 @@ import os
import errno
import math
-try:
- import urlparse as url_parser
-except ImportError:
- import urllib.parse as url_parser
-
-from m3u8 import parser
+from m3u8.parser import parse, format_date_time
+from m3u8.mixins import BasePathMixin, GroupedBasePathMixin
class M3U8(object):
@@ -41,8 +37,22 @@ class M3U8(object):
Attributes:
- `key`
- it's a `Key` object, the EXT-X-KEY from m3u8. Or None
+ `keys`
+ Returns the list of `Key` objects used to encrypt the segments from m3u8.
+ It covers the whole list of possible situations when encryption either is
+ used or not.
+
+ 1. No encryption.
+ `keys` list will only contain a `None` element.
+
+ 2. Encryption enabled for all segments.
+ `keys` list will contain the key used for the segments.
+
+ 3. No encryption for first element(s), encryption is applied afterwards
+ `keys` list will contain `None` and the key used for the rest of segments.
+
+ 4. Multiple keys used during the m3u8 manifest.
+ `keys` list will contain the key used for each set of segments.
`segments`
a `SegmentList` object, represents the list of `Segment`s from this playlist
@@ -122,11 +132,11 @@ class M3U8(object):
('version', 'version'),
('allow_cache', 'allow_cache'),
('playlist_type', 'playlist_type')
- )
+ )
def __init__(self, content=None, base_path=None, base_uri=None, strict=False):
if content is not None:
- self.data = parser.parse(content, strict)
+ self.data = parse(content, strict)
else:
self.data = {}
self._base_uri = base_uri
@@ -137,35 +147,35 @@ class M3U8(object):
self._initialize_attributes()
self.base_path = base_path
- def _initialize_attributes(self):
- self.key = Key(base_uri=self.base_uri, **self.data['key']) if 'key' in self.data else None
- self.segments = SegmentList([ Segment(base_uri=self.base_uri, **params)
- for params in self.data.get('segments', []) ])
+ def _initialize_attributes(self):
+ self.keys = [ Key(base_uri=self.base_uri, **params) if params else None
+ for params in self.data.get('keys', []) ]
+ self.segments = SegmentList([ Segment(base_uri=self.base_uri, keyobject=find_key(segment.get('key', {}), self.keys), **segment)
+ for segment in self.data.get('segments', []) ])
+ #self.keys = get_uniques([ segment.key for segment in self.segments ])
for attr, param in self.simple_attributes:
setattr(self, attr, self.data.get(param))
self.files = []
- if self.key:
- self.files.append(self.key.uri)
+ for key in self.keys:
+ # Avoid None key, it could be the first one, don't repeat them
+ if key and key.uri not in self.files:
+ self.files.append(key.uri)
self.files.extend(self.segments.uri)
- self.media = MediaList([ Media(base_uri=self.base_uri,
- **media)
+ self.media = MediaList([ Media(base_uri=self.base_uri, **media)
for media in self.data.get('media', []) ])
- self.playlists = PlaylistList([ Playlist(base_uri=self.base_uri,
- media=self.media,
- **playlist)
+ self.playlists = PlaylistList([ Playlist(base_uri=self.base_uri, media=self.media, **playlist)
for playlist in self.data.get('playlists', []) ])
self.iframe_playlists = PlaylistList()
for ifr_pl in self.data.get('iframe_playlists', []):
- self.iframe_playlists.append(
- IFramePlaylist(base_uri=self.base_uri,
- uri=ifr_pl['uri'],
- iframe_stream_info=ifr_pl['iframe_stream_info'])
- )
+ self.iframe_playlists.append(IFramePlaylist(base_uri=self.base_uri,
+ uri=ifr_pl['uri'],
+ iframe_stream_info=ifr_pl['iframe_stream_info'])
+ )
def __unicode__(self):
return self.dumps()
@@ -180,6 +190,9 @@ class M3U8(object):
self.media.base_uri = new_base_uri
self.playlists.base_uri = new_base_uri
self.segments.base_uri = new_base_uri
+ for key in self.keys:
+ if key:
+ key.base_uri = new_base_uri
@property
def base_path(self):
@@ -193,11 +206,13 @@ class M3U8(object):
def _update_base_path(self):
if self._base_path is None:
return
- if self.key:
- self.key.base_path = self.base_path
- self.media.base_path = self.base_path
- self.segments.base_path = self.base_path
- self.playlists.base_path = self.base_path
+ for key in self.keys:
+ if key:
+ key.base_path = self._base_path
+ self.media.base_path = self._base_path
+ self.segments.base_path = self._base_path
+ self.playlists.base_path = self._base_path
+
def add_playlist(self, playlist):
self.is_variant = True
@@ -228,15 +243,13 @@ class M3U8(object):
output.append('#EXT-X-ALLOW-CACHE:' + self.allow_cache.upper())
if self.version:
output.append('#EXT-X-VERSION:' + self.version)
- if self.key:
- output.append(str(self.key))
if self.target_duration:
- output.append('#EXT-X-TARGETDURATION:' + int_or_float_to_string(self.target_duration))
+ output.append('#EXT-X-TARGETDURATION:' +
+ int_or_float_to_string(self.target_duration))
if self.program_date_time is not None:
- output.append('#EXT-X-PROGRAM-DATE-TIME:' + parser.format_date_time(self.program_date_time))
+ output.append('#EXT-X-PROGRAM-DATE-TIME:' + format_date_time(self.program_date_time))
if not (self.playlist_type is None or self.playlist_type == ''):
- output.append(
- '#EXT-X-PLAYLIST-TYPE:%s' % str(self.playlist_type).upper())
+ output.append('#EXT-X-PLAYLIST-TYPE:%s' % str(self.playlist_type).upper())
if self.is_i_frames_only:
output.append('#EXT-X-I-FRAMES-ONLY')
if self.is_variant:
@@ -245,7 +258,6 @@ class M3U8(object):
output.append(str(self.playlists))
if self.iframe_playlists:
output.append(str(self.iframe_playlists))
-
output.append(str(self.segments))
if self.is_endlist:
@@ -270,42 +282,6 @@ class M3U8(object):
if error.errno != errno.EEXIST:
raise
-class BasePathMixin(object):
-
- @property
- def absolute_uri(self):
- if self.uri is None:
- return None
- if parser.is_url(self.uri):
- return self.uri
- else:
- if self.base_uri is None:
- raise ValueError('There can not be `absolute_uri` with no `base_uri` set')
- return _urijoin(self.base_uri, self.uri)
-
- @property
- def base_path(self):
- return os.path.dirname(self.uri)
-
- @base_path.setter
- def base_path(self, newbase_path):
- if not self.base_path:
- self.uri = "%s/%s" % (newbase_path, self.uri)
- self.uri = self.uri.replace(self.base_path, newbase_path)
-
-class GroupedBasePathMixin(object):
-
- def _set_base_uri(self, new_base_uri):
- for item in self:
- item.base_uri = new_base_uri
-
- base_uri = property(None, _set_base_uri)
-
- def _set_base_path(self, newbase_path):
- for item in self:
- item.base_path = newbase_path
-
- base_path = property(None, _set_base_path)
class Segment(BasePathMixin):
'''
@@ -349,7 +325,7 @@ class Segment(BasePathMixin):
def __init__(self, uri, base_uri, program_date_time=None, duration=None,
title=None, byterange=None, cue_out=False, discontinuity=False, key=None,
- scte35=None, scte35_duration=None):
+ scte35=None, scte35_duration=None, keyobject=None):
self.uri = uri
self.duration = duration
self.title = title
@@ -360,19 +336,25 @@ class Segment(BasePathMixin):
self.cue_out = cue_out
self.scte35 = scte35
self.scte35_duration = scte35_duration
- self.key = Key(base_uri=base_uri,**key) if key else None
-
+ self.key = keyobject
+ # Key(base_uri=base_uri, **key) if key else None
def dumps(self, last_segment):
output = []
if last_segment and self.key != last_segment.key:
- output.append(str(self.key))
- output.append('\n')
+ output.append(str(self.key))
+ output.append('\n')
+ else:
+ # The key must be checked anyway now for the first segment
+ if self.key and last_segment is None:
+ output.append(str(self.key))
+ output.append('\n')
if self.discontinuity:
output.append('#EXT-X-DISCONTINUITY\n')
if self.program_date_time:
- output.append('#EXT-X-PROGRAM-DATE-TIME:%s\n' % parser.format_date_time(self.program_date_time))
+ output.append('#EXT-X-PROGRAM-DATE-TIME:%s\n' %
+ format_date_time(self.program_date_time))
if self.cue_out:
output.append('#EXT-X-CUE-OUT-CONT\n')
output.append('#EXTINF:%s,' % int_or_float_to_string(self.duration))
@@ -391,20 +373,27 @@ class Segment(BasePathMixin):
def __str__(self):
return self.dumps(None)
+
class SegmentList(list, GroupedBasePathMixin):
def __str__(self):
output = []
last_segment = None
for segment in self:
- output.append(segment.dumps(last_segment))
- last_segment = segment
+ output.append(segment.dumps(last_segment))
+ last_segment = segment
return '\n'.join(output)
@property
def uri(self):
return [seg.uri for seg in self]
+
+ def by_key(self, key):
+ return [ segment for segment in self if segment.key == key ]
+
+
+
class Key(BasePathMixin):
'''
Key used to encrypt the segments in a m3u8 playlist (EXT-X-KEY)
@@ -422,6 +411,7 @@ class Key(BasePathMixin):
initialization vector. a string representing a hexadecimal number. ex.: 0X12A
'''
+
def __init__(self, method, uri, base_uri, iv=None, keyformat=None, keyformatversions=None):
self.method = method
self.uri = uri
@@ -433,7 +423,7 @@ class Key(BasePathMixin):
def __str__(self):
output = [
'METHOD=%s' % self.method,
- ]
+ ]
if self.uri:
output.append('URI="%s"' % self.uri)
if self.iv:
@@ -446,12 +436,14 @@ class Key(BasePathMixin):
return '#EXT-X-KEY:' + ','.join(output)
def __eq__(self, other):
+ if not other:
+ return False
return self.method == other.method and \
- self.uri == other.uri and \
- self.iv == other.iv and \
- self.base_uri == other.base_uri and \
- self.keyformat == other.keyformat and \
- self.keyformatversions == other.keyformatversions
+ self.uri == other.uri and \
+ self.iv == other.iv and \
+ self.base_uri == other.base_uri and \
+ self.keyformat == other.keyformat and \
+ self.keyformatversions == other.keyformatversions
def __ne__(self, other):
return not self.__eq__(other)
@@ -471,6 +463,7 @@ class Playlist(BasePathMixin):
More info: http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.10
'''
+
def __init__(self, uri, stream_info, media, base_uri):
self.uri = uri
self.base_uri = base_uri
@@ -508,7 +501,8 @@ class Playlist(BasePathMixin):
stream_inf.append('AVERAGE-BANDWIDTH=%d' %
self.stream_info.average_bandwidth)
if self.stream_info.resolution:
- res = str(self.stream_info.resolution[0]) + 'x' + str(self.stream_info.resolution[1])
+ res = str(self.stream_info.resolution[
+ 0]) + 'x' + str(self.stream_info.resolution[1])
stream_inf.append('RESOLUTION=' + res)
if self.stream_info.codecs:
stream_inf.append('CODECS=' + quoted(self.stream_info.codecs))
@@ -524,6 +518,7 @@ class Playlist(BasePathMixin):
return '#EXT-X-STREAM-INF:' + ','.join(stream_inf) + '\n' + self.uri
+
class IFramePlaylist(BasePathMixin):
'''
IFramePlaylist object representing a link to a
@@ -537,6 +532,7 @@ class IFramePlaylist(BasePathMixin):
More info: http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.13
'''
+
def __init__(self, base_uri, uri, iframe_stream_info):
self.uri = uri
self.base_uri = base_uri
@@ -581,6 +577,7 @@ StreamInfo = namedtuple(
['bandwidth', 'average_bandwidth', 'program_id', 'resolution', 'codecs']
)
+
class Media(BasePathMixin):
'''
A media object from a M3U8 playlist
@@ -608,7 +605,7 @@ class Media(BasePathMixin):
def __init__(self, uri=None, type=None, group_id=None, language=None,
name=None, default=None, autoselect=None, forced=None,
characteristics=None, assoc_language=None,
- instream_id=None,base_uri=None, **extras):
+ instream_id=None, base_uri=None, **extras):
self.base_uri = base_uri
self.uri = uri
self.type = type
@@ -654,6 +651,7 @@ class Media(BasePathMixin):
def __str__(self):
return self.dumps()
+
class MediaList(list, GroupedBasePathMixin):
def __str__(self):
@@ -664,6 +662,7 @@ class MediaList(list, GroupedBasePathMixin):
def uri(self):
return [media.uri for media in self]
+
class PlaylistList(list, GroupedBasePathMixin):
def __str__(self):
@@ -671,17 +670,26 @@ class PlaylistList(list, GroupedBasePathMixin):
return '\n'.join(output)
+def find_key(keydata, keylist):
+ if not keydata:
+ return None
+ for key in keylist:
+ if key:
+ # Check the intersection of keys and values
+ if keydata.get('uri', None) == key.uri and \
+ keydata.get('method', 'NONE') == key.method and \
+ keydata.get('iv', None) == key.iv:
+ return key
+ raise KeyError("No key found for key data")
+
+
def denormalize_attribute(attribute):
- return attribute.replace('_','-').upper()
+ return attribute.replace('_', '-').upper()
+
def quoted(string):
return '"%s"' % string
-def _urijoin(base_uri, path):
- if parser.is_url(base_uri):
- return url_parser.urljoin(base_uri, path)
- else:
- return os.path.normpath(os.path.join(base_uri, path.strip('/')))
def int_or_float_to_string(number):
return str(int(number)) if number == math.floor(number) else str(number)
diff --git a/m3u8/parser.py b/m3u8/parser.py
index 9840d1a..21e6207 100644
--- a/m3u8/parser.py
+++ b/m3u8/parser.py
@@ -9,25 +9,24 @@ import itertools
import re
from m3u8 import protocol
-try:
- import exceptions
- ExceptionBaseClass = exceptions.Exception
-except ImportError:
- ExceptionBaseClass = object
-
'''
http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.2
http://stackoverflow.com/questions/2785755/how-to-split-but-ignore-separators-in-quoted-strings-in-python
'''
ATTRIBUTELISTPATTERN = re.compile(r'''((?:[^,"']|"[^"]*"|'[^']*')+)''')
+
def cast_date_time(value):
return iso8601.parse_date(value)
+
def format_date_time(value):
return value.isoformat()
-class ParseError(ExceptionBaseClass):
+
+
+class ParseError(Exception):
+
def __init__(self, lineno, line):
self.lineno = lineno
self.line = line
@@ -35,6 +34,8 @@ class ParseError(ExceptionBaseClass):
def __str__(self):
return 'Syntax error in manifest on line %d: %s' % (self.lineno, self.line)
+
+
def parse(content, strict=False):
'''
Given a M3U8 playlist content returns a dictionary with all data found
@@ -47,15 +48,17 @@ def parse(content, strict=False):
'is_independent_segments': False,
'playlist_type': None,
'playlists': [],
- 'iframe_playlists': [],
'segments': [],
+ 'iframe_playlists': [],
'media': [],
- }
+ 'keys': [],
+ }
state = {
'expect_segment': False,
'expect_playlist': False,
- }
+ 'current_key': None,
+ }
lineno = 0
for line in string_to_lines(content):
@@ -86,6 +89,15 @@ def parse(content, strict=False):
state['cue_out'] = True
state['cue_start'] = True
+ elif line.startswith(protocol.ext_x_cue_out_start):
+ _parse_cueout_start(line, state, string_to_lines(content)[lineno - 2])
+ state['cue_out'] = True
+ state['cue_start'] = True
+
+ elif line.startswith(protocol.ext_x_cue_span):
+ state['cue_out'] = True
+ state['cue_start'] = True
+
elif line.startswith(protocol.ext_x_version):
_parse_simple_parameter(line, data)
@@ -93,11 +105,13 @@ def parse(content, strict=False):
_parse_simple_parameter(line, data)
elif line.startswith(protocol.ext_x_key):
- state['current_key'] = _parse_key(line)
- data['key'] = data.get('key', state['current_key'])
+ key = _parse_key(line)
+ state['current_key'] = key
+ if key not in data['keys']:
+ data['keys'].append(key)
elif line.startswith(protocol.extinf):
- _parse_extinf(line, data, state)
+ _parse_extinf(line, data, state, lineno, strict)
state['expect_segment'] = True
elif line.startswith(protocol.ext_x_stream_inf):
@@ -143,6 +157,7 @@ def parse(content, strict=False):
return data
+
def _parse_key(line):
params = ATTRIBUTELISTPATTERN.split(line.replace(protocol.ext_x_key + ':', ''))[1::2]
key = {}
@@ -151,13 +166,23 @@ def _parse_key(line):
key[normalize_attribute(name)] = remove_quotes(value)
return key
-def _parse_extinf(line, data, state):
- duration, title = line.replace(protocol.extinf + ':', '').split(',')
+
+def _parse_extinf(line, data, state, lineno, strict):
+ chunks = line.replace(protocol.extinf + ':', '').split(',')
+ if len(chunks) == 2:
+ duration, title = chunks
+ elif len(chunks) == 1:
+ if strict:
+ raise ParseError(lineno, line)
+ else:
+ duration = chunks[0]
+ title = ''
if 'segment' not in state:
state['segment'] = {}
state['segment']['duration'] = float(duration)
state['segment']['title'] = remove_quotes(title)
+
def _parse_ts_chunk(line, data, state):
segment = state.pop('segment')
if state.get('current_program_date_time'):
@@ -167,12 +192,17 @@ def _parse_ts_chunk(line, data, state):
segment['cue_out'] = state.pop('cue_out', False)
if state.get('current_cue_out_scte35'):
segment['scte35'] = state['current_cue_out_scte35']
- segment['scte35_duration'] = state['current_cue_out_duration']
+ segment['scte35_duration'] = state['current_cue_out_duration']
segment['discontinuity'] = state.pop('discontinuity', False)
if state.get('current_key'):
- segment['key'] = state['current_key']
+ segment['key'] = state['current_key']
+ else:
+ # For unencrypted segments, the initial key would be None
+ if None not in data['keys']:
+ data['keys'].append(None)
data['segments'].append(segment)
+
def _parse_attribute_list(prefix, line, atribute_parser):
params = ATTRIBUTELISTPATTERN.split(line.replace(prefix + ':', ''))[1::2]
@@ -188,6 +218,7 @@ def _parse_attribute_list(prefix, line, atribute_parser):
return attributes
+
def _parse_stream_inf(line, data, state):
data['is_variant'] = True
data['media_sequence'] = None
@@ -197,6 +228,7 @@ def _parse_stream_inf(line, data, state):
atribute_parser["average_bandwidth"] = int
state['stream_info'] = _parse_attribute_list(protocol.ext_x_stream_inf, line, atribute_parser)
+
def _parse_i_frame_stream_inf(line, data):
atribute_parser = remove_quotes_parser('codecs', 'uri')
atribute_parser["program_id"] = int
@@ -207,22 +239,26 @@ def _parse_i_frame_stream_inf(line, data):
data['iframe_playlists'].append(iframe_playlist)
+
def _parse_media(line, data, state):
quoted = remove_quotes_parser('uri', 'group_id', 'language', 'name', 'characteristics')
media = _parse_attribute_list(protocol.ext_x_media, line, quoted)
data['media'].append(media)
+
def _parse_variant_playlist(line, data, state):
playlist = {'uri': line,
'stream_info': state.pop('stream_info')}
data['playlists'].append(playlist)
+
def _parse_byterange(line, state):
if 'segment' not in state:
state['segment'] = {}
state['segment']['byterange'] = line.replace(protocol.ext_x_byterange + ':', '')
+
def _parse_simple_parameter_raw_value(line, cast_to=str, normalize=False):
param, value = line.split(':', 1)
param = normalize_attribute(param.replace('#EXT-X-', ''))
@@ -230,14 +266,17 @@ def _parse_simple_parameter_raw_value(line, cast_to=str, normalize=False):
value = normalize_attribute(value)
return param, cast_to(value)
+
def _parse_and_set_simple_parameter_raw_value(line, data, cast_to=str, normalize=False):
param, value = _parse_simple_parameter_raw_value(line, cast_to, normalize)
data[param] = value
return data[param]
+
def _parse_simple_parameter(line, data, cast_to=str):
return _parse_and_set_simple_parameter_raw_value(line, data, cast_to, True)
+
def _parse_cueout(line, state):
param, value = line.split(':', 1)
res = re.match('.*Duration=(.*),SCTE35=(.*)$', value)
@@ -245,12 +284,36 @@ def _parse_cueout(line, state):
state['current_cue_out_duration'] = res.group(1)
state['current_cue_out_scte35'] = res.group(2)
+def _cueout_elemental(line, state, prevline):
+ param, value = line.split(':', 1)
+ res = re.match('.*EXT-OATCLS-SCTE35:(.*)$', prevline)
+ if res:
+ return (res.group(1), value)
+ else:
+ return None
+
+def _cueout_envivio(line, state, prevline):
+ param, value = line.split(':', 1)
+ res = re.match('.*DURATION=(.*),.*,CUE="(.*)"', value)
+ if res:
+ return (res.group(2), res.group(1))
+ else:
+ return None
+
+def _parse_cueout_start(line, state, prevline):
+ _cueout_state = _cueout_elemental(line, state, prevline) or _cueout_envivio(line, state, prevline)
+ if _cueout_state:
+ state['current_cue_out_scte35'] = _cueout_state[0]
+ state['current_cue_out_duration'] = _cueout_state[1]
+
def string_to_lines(string):
return string.strip().replace('\r\n', '\n').split('\n')
+
def remove_quotes_parser(*attrs):
return dict(zip(attrs, itertools.repeat(remove_quotes)))
+
def remove_quotes(string):
'''
Remove quotes from string.
@@ -266,8 +329,10 @@ def remove_quotes(string):
return string[1:-1]
return string
+
def normalize_attribute(attribute):
return attribute.replace('-', '_').lower().strip()
+
def is_url(uri):
return re.match(r'https?://', uri) is not None
diff --git a/m3u8/protocol.py b/m3u8/protocol.py
index bfa5992..2fb3be5 100644
--- a/m3u8/protocol.py
+++ b/m3u8/protocol.py
@@ -18,8 +18,10 @@ ext_i_frames_only = '#EXT-X-I-FRAMES-ONLY'
ext_x_byterange = '#EXT-X-BYTERANGE'
ext_x_i_frame_stream_inf = '#EXT-X-I-FRAME-STREAM-INF'
ext_x_discontinuity = '#EXT-X-DISCONTINUITY'
+ext_x_cue_out_start = '#EXT-X-CUE-OUT'
ext_x_cue_out = '#EXT-X-CUE-OUT-CONT'
ext_is_independent_segments = '#EXT-X-INDEPENDENT-SEGMENTS'
ext_x_scte35 = '#EXT-OATCLS-SCTE35'
ext_x_cue_start = '#EXT-X-CUE-OUT'
ext_x_cue_end = '#EXT-X-CUE-IN'
+ext_x_cue_span = '#EXT-X-CUE-SPAN'
diff --git a/setup.py b/setup.py
index 1315e2c..8c727c2 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,8 @@ from setuptools import setup
long_description = None
if exists("README.rst"):
- long_description = open("README.rst").read()
+ with open("README.rst") as file:
+ long_description = file.read()
install_reqs = [req for req in open(abspath(join(dirname(__file__), 'requirements.txt')))]
@@ -11,7 +12,7 @@ setup(
name="m3u8",
author='Globo.com',
author_email='videos3 at corp.globo.com',
- version="0.2.10",
+ version="0.3.0",
license='MIT',
zip_safe=False,
include_package_data=True,
diff --git a/tests/playlists.py b/tests/playlists.py
index 3c9c78d..bad5da0 100755
--- a/tests/playlists.py
+++ b/tests/playlists.py
@@ -15,7 +15,8 @@ http://media.example.com/entire.ts
#EXT-X-ENDLIST
'''
-SIMPLE_PLAYLIST_FILENAME = abspath(join(dirname(__file__), 'playlists/simple-playlist.m3u8'))
+SIMPLE_PLAYLIST_FILENAME = abspath(
+ join(dirname(__file__), 'playlists/simple-playlist.m3u8'))
SIMPLE_PLAYLIST_URI = TEST_HOST + '/simple.m3u8'
TIMEOUT_SIMPLE_PLAYLIST_URI = TEST_HOST + '/timeout_simple.m3u8'
@@ -191,6 +192,27 @@ PLAYLIST_WITH_ENCRIPTED_SEGMENTS_AND_IV = '''
../../../../hls/streamNum82405.ts
'''
+PLAYLIST_WITH_ENCRIPTED_SEGMENTS_AND_IV_SORTED = '''
+#EXTM3U
+#EXT-X-MEDIA-SEQUENCE:82400
+#EXT-X-ALLOW-CACHE:NO
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:8
+#EXT-X-KEY:METHOD=AES-128,URI="/hls-key/key.bin", IV=0X10ef8f758ca555115584bb5b3c687f52
+#EXTINF:8,
+../../../../hls/streamNum82400.ts
+#EXTINF:8,
+../../../../hls/streamNum82401.ts
+#EXTINF:8,
+../../../../hls/streamNum82402.ts
+#EXTINF:8,
+../../../../hls/streamNum82403.ts
+#EXTINF:8,
+../../../../hls/streamNum82404.ts
+#EXTINF:8,
+../../../../hls/streamNum82405.ts
... 995 lines suppressed ...
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-m3u8.git
More information about the Python-modules-commits
mailing list