Class: M3Uzi

Inherits:
Object
  • Object
show all
Defined in:
lib/m3uzi.rb,
lib/m3uzi/tag.rb,
lib/m3uzi/file.rb,
lib/m3uzi/item.rb,
lib/m3uzi/stream.rb,
lib/m3uzi/comment.rb,
lib/m3uzi/version.rb

Defined Under Namespace

Classes: Comment, File, Item, Stream, Tag

Constant Summary collapse

VERSION =
'0.5.1'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeM3Uzi

Returns a new instance of M3Uzi.



15
16
17
18
19
20
21
22
23
24
# File 'lib/m3uzi.rb', line 15

def initialize
  @header_tags = {}
  @playlist_items = []
  @final_media_file = true
  @version = 1
  @initial_media_sequence = 0
  @sliding_window_duration = nil
  @removed_file_count = 0
  @playlist_type = :live
end

Instance Attribute Details

#final_media_fileObject

Returns the value of attribute final_media_file.



12
13
14
# File 'lib/m3uzi.rb', line 12

def final_media_file
  @final_media_file
end

#header_tagsObject

Returns the value of attribute header_tags.



11
12
13
# File 'lib/m3uzi.rb', line 11

def header_tags
  @header_tags
end

#initial_media_sequenceObject

Returns the value of attribute initial_media_sequence.



13
14
15
# File 'lib/m3uzi.rb', line 13

def initial_media_sequence
  @initial_media_sequence
end

#playlist_itemsObject

Returns the value of attribute playlist_items.



11
12
13
# File 'lib/m3uzi.rb', line 11

def playlist_items
  @playlist_items
end

#playlist_typeObject

Returns the value of attribute playlist_type.



12
13
14
# File 'lib/m3uzi.rb', line 12

def playlist_type
  @playlist_type
end

#sliding_window_durationObject

Returns the value of attribute sliding_window_duration.



13
14
15
# File 'lib/m3uzi.rb', line 13

def sliding_window_duration
  @sliding_window_duration
end

#versionObject

Returns the value of attribute version.



13
14
15
# File 'lib/m3uzi.rb', line 13

def version
  @version
end

Instance Method Details

#add_comment(comment = nil) {|new_comment| ... } ⇒ Object


Comments


Yields:

  • (new_comment)


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

def add_comment(comment = nil)
  new_comment = M3Uzi::Comment.new
  new_comment.text = comment
  yield(new_comment) if block_given?
  @playlist_items << new_comment
end

#add_file(path = nil, duration = nil) {|new_file| ... } ⇒ Object


Files


Yields:

  • (new_file)


195
196
197
198
199
200
201
202
# File 'lib/m3uzi.rb', line 195

def add_file(path = nil, duration = nil)
  new_file = M3Uzi::File.new
  new_file.path = path if path
  new_file.duration = duration if duration
  yield(new_file) if block_given?
  @playlist_items << new_file
  cleanup_sliding_window
end

#add_stream(path = nil, bandwidth = nil) {|new_stream| ... } ⇒ Object


Streams


Yields:

  • (new_stream)


213
214
215
216
217
218
219
# File 'lib/m3uzi.rb', line 213

def add_stream(path = nil, bandwidth = nil)
  new_stream = M3Uzi::Stream.new
  new_stream.path = path
  new_stream.bandwidth = bandwidth
  yield(new_stream) if block_given?
  @playlist_items << new_stream
end

#add_tag(name = nil, value = nil) {|new_tag| ... } ⇒ Object


Tags


Yields:

  • (new_tag)


230
231
232
233
234
235
236
# File 'lib/m3uzi.rb', line 230

def add_tag(name = nil, value = nil)
  new_tag = M3Uzi::Tag.new
  new_tag.name = name
  new_tag.value = value
  yield(new_tag) if block_given?
  @header_tags[new_tag.name] = new_tag
end

#check_version_restrictionsObject

def <<(comment)

add_comment(comment)

end



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/m3uzi.rb', line 267

def check_version_restrictions
  @version = 1

  #
  # Version 2 Features
  #

  # Check for custom IV
  if valid_items(File).detect { |item| item.encryption_key_url && item.encryption_iv }
    @version = 2 if @version < 2
  end

  # Version 3 Features
  if valid_items(File).detect { |item| item.duration.kind_of?(Float) }
    @version = 3 if @version < 3
  end

  # Version 4 Features
  if valid_items(File).detect { |item| item.byterange }
    @version = 4 if @version < 4
  end
  if valid_items(Tag).detect { |item| ['MEDIA','I-FRAMES-ONLY'].include?(item.name) }
    @version = 4 if @version < 4
  end

  # NOTES
  #   EXT-X-I-FRAME-STREAM-INF is supposed to be ignored by older clients.
  #   AUDIO/VIDEO attributes of X-STREAM-INF are used in conjunction with MEDIA, so it should trigger v4.

  @version
end

#filenamesObject



204
205
206
# File 'lib/m3uzi.rb', line 204

def filenames
  items(File).map { |file| file.path }
end

#generate_byterange_line(file) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/m3uzi.rb', line 167

