Class: HLSManifest

Inherits:
Object
  • Object
show all
Includes:
HLSConstants
Defined in:
lib/hls_manifest.rb

Constant Summary

Constants included from HLSConstants

HLSConstants::EXTINF, HLSConstants::EXTM3U, HLSConstants::EXT_X_ALLOW_CACHE, HLSConstants::EXT_X_CUE_IN, HLSConstants::EXT_X_CUE_OUT, HLSConstants::EXT_X_DISCONTINUITY, HLSConstants::EXT_X_ENDLIST, HLSConstants::EXT_X_FAXS_CM, HLSConstants::EXT_X_KEY, HLSConstants::EXT_X_KEY_ATTRS, HLSConstants::EXT_X_MEDIA, HLSConstants::EXT_X_MEDIA_SEQUENCE, HLSConstants::EXT_X_PLAYLIST_TYPE, HLSConstants::EXT_X_STREAM_INF, HLSConstants::EXT_X_STREAM_INF_ATTRS, HLSConstants::EXT_X_TARGETDURATION, HLSConstants::EXT_X_VERSION, HLSConstants::FIXNUM_MAX, HLSConstants::SUPPORTED_M3U8_VERSIONS

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(content = nil) ⇒ HLSManifest

Returns a new instance of HLSManifest.



119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/hls_manifest.rb', line 119

def initialize(content = nil)
  @allow_cache = nil
  @media_sequence = nil
  @playlist_type = nil
  @target_duration = nil
  @version = nil
  @is_live = true
  @line_items = []
  @cue_out_items = []

  parse(content)
end

Instance Attribute Details

#cue_out_itemsObject

Returns the value of attribute cue_out_items.



117
118
119
# File 'lib/hls_manifest.rb', line 117

def cue_out_items
  @cue_out_items
end

#line_itemsObject

Returns the value of attribute line_items.



116
117
118
# File 'lib/hls_manifest.rb', line 116

def line_items
  @line_items
end

#media_sequenceObject

Returns the value of attribute media_sequence.



112
113
114
# File 'lib/hls_manifest.rb', line 112

def media_sequence
  @media_sequence
end

#playlist_typeObject

Returns the value of attribute playlist_type.



113
114
115
# File 'lib/hls_manifest.rb', line 113

def playlist_type
  @playlist_type
end

#target_durationObject

Returns the value of attribute target_duration.



114
115
116
# File 'lib/hls_manifest.rb', line 114

def target_duration
  @target_duration
end

#versionObject

Returns the value of attribute version.



115
116
117
# File 'lib/hls_manifest.rb', line 115

def version
  @version
end

Instance Method Details

#allow_cache?Boolean

Returns:

  • (Boolean)


248
249
250
251
# File 'lib/hls_manifest.rb', line 248

def allow_cache?
  # Not raising an error here in case the EXT-X-ALLOW-CACHE tag is supported for master-level m3u8s
  @allow_cache.nil? || @allow_cache == "YES"
end

#buildObject



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/hls_manifest.rb', line 273

def build
  if is_master?
    return <<-EOT.remove_extra_spaces
      #{EXTM3U}
      #{@line_items.map { |item| item.build }.join("\n")}
    EOT
  else
    return <<-EOT.remove_extra_spaces
      #{EXTM3U}
      #{EXT_X_ALLOW_CACHE + ":" + @allow_cache unless @allow_cache.nil?}
      #{EXT_X_MEDIA_SEQUENCE + ":" + @media_sequence.to_s unless @media_sequence.nil?}
      #{EXT_X_PLAYLIST_TYPE + ":" + @playlist_type unless @playlist_type.nil?}
      #{EXT_X_TARGETDURATION + ":" + @target_duration.to_s unless @target_duration.nil?}
      #{EXT_X_VERSION + ":" + @version.to_s unless @version.nil?}
      #{@line_items.map { |item| item.build }.join("\n")}
      #{EXT_X_ENDLIST unless is_live?}
    EOT
  end
end

#empty?Boolean

Returns:

  • (Boolean)


244
245
246
# File 'lib/hls_manifest.rb', line 244

def empty?
  @line_items.empty?
end

#is_live?Boolean

Returns:

  • (Boolean)

Raises:

  • (NoMethodError)


263
264
265
266
# File 'lib/hls_manifest.rb', line 263

def is_live?
  raise NoMethodError, "A master-level manifest cannot access this method" if is_master?
  @is_live
end

#is_master?Boolean

Returns:

  • (Boolean)


268
269
270
271
# File 'lib/hls_manifest.rb', line 268

def is_master?
  # Note: This currently assumes an empty m3u8 is NOT a master m3u8
  !@line_items.empty? && @line_items[0].instance_of?(HLSPlaylist)
end

#lengthObject



253
254
255
# File 'lib/hls_manifest.rb', line 253

def length
  @line_items.length
end

#media_lengthObject

Raises:

  • (NoMethodError)


257
258
259
260
261
# File 'lib/hls_manifest.rb', line 257

def media_length
  # Note(bz): Should we be raising errors here? I'm not sure what our standard or the Ruby standard is
  raise NoMethodError, "A master-level manifest cannot access this method" if is_master?
  @line_items.inject(0) { |sum, x| sum + x.duration }
