Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ the playlist object used in the for loop above has a few attributes:
- ``uri``: the url to the stream
- ``stream_info``: a ``StreamInfo`` object (actually a namedtuple) with
all the attributes available to `#EXT-X-STREAM-INF`_
- ``media``: a list of related ``Media`` objects with all the attributes
available to `#EXT-X-MEDIA`_
- ``playlist_type``: the type of the playlist, which can be one of `VOD`_
(video on demand) or `EVENT`_


**NOTE: the following attributes are not implemented yet**, follow
`issue 4`_ for updates
Expand All @@ -66,7 +71,7 @@ the playlist object used in the for loop above has a few attributes:
with all the attribute available to `#EXT-X-I-FRAME-STREAM-INF`_
- ``alternative_audios``: its an empty list, unless it's a playlist
with `Alternative audio`_, in this case it's a list with ``Media``
objects with all the attributes available to `#X-EXT-MEDIA`_
objects with all the attributes available to `#EXT-X-MEDIA`_
- ``alternative_videos``: same as ``alternative_audios``

Running Tests
Expand Down Expand Up @@ -98,4 +103,6 @@ the same thing.
.. _I-Frames: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.13
.. _#EXT-X-I-FRAME-STREAM-INF: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.13
.. _Alternative audio: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-8.7
.. _#X-EXT-MEDIA: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.9
.. _#EXT-X-MEDIA: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.9
.. _VOD: https://developer.apple.com/library/mac/technotes/tn2288/_index.html#//apple_ref/doc/uid/DTS40012238-CH1-TNTAG2
.. _EVENT: https://developer.apple.com/library/mac/technotes/tn2288/_index.html#//apple_ref/doc/uid/DTS40012238-CH1-EVENT_PLAYLIST
4 changes: 2 additions & 2 deletions m3u8/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import urlparse
from urllib2 import urlopen

from m3u8.model import M3U8, Playlist
from m3u8.model import M3U8, Playlist, Media
from m3u8.parser import parse, is_url

__all__ = 'M3U8', 'Playlist', 'loads', 'load', 'parse'
__all__ = 'M3U8', 'Playlist', 'Media', 'loads', 'load', 'parse'

def loads(content):
'''
Expand Down
83 changes: 78 additions & 5 deletions m3u8/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ class M3U8(object):
If this is a variant playlist (`is_variant` is True), returns a list of
Playlist objects

`playlist_type`
A lower-case string representing the type of the playlist, which can be
one of VOD (video on demand) or EVENT.

`media`
If this is a variant playlist (`is_variant` is True), returns a list of
Media objects

`target_duration`
Returns the EXT-X-TARGETDURATION as an integer
http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.2
Expand Down Expand Up @@ -85,6 +93,7 @@ class M3U8(object):
('media_sequence', 'media_sequence'),
('version', 'version'),
('allow_cache', 'allow_cache'),
('playlist_type', 'playlist_type')
)

def __init__(self, content=None, base_path=None, base_uri=None):
Expand All @@ -109,9 +118,25 @@ def _initialize_attributes(self):
self.files.append(self.key.uri)
self.files.extend(self.segments.uri)

self.playlists = PlaylistList([ Playlist(base_uri=self.base_uri, **playlist)
self.media = []
for media in self.data.get('media', []):
self.media.append(Media(uri=media.get('uri'),
type=media.get('type'),
group_id=media.get('group_id'),
language=media.get('language'),
name=media.get('name'),
default=media.get('default'),
autoselect=media.get('autoselect'),
forced=media.get('forced'),
characteristics=media.get('characteristics')))

self.playlists = PlaylistList([ Playlist(base_uri=self.base_uri,
media=self.media,
**playlist)
for playlist in self.data.get('playlists', []) ])



def __unicode__(self):
return self.dumps()

Expand Down Expand Up @@ -145,13 +170,16 @@ def add_playlist(self, playlist):
self.is_variant = True
self.playlists.append(playlist)

def add_media(self, media):
self.media.append(media)

def dumps(self):
'''
Returns the current m3u8 as a string.
You could also use unicode(<this obj>) or str(<this obj>)
'''
output = ['#EXTM3U']
if self.media_sequence:
if self.media_sequence is not None:
output.append('#EXT-X-MEDIA-SEQUENCE:' + str(self.media_sequence))
if self.allow_cache:
output.append('#EXT-X-ALLOW-CACHE:' + self.allow_cache.upper())
Expand All @@ -161,7 +189,33 @@ def dumps(self):
output.append(str(self.key))
if self.target_duration:
output.append('#EXT-X-TARGETDURATION:' + int_or_float_to_string(self.target_duration))
if not (self.playlist_type is None or self.playlist_type == ''):
output.append(
'#EXT-X-PLAYLIST-TYPE:%s' % str(self.playlist_type).upper())
if self.is_variant:
for media in self.media:
media_out = []

if media.uri:
media_out.append('URI=' + quoted(media.uri))
if media.type:
media_out.append('TYPE=' + media.type)
if media.group_id:
media_out.append('GROUP-ID=' + quoted(media.group_id))
if media.language:
media_out.append('LANGUAGE=' + quoted(media.language))
if media.name:
media_out.append('NAME=' + quoted(media.name))
if media.default:
media_out.append('DEFAULT=' + media.default)
if media.autoselect:
media_out.append('AUTOSELECT=' + media.autoselect)
if media.forced:
media_out.append('FORCED=' + media.forced)
if media.characteristics:
media_out.append('CHARACTERISTICS=' + quoted(media.characteristics))

output.append('#EXT-X-MEDIA:' + ','.join(media_out))
output.append(str(self.playlists))

output.append(str(self.segments))
Expand Down Expand Up @@ -304,12 +358,17 @@ def __str__(self):
class Playlist(BasePathMixin):
'''
Playlist object representing a link to a variant M3U8 with a specific bitrate.
Each `stream_info` attribute has: `program_id`, `bandwidth`, `resolution` and `codecs`
`resolution` is a tuple (h, v) of integers

