diff --git a/m3u8/model.py b/m3u8/model.py index ed5e3c57..2e1c2a49 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -167,7 +167,13 @@ 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.get('keys', [])], + keyobject=find_key(segment.get('key', {}), self.keys), + **segment) for segment in self.data.get('segments', []) ]) @@ -430,8 +436,11 @@ class Segment(BasePathMixin): `byterange` byterange attribute from EXT-X-BYTERANGE parameter + `keys` + Keys used to encrypt the segment (list of EXT-X-KEY) + `key` - Key used to encrypt the segment (EXT-X-KEY) + Last Key within keys used to encrypt the segment (EXT-X-KEY) `parts` partial segments that make up this segment @@ -448,9 +457,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, key=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, + keyobject=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 @@ -470,6 +479,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.keys = keyobjects or [] self.key = keyobject self.parts = PartialSegmentList( [ PartialSegment(base_uri=self._base_uri, **partial) for partial in parts ] if parts else [] ) if init_section is not None: @@ -486,14 +496,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: @@ -610,7 +615,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 ] diff --git a/m3u8/parser.py b/m3u8/parser.py index 82d43071..76e2df77 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -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 @@ -62,6 +63,7 @@ def parse(content, strict=False, custom_tags_parser=None): state = { 'expect_segment': False, 'expect_playlist': False, + 'current_keys': [], 'current_key': None, 'current_segment_map': None, } @@ -95,6 +97,8 @@ 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() + state['current_key'] = None elif line.startswith(protocol.ext_x_program_date_time): _, program_date_time = _parse_simple_parameter_raw_value(line, cast_date_time) @@ -135,6 +139,7 @@ def parse(content, strict=False, custom_tags_parser=None): elif line.startswith(protocol.ext_x_key): key = _parse_key(line) + state['current_keys'].append(key) state['current_key'] = key if key not in data['keys']: data['keys'].append(key) @@ -222,6 +227,8 @@ 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() + state['current_key'] = None elif state['expect_playlist']: _parse_variant_playlist(line, data, state) @@ -280,9 +287,10 @@ 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) + segment['keys'] = copy(state['current_keys']) if state.get('current_key'): segment['key'] = state['current_key'] - else: + if not state['current_keys']: # For unencrypted segments, the initial key would be None if None not in data['keys']: data['keys'].append(None) diff --git a/tests/test_model.py b/tests/test_model.py index a5ebca60..c4eae4b3 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -373,9 +373,7 @@ def test_segment_attribute_with_multiple_keys(): segments = obj.segments assert segments[0].key.uri == '/hls-key/key.bin' - assert segments[1].key.uri == '/hls-key/key.bin' assert segments[4].key.uri == '/hls-key/key2.bin' - assert segments[5].key.uri == '/hls-key/key2.bin' def test_segment_title_dumps(): obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST_WITH_QUOTED_TITLE) diff --git a/tests/test_parser.py b/tests/test_parser.py index 24c9a15e..edd02689 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -65,10 +65,10 @@ def test_should_add_key_attribute_to_segment_from_playlist(): assert "/hls-key/key.bin" == first_segment_key['uri'] assert "AES-128" == first_segment_key['method'] assert "0X10ef8f758ca555115584bb5b3c687f52" == first_segment_key['iv'] - last_segment_key = data['segments'][-1]['key'] - assert "/hls-key/key2.bin" == last_segment_key['uri'] - assert "AES-128" == last_segment_key['method'] - assert "0Xcafe8f758ca555115584bb5b3c687f52" == last_segment_key['iv'] + second_last_segment_key = data['segments'][-2]['key'] + assert "/hls-key/key2.bin" == second_last_segment_key['uri'] + assert "AES-128" == second_last_segment_key['method'] + assert "0Xcafe8f758ca555115584bb5b3c687f52" == second_last_segment_key['iv'] def test_should_add_non_key_for_multiple_keys_unencrypted_and_encrypted(): data = m3u8.parse(playlists.PLAYLIST_WITH_MULTIPLE_KEYS_UNENCRYPTED_AND_ENCRYPTED) @@ -79,10 +79,10 @@ def test_should_add_non_key_for_multiple_keys_unencrypted_and_encrypted(): assert "/hls-key/key.bin" == third_segment_key['uri'] assert "AES-128" == third_segment_key['method'] assert "0X10ef8f758ca555115584bb5b3c687f52" == third_segment_key['iv'] - last_segment_key = data['segments'][-1]['key'] - assert "/hls-key/key2.bin" == last_segment_key['uri'] - assert "AES-128" == last_segment_key['method'] - assert "0Xcafe8f758ca555115584bb5b3c687f52" == last_segment_key['iv'] + second_last_segment_key = data['segments'][-2]['key'] + assert "/hls-key/key2.bin" == second_last_segment_key['uri'] + assert "AES-128" == second_last_segment_key['method'] + assert "0Xcafe8f758ca555115584bb5b3c687f52" == second_last_segment_key['iv'] def test_should_handle_key_method_none_and_no_uri_attr(): data = m3u8.parse(playlists.PLAYLIST_WITH_MULTIPLE_KEYS_UNENCRYPTED_AND_ENCRYPTED_NONE_AND_NO_URI_ATTR)