Skip to content

Commit

Permalink
EXT-X-GAP support
Browse files Browse the repository at this point in the history
  • Loading branch information
davemevans authored Apr 22, 2020
1 parent 87c19e4 commit b071688
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 3 deletions.
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Supported tags
* #EXT-X-SKIP
* `#EXT-X-SESSION-DATA`_
* `#EXT-X-DATERANGE`_
* `#EXT-X-GAP`_

Encryption keys
---------------
Expand Down Expand Up @@ -254,6 +255,7 @@ the same thing.
.. _#EXT-X-INDEPENDENT-SEGMENTS: https://tools.ietf.org/html/rfc8216#section-4.3.5.1
.. _#EXT-X-START: https://tools.ietf.org/html/rfc8216#section-4.3.5.2
.. _#EXT-X-DATERANGE: https://tools.ietf.org/html/rfc8216#section-4.3.2.7
.. _#EXT-X-GAP: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.2.7
.. _issue 1: https://github.com/globocom/m3u8/issues/1
.. _variant streams: https://tools.ietf.org/html/rfc8216#section-6.2.4
.. _example here: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-8.5
Expand Down
23 changes: 20 additions & 3 deletions m3u8/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,12 +419,15 @@ class Segment(BasePathMixin):
`dateranges`
any dateranges that should preceed the segment
`gap_tag`
GAP tag indicates that a Media Segment is missing
'''

def __init__(self, uri=None, base_uri=None, program_date_time=None, current_program_date_time=None,
duration=None, title=None, byterange=None, cue_out=False, cue_out_start=False,
cue_in=False, discontinuity=False, key=None, scte35=None, scte35_duration=None,
keyobject=None, parts=None, init_section=None, dateranges=None):
keyobject=None, parts=None, init_section=None, dateranges=None, gap_tag=None):
self.uri = uri
self.duration = duration
self.title = title
Expand All @@ -445,6 +448,7 @@ def __init__(self, uri=None, base_uri=None, program_date_time=None, current_prog
else:
self.init_section = None
self.dateranges = DateRangeList( [ DateRange(**daterange) for daterange in dateranges ] if dateranges else [] )
self.gap_tag = gap_tag

# Key(base_uri=base_uri, **key) if key else None

Expand Down Expand Up @@ -507,6 +511,9 @@ def dumps(self, last_segment):
if self.byterange:
output.append('#EXT-X-BYTERANGE:%s\n' % self.byterange)

if self.gap_tag:
output.append('#EXT-X-GAP\n')

output.append(self.uri)

return ''.join(output)
Expand Down Expand Up @@ -583,15 +590,21 @@ class PartialSegment(BasePathMixin):
the Partial Segment contains an independent frame
`gap`
the Partial Segment is not available
GAP attribute indicates the Partial Segment is not available
`dateranges`
any dateranges that should preceed the partial segment
`gap_tag`
GAP tag indicates one or more of the parent Media Segment's Partial
Segments have a GAP=YES attribute. This tag should appear immediately
after the first EXT-X-PART tag in the Parent Segment with a GAP=YES
attribute.
'''