def generate_byterange_line(file)
  line = nil

  if file.byterange
    if file.byterange_offset && file.byterange_offset != @prev_byterange_endpoint
      offset = file.byterange_offset
    elsif @prev_byterange_endpoint.nil?
      offset = 0
    else
      offset = nil
    end

    line = "#EXT-X-BYTERANGE:#{file.byterange_offset.to_i}"
    line += "@#{offset}" if offset

    @prev_byterange_endpoint = offset + file.byterange
  else
    @prev_byterange_endpoint = nil
  end

  line
end

#generate_encryption_key_line(file) ⇒ 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
# File 'lib/m3uzi.rb', line 132

def generate_encryption_key_line(file)
  generate_line = false

  default_iv = @encryption_iv || format_iv(@encryption_sequence)

  if (file.encryption_key_url != :unset) && (file.encryption_key_url != @encryption_key_url)
    @encryption_key_url = file.encryption_key_url
    generate_line = true
  end

  if @encryption_key_url && file.encryption_iv != @encryption_iv
    @encryption_iv = file.encryption_iv
    generate_line = true
  end

  @encryption_sequence += 1

  if generate_line
    if @encryption_key_url.nil?
      "#EXT-X-KEY:METHOD=NONE"
    else
      attrs = ['METHOD=AES-128']
      attrs << 'URI="' + @encryption_key_url.gsub('"','%22').gsub(/[\r\n]/,'').strip + '"'
      attrs << "IV=#{@encryption_iv}" if @encryption_iv
      '#EXT-X-KEY:' + attrs.join(',')
    end
  else
    nil
  end
end

#items(kind) ⇒ Object



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

def items(kind)
  @playlist_items.select { |item| item.kind_of?(kind) }
end

#reset_byterange_historyObject



163
164
165
# File 'lib/m3uzi.rb', line 163

def reset_byterange_history
  @prev_byterange_endpoint = nil
end

#reset_encryption_key_historyObject


Playlist generation helpers.




126
127
128
129
130
# File 'lib/m3uzi.rb', line 126

def reset_encryption_key_history
  @encryption_key_url = nil
  @encryption_iv = nil
  @encryption_sequence = 0
end

#stream_namesObject



221
222
223
# File 'lib/m3uzi.rb', line 221

def stream_names
  items(Stream).map { |stream| stream.path }
end

#valid_items(kind) ⇒ Object



118
119
120
# File 'lib/m3uzi.rb', line 118

def valid_items(kind)
  @playlist_items.select { |item| item.kind_of?(kind) && item.valid? }
end

#write(path) ⇒ Object



110
111
112
# File 'lib/m3uzi.rb', line 110

def write(path)
  ::File.open(path, "w") { |f| write_to_io(f) }
end

#write_to_io(io_stream) ⇒ Object

For now, reading m3u8 files is not keeping up to date with writing, so we’re disabling it in this version. (Possibly to be re-introduced in the future.)

def self.read(path)

m3u = self.new
lines = ::File.readlines(path)
lines.each_with_index do |line, i|
  case type(line)
  when :tag
    name, value = parse_general_tag(line)
    m3u.add_tag do |tag|
      tag.name = name
      tag.value = value
    end
  when :info
    duration, description = parse_file_tag(line)
    m3u.add_file do |file|
      file.path = lines[i+1].strip
      file.duration = duration
      file.description = description
    end
    m3u.final_media_file = false
  when :stream
    attributes = parse_stream_tag(line)
    m3u.add_stream do |stream|
      stream.path = lines[i+1].strip
      attributes.each_pair do |k,v|
        k = k.to_s.downcase.sub('-','_')
        next unless [:bandwidth, :program_id, :codecs, :resolution].include?(k)
        v = $1 if v.to_s =~ /^"(.*)"$/
        stream.send("#{k}=", v)
      end
    end
  when :final
    m3u.final_media_file = true
  else
    next
  end
end
m3u

end



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/m3uzi.rb', line 74

def write_to_io(io_stream)
  reset_encryption_key_history
  reset_byterange_history

  check_version_restrictions
  io_stream << "#EXTM3U\n"
  io_stream << "#EXT-X-VERSION:#{@version.to_i}\n" if @version > 1
  io_stream << "#EXT-X-PLAYLIST-TYPE:#{@playlist_type.to_s.upcase}\n" if [:event,:vod].include?(@playlist_type)

  if items(File).length > 0
    io_stream << "#EXT-X-MEDIA-SEQUENCE:#{@initial_media_sequence+@removed_file_count}\n" if @playlist_type == :live
    max_duration = valid_items(File).map { |f| f.duration.to_f }.max || 10.0
    io_stream << "#EXT-X-TARGETDURATION:#{max_duration.ceil}\n"
  end

  @header_tags.each do |item|
    io_stream << (item.format + "\n") if item.valid?
  end

  @playlist_items.each do |item|
    next unless item.valid?

    if item.kind_of?(File)
      encryption_key_line = generate_encryption_key_line(item)
      io_stream << (encryption_key_line + "\n") if encryption_key_line

      byterange_line = generate_byterange_line(item)
      io_stream << (byterange_line + "\n") if byterange_line
    end

    io_stream << (item.format + "\n")
  end

  io_stream << "#EXT-X-ENDLIST\n" if items(File).length > 0 && (@final_media_file || @playlist_type == :vod)
end