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
6 changes: 5 additions & 1 deletion lib/m3u8.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

# M3u8 provides parsing, generation, and validation of m3u8 playlists
module M3u8
def intialize_with_byterange(params = {})
def initialize_with_byterange(params = {})
params.each do |key, value|
value = ByteRange.new(value) if value.is_a?(Hash)
instance_variable_set("@#{key}", value)
Expand All @@ -22,6 +22,10 @@ def parse_float(value)
value&.to_f
end

def parse_int(value)
value&.to_i
end

def parse_yes_no(value)
value == 'YES'
end
Expand Down
18 changes: 18 additions & 0 deletions lib/m3u8/attribute_formatter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module M3u8
# Shared helpers for formatting HLS tag attributes
module AttributeFormatter
def quoted_format(key, value)
%(#{key}="#{value}") unless value.nil?
end

def unquoted_format(key, value)
"#{key}=#{value}" unless value.nil?
end

def boolean_format(key, value)
"#{key}=#{value == true ? 'YES' : 'NO'}" unless value.nil?
end
end
end
82 changes: 82 additions & 0 deletions lib/m3u8/codecs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# frozen_string_literal: true

module M3u8
# Codec lookup tables for HLS playlist items
module Codecs
AUDIO_CODECS = {
'aac-lc' => 'mp4a.40.2',
'he-aac' => 'mp4a.40.5',
'mp3' => 'mp4a.40.34',
'ac-3' => 'ac-3',
'ec-3' => 'ec-3',
'e-ac-3' => 'ec-3',
'flac' => 'fLaC',
'opus' => 'Opus'
}.freeze

BASELINE_CODECS = {
3.0 => 'avc1.66.30',
3.1 => 'avc1.42001f'
}.freeze

MAIN_CODECS = {
3.0 => 'avc1.77.30',
3.1 => 'avc1.4d001f',
4.0 => 'avc1.4d0028',
4.1 => 'avc1.4d0029'
}.freeze

HIGH_LEVELS = [3.0, 3.1, 3.2, 4.0, 4.1, 4.2,
5.0, 5.1, 5.2].freeze

HEVC_CODECS = {
['hevc-main', 3.1] => 'hvc1.1.6.L93.B0',
['hevc-main', 4.0] => 'hvc1.1.6.L120.B0',
['hevc-main', 5.0] => 'hvc1.1.6.L150.B0',
['hevc-main', 5.1] => 'hvc1.1.6.L153.B0',
['hevc-main-10', 3.1] => 'hvc1.2.4.L93.B0',
['hevc-main-10', 4.0] => 'hvc1.2.4.L120.B0',
['hevc-main-10', 5.0] => 'hvc1.2.4.L150.B0',
['hevc-main-10', 5.1] => 'hvc1.2.4.L153.B0'
}.freeze

AV1_CODECS = {
['av1-main', 3.1] => 'av01.0.04M.08',
['av1-main', 4.0] => 'av01.0.08M.08',
['av1-main', 5.0] => 'av01.0.12M.08',
['av1-main', 5.1] => 'av01.0.13M.08',
['av1-high', 3.1] => 'av01.1.04H.10',
['av1-high', 4.0] => 'av01.1.08H.10',
['av1-high', 5.0] => 'av01.1.12H.10',
['av1-high', 5.1] => 'av01.1.13H.10'
}.freeze

def self.audio_codec(codec)
return if codec.nil?

AUDIO_CODECS[codec.downcase]
end

def self.video_codec(profile, level)
return if profile.nil? || level.nil?

level = level.to_f
name = profile.downcase
return BASELINE_CODECS[level] if name == 'baseline'
return MAIN_CODECS[level] if name == 'main'
return high_codec_string(level) if name == 'high'
return HEVC_CODECS[[profile, level]] if name.start_with?('hevc-')

AV1_CODECS[[profile, level]] if name.start_with?('av1-')
end

def self.high_codec_string(level)
return nil unless HIGH_LEVELS.include?(level)

hex = level.to_s.sub('.', '').to_i.to_s(16)
"avc1.6400#{hex}"
end

private_class_method :high_codec_string
end
end
15 changes: 3 additions & 12 deletions lib/m3u8/content_steering_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module M3u8
# indicates a Content Steering Manifest for dynamic pathway selection.
class ContentSteeringItem
extend M3u8
include AttributeFormatter

attr_accessor :server_uri, :pathway_id

Expand All @@ -29,18 +30,8 @@ def to_s
private

def formatted_attributes
[server_uri_format,
pathway_id_format].compact.join(',')
end

def server_uri_format
%(SERVER-URI="#{server_uri}")
end

def pathway_id_format
return if pathway_id.nil?

%(PATHWAY-ID="#{pathway_id}")
[quoted_format('SERVER-URI', server_uri),
quoted_format('PATHWAY-ID', pathway_id)].compact.join(',')
end
end
end
111 changes: 20 additions & 91 deletions lib/m3u8/date_range_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module M3u8
# DateRangeItem represents a #EXT-X-DATERANGE tag
class DateRangeItem
include M3u8
include AttributeFormatter

attr_accessor :id, :class_name, :start_date, :end_date, :duration,
:planned_duration, :scte35_cmd, :scte35_out, :scte35_in,
Expand Down Expand Up @@ -62,43 +63,27 @@ def scte35_in_info

def formatted_attributes
[%(ID="#{id}"),
class_name_format,
quoted_format('CLASS', class_name),
%(START-DATE="#{start_date}"),
end_date_format,
duration_format,
planned_duration_format,
quoted_format('END-DATE', end_date),
unquoted_format('DURATION', duration),
unquoted_format('PLANNED-DURATION', planned_duration),
client_attributes_format,
interstitial_formats,
scte35_cmd_format,
scte35_out_format,
scte35_in_format,
cue_format,
unquoted_format('SCTE35-CMD', scte35_cmd),
unquoted_format('SCTE35-OUT', scte35_out),
unquoted_format('SCTE35-IN', scte35_in),
quoted_format('CUE', cue),
end_on_next_format].flatten.compact.join(',')
end

def class_name_format
quoted_format('CLASS', class_name)
end

def end_date_format
quoted_format('END-DATE', end_date)
end

def duration_format
unquoted_format('DURATION', duration)
end

def planned_duration_format
unquoted_format('PLANNED-DURATION', planned_duration)
end

def client_attributes_format
return if client_attributes.nil? || client_attributes.empty?

client_attributes.map do |attribute|
value = attribute.last
value_format = decimal?(value) ? value : %("#{value}")
"#{attribute.first}=#{value_format}"
fmt = decimal?(value) ? value : %("#{value}")
"#{attribute.first}=#{fmt}"
end
end

Expand Down Expand Up @@ -126,63 +111,15 @@ def parse_interstitials(attributes)
end

def interstitial_formats
[asset_uri_format, asset_list_format,
resume_offset_format, playout_limit_format,
restrict_format, snap_format,
timeline_occupies_format, timeline_style_format,
content_may_vary_format]
end

def asset_uri_format
quoted_format('X-ASSET-URI', asset_uri)
end

def asset_list_format
quoted_format('X-ASSET-LIST', asset_list)
end

def resume_offset_format
unquoted_format('X-RESUME-OFFSET', resume_offset)
end

def playout_limit_format
unquoted_format('X-PLAYOUT-LIMIT', playout_limit)
end

def restrict_format
quoted_format('X-RESTRICT', restrict)
end

def snap_format
quoted_format('X-SNAP', snap)
end

def timeline_occupies_format
quoted_format('X-TIMELINE-OCCUPIES', timeline_occupies)
end

def timeline_style_format
quoted_format('X-TIMELINE-STYLE', timeline_style)
end

def content_may_vary_format
quoted_format('X-CONTENT-MAY-VARY', content_may_vary)
end

def scte35_cmd_format
unquoted_format('SCTE35-CMD', scte35_cmd)
end

def scte35_out_format
unquoted_format('SCTE35-OUT', scte35_out)
end

def scte35_in_format
unquoted_format('SCTE35-IN', scte35_in)
end

def cue_format
quoted_format('CUE', cue)
[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),
quoted_format('X-RESTRICT', restrict),
quoted_format('X-SNAP', snap),
quoted_format('X-TIMELINE-OCCUPIES', timeline_occupies),
quoted_format('X-TIMELINE-STYLE', timeline_style),
quoted_format('X-CONTENT-MAY-VARY', content_may_vary)]
end

