Skip to content

Commit

Permalink
EXTINF tag must not violate floating point spec
Browse files Browse the repository at this point in the history
Duration containing 0.000011 should not be converted to
1.1e-0 when dumping the playlist.
  • Loading branch information
mauricioabreu committed May 31, 2020
1 parent 25cf1b5 commit 200ebfd
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 15 deletions.
33 changes: 18 additions & 15 deletions m3u8/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +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.

import decimal
import os
import errno
import math
Expand Down Expand Up @@ -292,14 +292,14 @@ def dumps(self):
output.append('#EXT-X-MEDIA-SEQUENCE:' + str(self.media_sequence))
if self.discontinuity_sequence:
output.append('#EXT-X-DISCONTINUITY-SEQUENCE:{}'.format(
int_or_float_to_string(self.discontinuity_sequence)))
number_to_string(self.discontinuity_sequence)))
if self.allow_cache:
output.append('#EXT-X-ALLOW-CACHE:' + self.allow_cache.upper())
if self.version:
output.append('#EXT-X-VERSION:' + str(self.version))
if self.target_duration:
output.append('#EXT-X-TARGETDURATION:' +
int_or_float_to_string(self.target_duration))
number_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.start:
Expand Down Expand Up @@ -503,7 +503,7 @@ def dumps(self, last_segment):

if self.uri:
if self.duration is not None:
output.append('#EXTINF:%s,' % int_or_float_to_string(self.duration))
output.append('#EXTINF:%s,' % number_to_string(self.duration))
if self.title:
output.append(self.title)
output.append('\n')
Expand Down Expand Up @@ -627,7 +627,7 @@ def dumps(self, last_segment):
output.append('#EXT-X-GAP\n')

output.append('#EXT-X-PART:DURATION=%s,URI="%s"' % (
int_or_float_to_string(self.duration), self.uri
number_to_string(self.duration), self.uri
))

if self.independent:
Expand Down Expand Up @@ -1039,9 +1039,9 @@ def __init__(self, base_uri, uri, last_msn, last_part=None):
def dumps(self):
report = []
report.append('URI=' + quoted(self.uri))
report.append('LAST-MSN=' + int_or_float_to_string(self.last_msn))
report.append('LAST-MSN=' + number_to_string(self.last_msn))
if self.last_part is not None:
report.append('LAST-PART=' + int_or_float_to_string(
report.append('LAST-PART=' + number_to_string(
self.last_part))

return ('#EXT-X-RENDITION-REPORT:' + ','.join(report))
Expand Down Expand Up @@ -1074,7 +1074,7 @@ def dumps(self):
if self[attr]:
ctrl.append('%s=%s' % (
denormalize_attribute(attr),
int_or_float_to_string(self[attr])
number_to_string(self[attr])
))

return '#EXT-X-SERVER-CONTROL:' + ','.join(ctrl)
Expand All @@ -1087,7 +1087,7 @@ def __init__(self, skipped_segments=None):
self.skipped_segments = skipped_segments

def dumps(self):
return '#EXT-X-SKIP:SKIPPED-SEGMENTS=%s' % int_or_float_to_string(
return '#EXT-X-SKIP:SKIPPED-SEGMENTS=%s' % number_to_string(
self.skipped_segments)

def __str__(self):
Expand All @@ -1098,7 +1098,7 @@ def __init__(self, part_target=None):
self.part_target = part_target

def dumps(self):
return '#EXT-X-PART-INF:PART-TARGET=%s' % int_or_float_to_string(
return '#EXT-X-PART-INF:PART-TARGET=%s' % number_to_string(
self.part_target)

def __str__(self):
Expand All @@ -1124,7 +1124,7 @@ def dumps(self):
if self[attr] is not None:
hint.append('%s=%s' % (
denormalize_attribute(attr),
int_or_float_to_string(self[attr])
number_to_string(self[attr])
))

return ('#EXT-X-PRELOAD-HINT:' + ','.join(hint))
Expand Down Expand Up @@ -1187,9 +1187,9 @@ def dumps(self):
if (self.end_date):
daterange.append('END-DATE=' + quoted(self.end_date))
if (self.duration):
daterange.append('DURATION=' + int_or_float_to_string(self.duration))
daterange.append('DURATION=' + number_to_string(self.duration))
if (self.planned_duration):
daterange.append('PLANNED-DURATION=' + int_or_float_to_string(self.planned_duration))
daterange.append('PLANNED-DURATION=' + number_to_string(self.planned_duration))
if (self.scte35_cmd):
daterange.append('SCTE35-CMD=' + self.scte35_cmd)
if (self.scte35_out):
Expand Down Expand Up @@ -1231,5 +1231,8 @@ def quoted(string):
return '"%s"' % string


def int_or_float_to_string(number):
return str(int(number)) if number == math.floor(number) else str(number)
def number_to_string(number):
with decimal.localcontext() as ctx:
ctx.prec = 20 # set floating point precision
d = decimal.Decimal(str(number))
return str(d.quantize(decimal.Decimal(1)) if d == d.to_integral_value() else d.normalize())
12 changes: 12 additions & 0 deletions tests/playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@
#EXT-X-ENDLIST
'''

SIMPLE_PLAYLIST_WITH_VERY_SHORT_DURATION = '''
#EXTM3U
#EXT-X-TARGETDURATION:5220
#EXTINF:5220,
http://media.example.com/entire1.ts
#EXTINF:5218.5,
http://media.example.com/entire2.ts
#EXTINF:0.000011,
http://media.example.com/entire3.ts
#EXT-X-ENDLIST
'''

SIMPLE_PLAYLIST_WITH_START_NEGATIVE_OFFSET = '''
#EXTM3U
#EXT-X-TARGETDURATION:5220
Expand Down
8 changes: 8 additions & 0 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,14 @@ def test_dump_should_not_ignore_zero_duration():
assert "EXTINF:5220" in obj.dumps().strip()


def test_dump_should_use_decimal_floating_point_for_very_short_durations():
obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST_WITH_VERY_SHORT_DURATION)

assert "EXTINF:5220" in obj.dumps().strip()
assert "EXTINF:5218.5" in obj.dumps().strip()
assert "EXTINF:0.000011" in obj.dumps().strip()


def test_dump_should_include_segment_level_program_date_time():
obj = m3u8.M3U8(playlists.DISCONTINUITY_PLAYLIST_WITH_PROGRAM_DATE_TIME)
# Tag being expected is in the segment level, not the global one
Expand Down

0 comments on commit 200ebfd

Please sign in to comment.