Skip to content

Commit

Permalink
Merge pull request #47 from neon-lab/master
Browse files Browse the repository at this point in the history
Added support for #EXT-X-CUE-OUT-CONT tags as attributes of Segment
  • Loading branch information
leandromoreira committed Jul 15, 2015
2 parents 1bdec40 + cb199fa commit c6a10cd
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 79 deletions.
9 changes: 7 additions & 2 deletions m3u8/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ class Segment(BasePathMixin):
Returns a boolean indicating if a EXT-X-DISCONTINUITY tag exists
http://tools.ietf.org/html/draft-pantos-http-live-streaming-13#section-3.4.11
`cue_out`
Returns a boolean indicating if a EXT-X-CUE-OUT-CONT tag exists
`duration`
duration attribute from EXTINF parameter
Expand All @@ -329,14 +332,15 @@ class Segment(BasePathMixin):
'''

def __init__(self, uri, base_uri, program_date_time=None, duration=None,
title=None, byterange=None, discontinuity=False, key=None):
title=None, byterange=None, cue_out=False, discontinuity=False, key=None):
self.uri = uri
self.duration = duration
self.title = title
self.base_uri = base_uri
self.byterange = byterange
self.program_date_time = program_date_time
self.discontinuity = discontinuity
self.cue_out = cue_out
self.key = Key(base_uri=base_uri,**key) if key else None


Expand All @@ -349,7 +353,8 @@ def dumps(self, last_segment):
if self.discontinuity:
output.append('#EXT-X-DISCONTINUITY\n')
output.append('#EXT-X-PROGRAM-DATE-TIME:%s\n' % parser.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))
if self.title:
output.append(quoted(self.title))
Expand Down
3 changes: 3 additions & 0 deletions m3u8/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ def parse(content):
state['current_program_date_time'] = program_date_time
elif line.startswith(protocol.ext_x_discontinuity):
state['discontinuity'] = True
elif line.startswith(protocol.ext_x_cue_out):
state['cue_out'] = True
elif line.startswith(protocol.ext_x_version):
_parse_simple_parameter(line, data)
elif line.startswith(protocol.ext_x_allow_cache):
Expand Down Expand Up @@ -120,6 +122,7 @@ def _parse_ts_chunk(line, data, state):
segment['program_date_time'] = state['current_program_date_time']
state['current_program_date_time'] += datetime.timedelta(seconds=segment['duration'])
segment['uri'] = line
segment['cue_out'] = state.pop('cue_out', False)
segment['discontinuity'] = state.pop('discontinuity', False)
if state.get('current_key'):
segment['key'] = state['current_key']
Expand Down
1 change: 1 addition & 0 deletions m3u8/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
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 = '#EXT-X-CUE-OUT-CONT'
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
name="m3u8",
author='Globo.com',
author_email='videos3@corp.globo.com',
version="0.2.4",
version="0.2.5",
zip_safe=False,
include_package_data=True,
install_requires=install_reqs,
Expand Down
66 changes: 34 additions & 32 deletions tests/playlists.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@
PLAYLIST_WITH_ENCRIPTED_SEGMENTS = '''
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:7794
#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52",KEYFORMAT="identity",KEYFORMATVERSIONS="1/2/5"
#EXT-X-TARGETDURATION:15
#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"
#EXTINF:15,
http://media.example.com/fileSequence52-1.ts
#EXTINF:15,
Expand Down Expand Up @@ -101,39 +103,9 @@
video-64k.m3u8
'''

VARIANT_PLAYLIST_WITH_MEDIA = '''
#EXTM3U
#EXT-X-MEDIA:URI="captions.m3u8",TYPE=SUBTITLES,GROUP-ID="subs",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=800000,RESOLUTION=624x352,CODECS="avc1.4d001f, mp4a.40.5",SUBTITLES="subs"
video-800k.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1200000,CODECS="avc1.4d001f, mp4a.40.5",SUBTITLES="subs"
video-1200k.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=400000,CODECS="avc1.4d001f, mp4a.40.5",SUBTITLES="subs"
video-400k.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=150000,CODECS="avc1.4d001f, mp4a.40.5",SUBTITLES="subs"
video-150k.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=64000,CODECS="mp4a.40.5",SUBTITLES="subs"
video-64k.m3u8
'''

VARIANT_PLAYLIST_WITH_CLOSED_CAPTIONS = '''
#EXTM3U
#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc",INSTREAM-ID=SERVICE43,LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=800000,RESOLUTION=624x352,CODECS="avc1.4d001f, mp4a.40.5",CLOSED-CAPTIONS="cc"
video-800k.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1200000,CODECS="avc1.4d001f, mp4a.40.5",CLOSED-CAPTIONS="cc"
video-1200k.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=400000,CODECS="avc1.4d001f, mp4a.40.5",CLOSED-CAPTIONS="cc"
video-400k.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=150000,CODECS="avc1.4d001f, mp4a.40.5",CLOSED-CAPTIONS="cc"
video-150k.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=64000,CODECS="mp4a.40.5",CLOSED-CAPTIONS="cc"
video-64k.m3u8
'''


IFRAME_PLAYLIST = '''
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-VERSION:4
#EXT-X-TARGETDURATION:10
#EXT-X-PLAYLIST-TYPE:VOD
Expand All @@ -152,6 +124,7 @@

