Skip to content

Commit

Permalink
Rework segment.key as segment.keys, a list of keys
Browse files Browse the repository at this point in the history
This closes globocom#283 as it now follows RFC and keeps all preceding keys for the segment.

- `m3u8_obj.keys` still has the same behaviour.
- `m3u8_obj.segments[0].key` is now removed.
- `m3u8_obj.segments[0].keys` is added in it's place, which will now always be a list.
- The list will always be present, it will never be `None` anymore, and if the segment explicitly has no DRM by specifying an EXT-X-KEY of METHOD=NONE then it will be a list with that key.
- It does not check if there's a METHOD=NONE with other conflicting EXT-X-KEY information.
- It still follows the same behaviour of not duplicating the EXT-X-KEY information on future segments when dumping.

Do note that it only clears the list of Keys when it reaches an EXT-X-DISCONTINUITY, or it has reached a ts segment.

The copy() statement is used because it clears after the segment is appended to `data`, yet the .clear() to `current_keys` follows in the appended `segment` unless we copy that data in memory prior.

I don't think there's a need for `.by_key()` or `m3u8_obj.keys` anymore, but I've made sure those still work as intended in case there's a different reason those exist.
  • Loading branch information
rlaphoenix committed Mar 9, 2023
1 parent 54c830c commit e58e878
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 20 deletions.
30 changes: 15 additions & 15 deletions m3u8/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,12 @@ def _initialize_attributes(self):
self.segment_map = [InitializationSection(base_uri=self.base_uri, **params) if params else None
for params in self.data.get('segment_map', [])]
self.segments = SegmentList([
Segment(base_uri=self.base_uri, keyobject=find_key(segment.get('key', {}), self.keys), **segment)
Segment(
base_uri=self.base_uri,
keyobjects=[
find_key(segment_key, self.keys)
for segment_key in segment['keys']],
**segment)
for segment in self.data.get('segments', [])
])

Expand Down Expand Up @@ -430,8 +435,8 @@ class Segment(BasePathMixin):
`byterange`
byterange attribute from EXT-X-BYTERANGE parameter
`key`
Key used to encrypt the segment (EXT-X-KEY)
`keys`
Keys used to encrypt the segment (EXT-X-KEY)
`parts`
partial segments that make up this segment
Expand All @@ -448,9 +453,9 @@ class Segment(BasePathMixin):

def __init__(self, uri=None, base_uri=None, program_date_time=None, current_program_date_time=None,
duration=None, title=None, bitrate=None, byterange=None, cue_out=False,
cue_out_start=False, cue_in=False, discontinuity=False, key=None, scte35=None,
cue_out_start=False, cue_in=False, discontinuity=False, keys=None, scte35=None,
oatcls_scte35=None, scte35_duration=None, scte35_elapsedtime=None, asset_metadata=None,
keyobject=None, parts=None, init_section=None, dateranges=None, gap_tag=None,
keyobjects=None, parts=None, init_section=None, dateranges=None, gap_tag=None,
media_sequence=None, custom_parser_values=None):
self.media_sequence = media_sequence
self.uri = uri
Expand All @@ -470,7 +475,7 @@ def __init__(self, uri=None, base_uri=None, program_date_time=None, current_prog
self.scte35_duration = scte35_duration
self.scte35_elapsedtime = scte35_elapsedtime
self.asset_metadata = asset_metadata
self.key = keyobject
self.keys = keyobjects
self.parts = PartialSegmentList( [ PartialSegment(base_uri=self._base_uri, **partial) for partial in parts ] if parts else [] )
if init_section is not None:
self.init_section = InitializationSection(self._base_uri, **init_section)
Expand All @@ -486,14 +491,9 @@ def add_part(self, part):
def dumps(self, last_segment, timespec='milliseconds'):
output = []

if last_segment and self.key != last_segment.key:
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 not last_segment or (self.keys and self.keys != last_segment.keys):
for key in self.keys:
output.append(str(key) + '\n')

if last_segment and self.init_section != last_segment.init_section:
if not self.init_section:
Expand Down Expand Up @@ -610,7 +610,7 @@ def uri(self):


def by_key(self, key):
return [ segment for segment in self if segment.key == key ]
return [ segment for segment in self if key in segment.keys ]



Expand Down
12 changes: 7 additions & 5 deletions m3u8/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Copyright 2014 Globo.com Player authors. All rights reserved.
# Use of this source code is governed by a MIT License
# license that can be found in the LICENSE file.
from copy import copy

import iso8601
import datetime
Expand Down Expand Up @@ -62,7 +63,7 @@ def parse(content, strict=False, custom_tags_parser=None):
state = {
'expect_segment': False,
'expect_playlist': False,
'current_key': None,
'current_keys': [],
'current_segment_map': None,
}

Expand Down Expand Up @@ -95,6 +96,7 @@ def parse(content, strict=False, custom_tags_parser=None):

elif line.startswith(protocol.ext_x_discontinuity_sequence):
_parse_simple_parameter(line, data, int)
state['current_keys'].clear()

elif line.startswith(protocol.ext_x_program_date_time):
_, program_date_time = _parse_simple_parameter_raw_value(line, cast_date_time)
Expand Down Expand Up @@ -135,7 +137,7 @@ def parse(content, strict=False, custom_tags_parser=None):

elif line.startswith(protocol.ext_x_key):
key = _parse_key(line)
state['current_key'] = key
state['current_keys'].append(key)
if key not in data['keys']:
data['keys'].append(key)

Expand Down Expand Up @@ -222,6 +224,7 @@ def parse(content, strict=False, custom_tags_parser=None):
elif state['expect_segment']:
_parse_ts_chunk(line, data, state)
state['expect_segment'] = False
state['current_keys'].clear()

elif state['expect_playlist']:
_parse_variant_playlist(line, data, state)
Expand Down Expand Up @@ -280,9 +283,8 @@ def _parse_ts_chunk(line, data, state):
segment['scte35_elapsedtime'] = scte_op('current_cue_out_elapsedtime', None)
segment['asset_metadata'] = scte_op('asset_metadata', None)
segment['discontinuity'] = state.pop('discontinuity', False)
if state.get('current_key'):
segment['key'] = state['current_key']
else:
segment['keys'] = copy(state['current_keys'])
if not state['current_keys']:
# For unencrypted segments, the initial key would be None
if None not in data['keys']:
data['keys'].append(None)
Expand Down

0 comments on commit e58e878

Please sign in to comment.