def end_on_next_format
Expand All @@ -191,14 +128,6 @@ def end_on_next_format
'END-ON-NEXT=YES'
end

def quoted_format(key, value)
%(#{key}="#{value}") unless value.nil?
end

def unquoted_format(key, value)
"#{key}=#{value}" unless value.nil?
end

def parse_client_attributes(attributes)
attributes.select do |key|
key.start_with?('X-') && !INTERSTITIAL_KEYS.include?(key)
Expand Down
37 changes: 8 additions & 29 deletions lib/m3u8/encryptable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
module M3u8
# Encapsulates logic common to encryption key tags
module Encryptable
include AttributeFormatter

def self.included(base)
base.send :attr_accessor, :method
base.send :attr_accessor, :uri
Expand All @@ -12,41 +14,18 @@ def self.included(base)
end

def attributes_to_s
[method_format,
uri_format,
iv_format,
key_format_format,
key_format_versions_format].compact.join(',')
[unquoted_format('METHOD', method),
quoted_format('URI', uri),
unquoted_format('IV', iv),
quoted_format('KEYFORMAT', key_format),
quoted_format('KEYFORMATVERSIONS',
key_format_versions)].compact.join(',')
end

def convert_key_names(attributes)
{ method: attributes['METHOD'], uri: attributes['URI'],
iv: attributes['IV'], key_format: attributes['KEYFORMAT'],
key_format_versions: attributes['KEYFORMATVERSIONS'] }
end

private

def method_format
"METHOD=#{method}"
end

def uri_format
%(URI="#{uri}") unless uri.nil?
end

def iv_format
"IV=#{iv}" unless iv.nil?
end

def key_format_format
%(KEYFORMAT="#{key_format}") unless key_format.nil?
end

def key_format_versions_format
return if key_format_versions.nil?

%(KEYFORMATVERSIONS="#{key_format_versions}")
end
end
end
2 changes: 1 addition & 1 deletion lib/m3u8/map_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class MapItem
attr_accessor :uri, :byterange

def initialize(params = {})
intialize_with_byterange(params)
initialize_with_byterange(params)
end

def self.parse(text)
Expand Down
Loading