Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions lib/m3u8/attribute_formatter.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'bigdecimal'

module M3u8
# Shared helpers for formatting HLS tag attributes
module AttributeFormatter
Expand All @@ -26,5 +28,20 @@ def unquoted_format(key, value)
def boolean_format(key, value)
"#{key}=#{value == true ? 'YES' : 'NO'}" unless value.nil?
end

# Format a decimal attribute, ensuring it formatted as a floating-point
# number or integer
# @param number [Float, Integer, nil] the number to format
# @return [String, nil] formatted string or nil when value is nil
def decimal_format(number)
case number
when nil
nil
when Float
BigDecimal(number).to_s('F')
else
number.to_s
end
end
end
end
10 changes: 5 additions & 5 deletions lib/m3u8/date_range_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ def formatted_attributes
quoted_format('CLASS', class_name),
%(START-DATE="#{start_date}"),
quoted_format('END-DATE', end_date),
unquoted_format('DURATION', duration),
unquoted_format('PLANNED-DURATION', planned_duration),
unquoted_format('DURATION', decimal_format(duration)),
unquoted_format('PLANNED-DURATION', decimal_format(planned_duration)),
client_attributes_format,
interstitial_formats,
unquoted_format('SCTE35-CMD', scte35_cmd),
Expand All @@ -147,7 +147,7 @@ def client_attributes_format

client_attributes.map do |attribute|
value = attribute.last
fmt = decimal?(value) ? value : %("#{value}")
fmt = decimal?(value) ? decimal_format(value) : %("#{value}")
"#{attribute.first}=#{fmt}"
end
end
Expand All @@ -166,8 +166,8 @@ def decimal?(value)
def interstitial_formats
[quoted_format('X-ASSET-URI', asset_uri),
quoted_format('X-ASSET-LIST', asset_list),
unquoted_format('X-RESUME-OFFSET', resume_offset),
unquoted_format('X-PLAYOUT-LIMIT', playout_limit),
unquoted_format('X-RESUME-OFFSET', decimal_format(resume_offset)),
unquoted_format('X-PLAYOUT-LIMIT', decimal_format(playout_limit)),
quoted_format('X-RESTRICT', restrict),
quoted_format('X-SNAP', snap),
quoted_format('X-TIMELINE-OCCUPIES', timeline_occupies),
Expand Down
2 changes: 1 addition & 1 deletion lib/m3u8/part_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def to_s
private

def formatted_attributes
[unquoted_format('DURATION', duration),
[unquoted_format('DURATION', decimal_format(duration)),
quoted_format('URI', uri),
independent_format,
quoted_format('BYTERANGE', byterange),
Expand Down
3 changes: 2 additions & 1 deletion lib/m3u8/segment_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module M3u8
# optionally allowing an EXT-X-BYTERANGE tag to be set.
class SegmentItem
include M3u8
include AttributeFormatter

# @return [Float, nil] segment duration in seconds
# @return [String, nil] segment URI
Expand Down Expand Up @@ -32,7 +33,7 @@ def self.parse(text)
# Render as an m3u8 EXTINF tag with segment URI.
# @return [String]
def to_s
"#EXTINF:#{duration},#{comment}#{byterange_format}" \
"#EXTINF:#{decimal_format(duration)},#{comment}#{byterange_format}" \
"\n#{date_format}#{segment}"
end

Expand Down
1 change: 1 addition & 0 deletions m3u8.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
spec.files = `git ls-files -z`.split("\x0")
.grep_v(/\A(CLAUDE|AGENTS)\.md\z/)
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.add_dependency 'bigdecimal'
spec.require_paths = ['lib']

end
20 changes: 20 additions & 0 deletions spec/lib/m3u8/date_range_item_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,26 @@
expect(item.to_s).to eq(expected)
end

it 'should render small float values as floating-point number instead of scientific notation' do
options = { id: 'test_id', start_date: '2014-03-05T11:15:00Z',
duration: 0.00001,
planned_duration: 0.00002,
resume_offset: 0.00003,
playout_limit: 0.00004,
client_attributes: { 'X-CUSTOM' => 0.00005 } }
item = described_class.new(options)

expected = '#EXT-X-DATERANGE:ID="test_id",' \
'START-DATE="2014-03-05T11:15:00Z",' \
'DURATION=0.00001,' \
'PLANNED-DURATION=0.00002,' \
'X-CUSTOM=0.00005,' \
'X-RESUME-OFFSET=0.00003,' \
'X-PLAYOUT-LIMIT=0.00004'

expect(item.to_s).to eq(expected)
end

it 'should ignore optional attributes' do
options = { id: 'test_id', start_date: '2014-03-05T11:15:00Z' }
item = described_class.new(options)
Expand Down
7 changes: 7 additions & 0 deletions spec/lib/m3u8/part_item_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,12 @@
expected = '#EXT-X-PART:DURATION=1.5,URI="part1.ts"'
expect(item.to_s).to eq(expected)
end

it 'should render small float values as floating-point number instead of scientific notation' do
options = { duration: 0.00001, uri: 'part1.ts' }
item = described_class.new(options)
expected = '#EXT-X-PART:DURATION=0.00001,URI="part1.ts"'
expect(item.to_s).to eq(expected)
end
end
end
11 changes: 11 additions & 0 deletions spec/lib/m3u8/segment_item_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,15 @@
'segment.aac'
expect(output).to eq expected
end

it 'converts very small durations to floating point' do
time = Time.iso8601('2020-11-25T20:27:00Z')
hash = { duration: 0.000001, segment: 'test.ts', program_date_time: time }
item = M3u8::SegmentItem.new(hash)
output = item.to_s
expected = "#EXTINF:0.000001,\n" \
"#EXT-X-PROGRAM-DATE-TIME:2020-11-25T20:27:00Z\n" \
'test.ts'
expect(output).to eq expected
end
end
Loading