def __init__(self, base_uri, uri, duration, program_date_time=None,
current_program_date_time=None, byterange=None,
independent=None, gap=None, dateranges=None):
independent=None, gap=None, dateranges=None, gap_tag=None):
self.base_uri = base_uri
self.uri = uri
self.duration = duration
Expand All @@ -601,6 +614,7 @@ def __init__(self, base_uri, uri, duration, program_date_time=None,
self.independent = independent
self.gap = gap
self.dateranges = DateRangeList( [ DateRange(**daterange) for daterange in dateranges ] if dateranges else [] )
self.gap_tag = gap_tag

def dumps(self, last_segment):
output = []
Expand All @@ -609,6 +623,9 @@ def dumps(self, last_segment):
output.append(str(self.dateranges))
output.append('\n')

if self.gap_tag:
output.append('#EXT-X-GAP\n')

output.append('#EXT-X-PART:DURATION=%s,URI="%s"' % (
int_or_float_to_string(self.duration), self.uri
))
Expand Down
5 changes: 5 additions & 0 deletions m3u8/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ def parse(content, strict=False, custom_tags_parser=None):
elif line.startswith(protocol.ext_x_daterange):
_parse_daterange(line, data, state)

elif line.startswith(protocol.ext_x_gap):
state['gap'] = True

# Comments and whitespace
elif line.startswith('#'):
if callable(custom_tags_parser):
Expand Down Expand Up @@ -264,6 +267,7 @@ def _parse_ts_chunk(line, data, state):
if state.get('current_segment_map'):
segment['init_section'] = state['current_segment_map']
segment['dateranges'] = state.pop('dateranges', None)
segment['gap_tag'] = state.pop('gap', None)
data['segments'].append(segment)


Expand Down Expand Up @@ -434,6 +438,7 @@ def _parse_part(line, data, state):
state['current_program_date_time'] += datetime.timedelta(seconds=part['duration'])

part['dateranges'] = state.pop('dateranges', None)
part['gap_tag'] = state.pop('gap', None)

if 'segment' not in state:
state['segment'] = {}
Expand Down
1 change: 1 addition & 0 deletions m3u8/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@
ext_x_session_key = '#EXT-X-SESSION-KEY'
ext_x_preload_hint = '#EXT-X-PRELOAD-HINT'
ext_x_daterange = "#EXT-X-DATERANGE"
ext_x_gap = "#EXT-X-GAP"
25 changes: 25 additions & 0 deletions tests/playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -988,4 +988,29 @@
#EXT-X-PART:DURATION=1,URI="filePart271.c.ts"
'''

GAP_PLAYLIST = '''
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:14
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:10
#EXTINF:9.84317,
fileSequence14.ts
#EXTINF:8.75875,
#EXT-X-GAP
missing-Sequence15.ts
#EXTINF:9.88487,
#EXT-X-GAP
missing-Sequence16.ts
#EXTINF:9.09242,
fileSequence17.ts
'''

GAP_IN_PARTS_PLAYLIST = '''
#EXTM3U
#EXT-X-PART:DURATION=1,URI="filePart271.a.ts"
#EXT-X-PART:DURATION=1,URI="filePart271.b.ts",GAP=YES
#EXT-X-GAP
#EXT-X-PART:DURATION=1,URI="filePart271.c.ts"
'''

del abspath, dirname, join
28 changes: 28 additions & 0 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1287,6 +1287,34 @@ def test_daterange_in_parts():

assert expected in result

def test_add_gap():
obj = m3u8.Segment(
uri='fileSequence271.ts',
duration=4,
gap_tag=True
)

result = str(obj)
expected = '#EXTINF:4,\n#EXT-X-GAP\nfileSequence271.ts'

assert result == expected

def test_gap():
obj = m3u8.M3U8(playlists.GAP_PLAYLIST)

result = obj.dumps().strip()
expected = playlists.GAP_PLAYLIST.strip()

assert result == expected

def test_gap_in_parts():
obj = m3u8.M3U8(playlists.GAP_IN_PARTS_PLAYLIST)

result = obj.dumps().strip()
expected = playlists.GAP_IN_PARTS_PLAYLIST.strip()

assert result == expected

# custom asserts


Expand Down
20 changes: 20 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,3 +454,23 @@ def test_date_range_in_parts():
assert data['segments'][0]['parts'][2]['dateranges'][0]['start_date'] == '2020-03-10T07:48:02Z'
assert data['segments'][0]['parts'][2]['dateranges'][0]['class'] == 'test_class'
assert data['segments'][0]['parts'][2]['dateranges'][0]['end_on_next'] == 'YES'

def test_gap():
data = m3u8.parse(playlists.GAP_PLAYLIST)

assert data['segments'][0]['gap_tag'] is None
assert data['segments'][1]['gap_tag'] == True
assert data['segments'][2]['gap_tag'] == True
assert data['segments'][3]['gap_tag'] is None

def test_gap_in_parts():
data = m3u8.parse(playlists.GAP_IN_PARTS_PLAYLIST)

print(data['segments'][0]['parts'])

assert data['segments'][0]['parts'][0]['gap_tag'] is None
assert data['segments'][0]['parts'][0].get('gap', None) is None
assert data['segments'][0]['parts'][1]['gap_tag'] is None
assert data['segments'][0]['parts'][1]['gap'] == 'YES'
assert data['segments'][0]['parts'][2]['gap_tag'] == True
assert data['segments'][0]['parts'][2].get('gap', None) is None

0 comments on commit b071688

Please sign in to comment.