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
19 changes: 19 additions & 0 deletions lib/m3u8.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,49 @@

# M3u8 provides parsing, generation, and validation of m3u8 playlists
module M3u8
# Initialize attributes from a params hash, converting any Hash
# values for :byterange into ByteRange instances.
# @param params [Hash] attribute key-value pairs
# @return [void]
def initialize_with_byterange(params = {})
params.each do |key, value|
value = ByteRange.new(value) if value.is_a?(Hash)
instance_variable_set("@#{key}", value)
end
end

# Parse an HLS attribute list string into a Hash.
# @param line [String] raw attribute list (e.g. 'KEY="val",NUM=1')
# @return [Hash<String, String>] attribute name-value pairs
def parse_attributes(line)
line.delete("\n").scan(/([A-Za-z0-9-]+)\s*=\s*("[^"]*"|[^,]*)/)
.to_h { |key, value| [key, value.delete('"')] }
end

# Convert a string value to Float, returning nil when nil.
# @param value [String, nil] numeric string
# @return [Float, nil]
def parse_float(value)
value&.to_f
end

# Convert a string value to Integer, returning nil when nil.
# @param value [String, nil] numeric string
# @return [Integer, nil]
def parse_int(value)
value&.to_i
end

# Parse an HLS YES/NO attribute into a boolean.
# @param value [String] 'YES' or 'NO'
# @return [Boolean]
def parse_yes_no(value)
value == 'YES'
end

# Convert a boolean into an HLS YES/NO string.
# @param boolean [Boolean] value to convert
# @return [String] 'YES' or 'NO'
def to_yes_no(boolean)
boolean == true ? 'YES' : 'NO'
end
Expand Down
12 changes: 12 additions & 0 deletions lib/m3u8/attribute_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,26 @@
module M3u8
# Shared helpers for formatting HLS tag attributes
module AttributeFormatter
# Format a quoted attribute (e.g. KEY="value").
# @param key [String] attribute name
# @param value [Object, nil] attribute value
# @return [String, nil] formatted string or nil when value is nil
def quoted_format(key, value)
%(#{key}="#{value}") unless value.nil?
end

# Format an unquoted attribute (e.g. KEY=value).
# @param key [String] attribute name
# @param value [Object, nil] attribute value
# @return [String, nil] formatted string or nil when value is nil
def unquoted_format(key, value)
"#{key}=#{value}" unless value.nil?
end

# Format a YES/NO boolean attribute (e.g. KEY=YES).
# @param key [String] attribute name
# @param value [Boolean, nil] attribute value
# @return [String, nil] formatted string or nil when value is nil
def boolean_format(key, value)
"#{key}=#{value == true ? 'YES' : 'NO'}" unless value.nil?
end
Expand Down
7 changes: 7 additions & 0 deletions lib/m3u8/bitrate_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,26 @@ module M3u8
# BitrateItem represents an EXT-X-BITRATE tag that indicates the
# approximate bitrate of the following media segments in kbps.
class BitrateItem
# @return [Integer, nil] approximate bitrate in kbps
attr_accessor :bitrate

# @param params [Hash] attribute key-value pairs
def initialize(params = {})
params.each do |key, value|
instance_variable_set("@#{key}", value)
end
end

# Parse an EXT-X-BITRATE tag.
# @param text [String] raw tag line
# @return [BitrateItem]
def self.parse(text)
value = text.gsub('#EXT-X-BITRATE:', '').strip
BitrateItem.new(bitrate: value.to_i)
end

# Render as an m3u8 EXT-X-BITRATE tag.
# @return [String]
def to_s
"#EXT-X-BITRATE:#{bitrate}"
end
Expand Down
1 change: 1 addition & 0 deletions lib/m3u8/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Builder
gap: 'GapItem'
}.freeze

# @param playlist [Playlist] playlist to build into
def initialize(playlist)
@playlist = playlist
end
Expand Down
8 changes: 8 additions & 0 deletions lib/m3u8/byte_range.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
module M3u8
# ByteRange represents sub range of a resource
class ByteRange
# @return [Integer, nil] number of bytes
# @return [Integer, nil] start offset in bytes
attr_accessor :length, :start

# @param params [Hash] :length and optional :start
def initialize(params = {})
params.each do |key, value|
instance_variable_set("@#{key}", value)
end
end

# Parse a byte range string (e.g. "4500@600").
# @param text [String] byte range string
# @return [ByteRange]
def self.parse(text)
values = text.split('@')
length_value = values[0].to_i
Expand All @@ -19,6 +25,8 @@ def self.parse(text)
ByteRange.new(options)
end

# Render as a byte range string (e.g. "4500@600").
# @return [String]
def to_s
"#{length}#{start_format}"
end
Expand Down
7 changes: 7 additions & 0 deletions lib/m3u8/codecs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,19 @@ module Codecs
['av1-high', 5.1] => 'av01.1.13H.10'
}.freeze