Attributes:

`stream_info` is a named tuple containing the attributes: `program_id`,
`bandwidth`,`resolution`, `codecs` and `resolution` which is a a tuple (w, h) of integers

`media` is a list of related Media entries.

More info: http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.10
'''
def __init__(self, uri, stream_info, base_uri):
def __init__(self, uri, stream_info, media, base_uri):
self.uri = uri
self.base_uri = base_uri

Expand All @@ -324,6 +383,13 @@ def __init__(self, uri, stream_info, base_uri):
program_id=stream_info.get('program_id'),
resolution=resolution_pair,
codecs=stream_info.get('codecs'))
self.media = []
for media_type in ('audio', 'video', 'subtitles'):
group_id = stream_info.get(media_type)
if not group_id:
continue

self.media += filter(lambda m: m.group_id == group_id, media)

def __str__(self):
stream_inf = []
Expand All @@ -336,9 +402,16 @@ def __str__(self):
stream_inf.append('RESOLUTION=' + res)
if self.stream_info.codecs:
stream_inf.append('CODECS=' + quoted(self.stream_info.codecs))

for media in self.media:
media_type = media.type.upper()
stream_inf.append('%s="%s"' % (media_type, media.group_id))

return '#EXT-X-STREAM-INF:' + ','.join(stream_inf) + '\n' + self.uri

StreamInfo = namedtuple('StreamInfo', ['bandwidth', 'program_id', 'resolution', 'codecs'])
Media = namedtuple('Media', ['uri', 'type', 'group_id', 'language', 'name',
'default', 'autoselect', 'forced', 'characteristics'])

class PlaylistList(list, GroupedBasePathMixin):

Expand Down
37 changes: 29 additions & 8 deletions m3u8/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

ext_x_targetduration = '#EXT-X-TARGETDURATION'
ext_x_media_sequence = '#EXT-X-MEDIA-SEQUENCE'
ext_x_media = '#EXT-X-MEDIA'
ext_x_playlist_type = '#EXT-X-PLAYLIST-TYPE'
ext_x_key = '#EXT-X-KEY'
ext_x_stream_inf = '#EXT-X-STREAM-INF'
ext_x_version = '#EXT-X-VERSION'
Expand All @@ -27,8 +29,10 @@ def parse(content):
data = {
'is_variant': False,
'is_endlist': False,
'playlist_type': None,
'playlists': [],
'segments': [],
'media': [],
}

state = {
Expand Down Expand Up @@ -67,6 +71,12 @@ def parse(content):
state['expect_playlist'] = True
_parse_stream_inf(line, data, state)

elif line.startswith(ext_x_media):
_parse_media(line, data, state)

elif line.startswith(ext_x_playlist_type):
_parse_simple_parameter(line, data)

elif line.startswith(ext_x_endlist):
data['is_endlist'] = True

Expand All @@ -88,19 +98,30 @@ def _parse_ts_chunk(line, data, state):
segment['uri'] = line
data['segments'].append(segment)

def _parse_stream_inf(line, data, state):
params = ATTRIBUTELISTPATTERN.split(line.replace(ext_x_stream_inf + ':', ''))[1::2]
def _parse_attribute_list(prefix, line, quoted):
params = ATTRIBUTELISTPATTERN.split(line.replace(prefix + ':', ''))[1::2]

stream_info = {}
attributes = {}
for param in params:
name, value = param.split('=', 1)
stream_info[normalize_attribute(name)] = value
name = normalize_attribute(name)

if 'codecs' in stream_info:
stream_info['codecs'] = remove_quotes(stream_info['codecs'])
if name in quoted:
value = remove_quotes(value)

attributes[name] = value

return attributes

def _parse_stream_inf(line, data, state):
data['is_variant'] = True
state['stream_info'] = stream_info
quoted = ('codecs', 'audio', 'video', 'subtitles')
state['stream_info'] = _parse_attribute_list(ext_x_stream_inf, line, quoted)

def _parse_media(line, data, state):
quoted = ('uri', 'group_id', 'language', 'name', 'characteristics')
media = _parse_attribute_list(ext_x_media, line, quoted)
data['media'].append(media)

def _parse_variant_playlist(line, data, state):
playlist = {'uri': line,
Expand Down Expand Up @@ -136,4 +157,4 @@ def normalize_attribute(attribute):
return attribute.replace('-', '_').lower().strip()

def is_url(uri):
return re.match(r'https?://', uri) is not None
return re.match(r'https?://', uri) is not None
16 changes: 16 additions & 0 deletions tests/playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@
index_0_a.m3u8?e=b471643725c47acd
'''

SIMPLE_PLAYLIST_WITH_VOD_PLAYLIST_TYPE = '''
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:180.00000,
some_video.ts
#EXT-X-ENDLIST
'''

SIMPLE_PLAYLIST_WITH_EVENT_PLAYLIST_TYPE = '''
#EXTM3U
#EXT-X-PLAYLIST-TYPE:EVENT
#EXTINF:180.00000,
some_video.ts
#EXT-X-ENDLIST
'''

RELATIVE_PLAYLIST_FILENAME = abspath(join(dirname(__file__), 'playlists/relative-playlist.m3u8'))

RELATIVE_PLAYLIST_URI = TEST_HOST + '/path/to/relative-playlist.m3u8'
Expand Down
Loading