PLAYLIST_USING_BYTERANGES = '''
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-VERSION:4
#EXT-X-TARGETDURATION:11
#EXTINF:10,
Expand Down Expand Up @@ -291,8 +264,37 @@
'''

CUE_OUT_PLAYLIST = '''
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:143474331
#EXT-X-VERSION:3
#EXTINF:10,
#EXT-X-PROGRAM-DATE-TIME:2015-06-18T23:22:10Z
1432451707508/ts/71737/sequence143474338.ts
#EXT-X-CUE-OUT-CONT:CAID=0x000000002310E3A8,ElapsedTime=161,Duration=181
#EXTINF:10,
#EXT-X-PROGRAM-DATE-TIME:2015-06-18T23:22:20Z
1432451707508/ts/71737/sequence143474339.ts
#EXT-X-CUE-OUT-CONT:CAID=0x000000002310E3A8,ElapsedTime=171,Duration=181
#EXTINF:10,
#EXT-X-PROGRAM-DATE-TIME:2015-06-18T23:22:30Z
1432451707508/ts/71737/sequence143474340.ts
#EXT-OATCLS-SCTE35:/DA5AAAAAAAA/wCABQb+aDhDgAAjAhdDVUVJQAAAV3+fCAgAAAAAIxDjqDUCAAAIQ1VFSQAAAABSV+PX
#EXT-X-CUE-IN
#EXTINF:10,
#EXT-X-PROGRAM-DATE-TIME:2015-06-18T23:22:40Z
1432451707508/ts/71737/sequence143474341.ts
'''


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

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

CUE_OUT_PLAYLIST_FILENAME = abspath(join(dirname(__file__), 'playlists/cue_out.m3u8'))

CUE_OUT_PLAYLIST_URI = TEST_HOST + '/path/to/cue_out.m3u8'

del abspath, dirname, join
59 changes: 15 additions & 44 deletions tests/test_model.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
#Tests M3U8 class to make sure all attributes and methods use the correct
#data returned from parser.parse()

import arrow
import datetime
import m3u8
import playlists
from m3u8.model import Segment
from m3u8.parser import cast_date_time

def test_target_duration_attribute():
obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST)
Expand All @@ -24,28 +24,23 @@ def test_media_sequence_attribute():

assert '1234567' == obj.media_sequence

def test_implicit_media_sequence_value():
obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST)

assert 0 == obj.media_sequence

def test_program_date_time_attribute():
obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST_WITH_PROGRAM_DATE_TIME)

assert cast_date_time('2014-08-13T13:36:33+00:00') == obj.program_date_time
assert arrow.get('2014-08-13T13:36:33+00:00').datetime == obj.program_date_time

def test_program_date_time_attribute_for_each_segment():
obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST_WITH_PROGRAM_DATE_TIME)

first_program_date_time = cast_date_time('2014-08-13T13:36:33+00:00')
first_program_date_time = arrow.get('2014-08-13T13:36:33+00:00').datetime
for idx, segment in enumerate(obj.segments):
assert segment.program_date_time == first_program_date_time + datetime.timedelta(seconds=idx * 3)

def test_program_date_time_attribute_with_discontinuity():
obj = m3u8.M3U8(playlists.DISCONTINUITY_PLAYLIST_WITH_PROGRAM_DATE_TIME)