# Look up the codec string for an audio codec name.
# @param codec [String, nil] audio codec name
# @return [String, nil] codec string
def self.audio_codec(codec)
return if codec.nil?

AUDIO_CODECS[codec.downcase]
end

# Look up the codec string for a video profile and level.
# @param profile [String, nil] video profile name
# @param level [Float, Integer, nil] video level
# @return [String, nil] codec string
def self.video_codec(profile, level)
return if profile.nil? || level.nil?

Expand Down
8 changes: 8 additions & 0 deletions lib/m3u8/content_steering_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@ class ContentSteeringItem
extend M3u8
include AttributeFormatter

# @return [String, nil] steering manifest server URI
# @return [String, nil] default pathway ID
attr_accessor :server_uri, :pathway_id

# @param params [Hash] attribute key-value pairs
def initialize(params = {})
params.each do |key, value|
instance_variable_set("@#{key}", value)
end
end

# Parse an EXT-X-CONTENT-STEERING tag.
# @param text [String] raw tag line
# @return [ContentSteeringItem]
def self.parse(text)
attributes = parse_attributes(text)
ContentSteeringItem.new(
Expand All @@ -23,6 +29,8 @@ def self.parse(text)
)
end

# Render as an m3u8 EXT-X-CONTENT-STEERING tag.
# @return [String]
def to_s
"#EXT-X-CONTENT-STEERING:#{formatted_attributes}"
end
Expand Down
113 changes: 80 additions & 33 deletions lib/m3u8/date_range_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,30 @@
module M3u8
# DateRangeItem represents a #EXT-X-DATERANGE tag
class DateRangeItem
include M3u8
extend M3u8
include AttributeFormatter

# @return [String, nil] unique date range identifier
# @return [String, nil] CLASS attribute
# @return [String, nil] start date (ISO 8601)
# @return [String, nil] end date (ISO 8601)
# @return [Float, nil] duration in seconds
# @return [Float, nil] planned duration in seconds
# @return [String, nil] SCTE-35 command hex string
# @return [String, nil] SCTE-35 out hex string
# @return [String, nil] SCTE-35 in hex string
# @return [String, nil] CUE attribute
# @return [Boolean, nil] END-ON-NEXT flag
# @return [Hash, nil] client-defined X- attributes
# @return [String, nil] interstitial asset URI
# @return [String, nil] interstitial asset list URI
# @return [Float, nil] interstitial resume offset
# @return [Float, nil] interstitial playout limit
# @return [String, nil] interstitial restrict value
# @return [String, nil] interstitial snap value
# @return [String, nil] interstitial timeline occupies
# @return [String, nil] interstitial timeline style
# @return [String, nil] content may vary flag
attr_accessor :id, :class_name, :start_date, :end_date, :duration,
:planned_duration, :scte35_cmd, :scte35_out, :scte35_in,
:cue, :end_on_next, :client_attributes,
Expand All @@ -20,45 +41,89 @@ class DateRangeItem
X-CONTENT-MAY-VARY
].freeze

# @param options [Hash] attribute key-value pairs
def initialize(options = {})
options.each do |key, value|
instance_variable_set("@#{key}", value)
end
end

def parse(text)
# Parse an EXT-X-DATERANGE tag.
# @param text [String] raw tag line
# @return [DateRangeItem]
def self.parse(text)
attributes = parse_attributes(text)
@id = attributes['ID']
@class_name = attributes['CLASS']
@start_date = attributes['START-DATE']
@end_date = attributes['END-DATE']
@duration = parse_float(attributes['DURATION'])
@planned_duration = parse_float(attributes['PLANNED-DURATION'])
@scte35_cmd = attributes['SCTE35-CMD']
@scte35_out = attributes['SCTE35-OUT']
@scte35_in = attributes['SCTE35-IN']
@cue = attributes['CUE']
@end_on_next = attributes.key?('END-ON-NEXT')
parse_interstitials(attributes)
@client_attributes = parse_client_attributes(attributes)
options = parse_base_attributes(attributes)
.merge(parse_interstitials(attributes))
.merge(client_attributes:
parse_client_attributes(attributes))
DateRangeItem.new(options)
end