end

#parse(content) ⇒ Object



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/hls_manifest.rb', line 132

def parse(content)
  return if content.nil? || content.empty?

  last_attributes = []
  last_key = nil
  last_playlist_or_media_segment = nil
  last_unhandled_tags = []
  last_sequence = 0

  # Remove any blank lines (including lines with spaces just to be safe)
  lines = content.split("\n").map(&:strip).reject(&:empty?)

  lines.each do |line|
    if line.start_with?(EXT_X_TARGETDURATION)
      @target_duration = line.split(":").last.to_i
    elsif line.start_with?(EXT_X_MEDIA_SEQUENCE)
      @media_sequence = line.split(":").last.to_i
    elsif line.start_with?(EXT_X_PLAYLIST_TYPE)
      @playlist_type = line.split(":").last
    elsif line.start_with?(EXT_X_ENDLIST)
      @is_live = false
    elsif line.start_with?(EXT_X_ALLOW_CACHE)
      @allow_cache = line.split(":").last
    elsif line.start_with?(EXT_X_VERSION)
      @version = line.split(":").last.to_i
    elsif line.start_with?(EXT_X_STREAM_INF)
      attributes = {}
      parse_csv(line.split(":").last).each do |value|
        attribute = value.split("=")
        attributes[attribute[0].strip.upcase] = attribute[1].strip
      end

      playlist = HLSPlaylist.new
      playlist.bandwidth = attributes[EXT_X_STREAM_INF_ATTRS[:BANDWIDTH]]
      playlist.program_id = attributes[EXT_X_STREAM_INF_ATTRS[:PROGRAM_ID]]
      playlist.codecs = attributes[EXT_X_STREAM_INF_ATTRS[:CODECS]]
      playlist.resolution = attributes[EXT_X_STREAM_INF_ATTRS[:RESOLUTION]]
      playlist.audio = attributes[EXT_X_STREAM_INF_ATTRS[:AUDIO]]
      playlist.video = attributes[EXT_X_STREAM_INF_ATTRS[:VIDEO]]

      playlist.bandwidth = playlist.bandwidth.to_i if playlist.bandwidth
      playlist.program_id = playlist.program_id if playlist.program_id
      playlist.codecs = parse_csv(playlist.codecs) if playlist.codecs

      last_playlist_or_media_segment = playlist
      @line_items.push(playlist)
    elsif line.start_with?(EXTINF)
      attributes = parse_csv(line.split(":").last)

      media_segment = HLSMediaSegment.new
      media_segment.duration = attributes[0].to_f
      media_segment.title = attributes[1]
      media_segment.key = last_key
      media_segment.sequence = last_sequence
      last_sequence += 1
      if last_playlist_or_media_segment && last_playlist_or_media_segment.key = last_key
        media_segment.show_key = false
      end

      last_playlist_or_media_segment = media_segment
      @line_items.push(media_segment)
    elsif line.start_with?(EXT_X_DISCONTINUITY)
      last_attributes.push({ :key => 'discontinuity', :val => true })
    elsif  line.start_with?(EXT_X_CUE_OUT)
      last_attributes.push({ :key => 'is_cue_out', :val => true })
      cue_dur = line.match('DURATION=(\d*)')
      last_attributes.push({ :key => 'cue_out_duration', :val => cue_dur[1].to_i }) if cue_dur
      # may or may not contain a duration.
    elsif line.start_with?(EXT_X_CUE_IN)
      last_attributes.push({ :key => 'is_cue_in', :val => true })
    elsif line.start_with?(EXT_X_KEY)
      attributes = {}
      parse_csv(line.split(":", 2).last).each do |attribute|
        attribute = attribute.split("=")
        attributes[attribute[0].strip.upcase] = attribute[1].strip
      end

      key = HLSKey.new(attributes[EXT_X_KEY_ATTRS[:METHOD]],
                       attributes[EXT_X_KEY_ATTRS[:URI]],
                       attributes[EXT_X_KEY_ATTRS[:IV]])

      key.uri.gsub!("\"", "") if key.uri
      key.iv = key.iv.hex if key.iv
      last_key = key

    elsif line.start_with?("#")
      next if EXTM3U == line
      # Handle other unhandled tags or comments
      # Note: Assume that all unhandled tags belong to the sub playlist or media segment that follow
      # This means that trailing tags are not parsed
      # TODO: Add support for other manifest level tags that don't belong to individual line items
      last_unhandled_tags.push(line)
    else
      # We should be dealing with the location of a sub playlist or a media segment here
      # Note: Assume that a manifest cannot contain a mixture of sub playlists and media segments
      if last_playlist_or_media_segment.instance_of?(HLSMediaSegment)
        last_attributes.each{|x| last_playlist_or_media_segment.send("#{x[:key]}=", x[:val]) }
        @cue_out_items.push(last_playlist_or_media_segment) if last_playlist_or_media_segment.is_cue_out
      end
      last_playlist_or_media_segment.location = line
      last_playlist_or_media_segment.tags = last_unhandled_tags
      last_attributes.clear
      last_unhandled_tags = []
    end
  end
end