first_program_date_time = cast_date_time('2014-08-13T13:36:33+00:00')
discontinuity_program_date_time = cast_date_time('2014-08-13T13:36:55+00:00')
first_program_date_time = arrow.get('2014-08-13T13:36:33+00:00').datetime
discontinuity_program_date_time = arrow.get('2014-08-13T13:36:55+00:00').datetime

segments = obj.segments

Expand All @@ -62,6 +57,14 @@ def test_segment_discontinuity_attribute():
assert segments[5].discontinuity == True
assert segments[6].discontinuity == False

def test_segment_cue_out_attribute():
obj = m3u8.M3U8(playlists.CUE_OUT_PLAYLIST)
segments = obj.segments

assert segments[0].cue_out == True
assert segments[1].cue_out == True
assert segments[2].cue_out == True

def test_key_attribute():
obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST)
data = {'key': {'method': 'AES-128',
Expand Down Expand Up @@ -318,11 +321,7 @@ def test_no_playlist_type_leaves_attribute_empty():
# dump m3u8

def test_dumps_should_build_same_string():
playlists_model = [
playlists.PLAYLIST_WITH_NON_INTEGER_DURATION,
playlists.PLAYLIST_WITH_ENCRIPTED_SEGMENTS,
playlists.PLAYLIST_WITH_ENCRIPTED_SEGMENTS_AND_IV,
]
playlists_model = [playlists.PLAYLIST_WITH_NON_INTEGER_DURATION, playlists.PLAYLIST_WITH_ENCRIPTED_SEGMENTS_AND_IV]
for playlist in playlists_model:
obj = m3u8.M3U8(playlist)
expected = playlist.replace(', IV', ',IV').strip()
Expand Down Expand Up @@ -369,13 +368,6 @@ def test_dump_should_work_for_variant_playlists_with_iframe_playlists():

assert expected == obj.dumps().strip()

def test_dump_should_work_for_variant_playlists_with_media():
obj = m3u8.M3U8(playlists.VARIANT_PLAYLIST_WITH_MEDIA)

expected = playlists.VARIANT_PLAYLIST_WITH_MEDIA.strip()

assert expected == obj.dumps().strip()

def test_dump_should_work_for_iframe_playlists():
obj = m3u8.M3U8(playlists.IFRAME_PLAYLIST)

Expand Down Expand Up @@ -499,7 +491,7 @@ def test_0_media_sequence_added_to_file():
obj = m3u8.M3U8()
obj.media_sequence = 0
result = obj.dumps()
expected = '#EXTM3U\n'
expected = '#EXTM3U\n#EXT-X-MEDIA-SEQUENCE:0\n'
assert result == expected

def test_none_media_sequence_gracefully_ignored():
Expand Down Expand Up @@ -535,27 +527,6 @@ def test_m3u8_should_propagate_base_uri_to_key():
assert '../key.bin' == obj.key.uri
assert '/any/key.bin' == obj.key.absolute_uri

def test_m3u8_should_propagate_base_uri_to_media():
content = playlists.VARIANT_PLAYLIST_WITH_MEDIA
obj = m3u8.M3U8(content, base_uri='/any/path/')
assert 'captions.m3u8' == obj.media[0].uri
assert '/any/path/captions.m3u8' == obj.media[0].absolute_uri
obj.base_uri = '/any/where/'
assert 'captions.m3u8' == obj.media[0].uri
assert '/any/where/captions.m3u8' == obj.media[0].absolute_uri

def test_m3u8_should_not_fail_on_closed_captions():
content = playlists.VARIANT_PLAYLIST_WITH_CLOSED_CAPTIONS
obj = m3u8.M3U8(content, base_uri='/any/path/')
assert 'video-800k.m3u8' == obj.playlists[0].uri
assert '/any/path/video-800k.m3u8' == obj.playlists[0].absolute_uri
obj.base_uri = '/any/where/'
assert 'video-800k.m3u8' == obj.playlists[0].uri
assert '/any/where/video-800k.m3u8' == obj.playlists[0].absolute_uri
assert obj.media[0].uri is None
assert obj.media[0].absolute_uri is None
assert obj.media[0].type == 'CLOSED-CAPTIONS'


# custom asserts

Expand Down

0 comments on commit c6a10cd

Please sign in to comment.