def self.parse_base_attributes(attributes)
{ id: attributes['ID'],
class_name: attributes['CLASS'],
start_date: attributes['START-DATE'],
end_date: attributes['END-DATE'],
duration: parse_float(attributes['DURATION']),
planned_duration:
parse_float(attributes['PLANNED-DURATION']),
scte35_cmd: attributes['SCTE35-CMD'],
scte35_out: attributes['SCTE35-OUT'],
scte35_in: attributes['SCTE35-IN'],
cue: attributes['CUE'],
end_on_next: attributes.key?('END-ON-NEXT') }
end
private_class_method :parse_base_attributes

def self.parse_interstitials(attributes)
{ asset_uri: attributes['X-ASSET-URI'],
asset_list: attributes['X-ASSET-LIST'],
resume_offset:
parse_float(attributes['X-RESUME-OFFSET']),
playout_limit:
parse_float(attributes['X-PLAYOUT-LIMIT']),
restrict: attributes['X-RESTRICT'],
snap: attributes['X-SNAP'],
timeline_occupies:
attributes['X-TIMELINE-OCCUPIES'],
timeline_style: attributes['X-TIMELINE-STYLE'],
content_may_vary:
attributes['X-CONTENT-MAY-VARY'] }
end
private_class_method :parse_interstitials

# Render as an m3u8 EXT-X-DATERANGE tag.
# @return [String]
def to_s
"#EXT-X-DATERANGE:#{formatted_attributes}"
end

# Parse SCTE-35 command data.
# @return [Scte35, nil]
def scte35_cmd_info
Scte35.parse(scte35_cmd) unless scte35_cmd.nil?
end

# Parse SCTE-35 out data.
# @return [Scte35, nil]
def scte35_out_info
Scte35.parse(scte35_out) unless scte35_out.nil?
end

# Parse SCTE-35 in data.
# @return [Scte35, nil]
def scte35_in_info
Scte35.parse(scte35_in) unless scte35_in.nil?
end

def self.parse_client_attributes(attributes)
attributes.select do |key|
key.start_with?('X-') && !INTERSTITIAL_KEYS.include?(key)
end
end
private_class_method :parse_client_attributes

private

def formatted_attributes
Expand Down Expand Up @@ -98,18 +163,6 @@ def decimal?(value)
end
end

def parse_interstitials(attributes)
@asset_uri = attributes['X-ASSET-URI']
@asset_list = attributes['X-ASSET-LIST']
@resume_offset = parse_float(attributes['X-RESUME-OFFSET'])
@playout_limit = parse_float(attributes['X-PLAYOUT-LIMIT'])
@restrict = attributes['X-RESTRICT']
@snap = attributes['X-SNAP']
@timeline_occupies = attributes['X-TIMELINE-OCCUPIES']
@timeline_style = attributes['X-TIMELINE-STYLE']
@content_may_vary = attributes['X-CONTENT-MAY-VARY']
end

def interstitial_formats
[quoted_format('X-ASSET-URI', asset_uri),
quoted_format('X-ASSET-LIST', asset_list),
Expand All @@ -127,11 +180,5 @@ def end_on_next_format

'END-ON-NEXT=YES'
end

def parse_client_attributes(attributes)
attributes.select do |key|
key.start_with?('X-') && !INTERSTITIAL_KEYS.include?(key)
end
end
end
end
10 changes: 10 additions & 0 deletions lib/m3u8/define_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,22 @@ module M3u8
class DefineItem
extend M3u8

# @return [String, nil] variable name
# @return [String, nil] variable value
# @return [String, nil] imported variable name
# @return [String, nil] query parameter name
attr_accessor :name, :value, :import, :queryparam

# @param params [Hash] attribute key-value pairs
def initialize(params = {})
params.each do |key, val|
instance_variable_set("@#{key}", val)
end
end

# Parse an EXT-X-DEFINE tag.
# @param text [String] raw tag line
# @return [DefineItem]
def self.parse(text)
attributes = parse_attributes(text)
DefineItem.new(
Expand All @@ -25,6 +33,8 @@ def self.parse(text)
)
end

# Render as an m3u8 EXT-X-DEFINE tag.
# @return [String]
def to_s
"#EXT-X-DEFINE:#{formatted_attributes}"
end
Expand Down
2 changes: 2 additions & 0 deletions lib/m3u8/discontinuity_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module M3u8
# DiscontinuityItem represents a EXT-X-DISCONTINUITY tag to indicate a
# discontinuity between the SegmentItems that proceed and follow it.
class DiscontinuityItem
# Render as an m3u8 EXT-X-DISCONTINUITY tag.
# @return [String]
def to_s
"#EXT-X-DISCONTINUITY\n"
end
Expand Down
Loading