m3u8_generator 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ # All tags and attribute names below are described in the HLS draft found at:
2
+ # http://tools.ietf.org/html/draft-pantos-http-live-streaming-08
3
+
4
+ module HLSConstants
5
+ # Versions
6
+ SUPPORTED_M3U8_VERSIONS = [4]
7
+ FIXNUM_MAX = (2**(0.size * 8 -2) -1)
8
+
9
+
10
+ # Global (or we're not sure what level they apply to) tags
11
+ EXTM3U = "#EXTM3U"
12
+ EXT_X_VERSION = "#EXT-X-VERSION"
13
+ EXT_X_FAXS_CM = "#EXT-X-FAXS-CM"
14
+
15
+ # Master-level m3u8 tags
16
+
17
+ # Playlist tags
18
+ EXT_X_MEDIA = "#EXT-X-MEDIA"
19
+ EXT_X_STREAM_INF = "#EXT-X-STREAM-INF"
20
+
21
+ # EXT_X_STREAM_INF attribute names
22
+ EXT_X_STREAM_INF_ATTRS = { :BANDWIDTH => "BANDWIDTH",
23
+ :CODECS => "CODECS",
24
+ :PROGRAM_ID => "PROGRAM-ID",
25
+ :RESOLUTION => "RESOLUTION",
26
+ :AUDIO => "AUDIO",
27
+ :VIDEO => "VIDEO" }
28
+
29
+ # Sub-level m3u8 tags
30
+ EXT_X_ENDLIST = "#EXT-X-ENDLIST"
31
+ EXT_X_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE"
32
+ EXT_X_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE"
33
+ EXT_X_TARGETDURATION = "#EXT-X-TARGETDURATION"
34
+
35
+ # Media segment tags
36
+ EXTINF = "#EXTINF"
37
+ EXT_X_ALLOW_CACHE = "#EXT-X-ALLOW-CACHE"
38
+ EXT_X_DISCONTINUITY = "#EXT-X-DISCONTINUITY"
39
+ EXT_X_KEY = "#EXT-X-KEY"
40
+
41
+ EXT_X_CUE_OUT = "#EXT-X-CUE-OUT"
42
+ EXT_X_CUE_IN = "#EXT-X-CUE-IN"
43
+
44
+ # EXT_X_KEY attribute names
45
+ EXT_X_KEY_ATTRS = { :METHOD => "METHOD",
46
+ :URI => "URI",
47
+ :IV => "IV" }
48
+ end
@@ -0,0 +1,311 @@
1
+ # An m3u8 parser for HLS (http live streaming) delivery. Converts a string representing an m3u8 manifest
2
+ # to and from a ruby object.
3
+ #
4
+ # Conforms to version 4 of the HLS draft found at:
5
+ # http://tools.ietf.org/html/draft-pantos-http-live-streaming-08
6
+
7
+ require_relative "hls_constants"
8
+
9
+ class String
10
+ # Remove all empty lines and pre whitespace
11
+ def remove_extra_spaces
12
+ self.split("\n").map(&:strip).reject(&:empty?).join("\n")
13
+ end
14
+ end
15
+
16
+ class HLSPlaylist
17
+ include HLSConstants
18
+
19
+ attr_accessor :bandwidth
20
+ attr_accessor :program_id
21
+ attr_accessor :codecs
22
+ attr_accessor :resolution
23
+ attr_accessor :audio
24
+ attr_accessor :video
25
+ attr_accessor :location
26
+ attr_accessor :tags
27
+
28
+ def build
29
+ return "" unless @location
30
+
31
+ attributes = []
32
+ attributes.push("#{EXT_X_STREAM_INF_ATTRS[:BANDWIDTH]}=#{@bandwidth}") if @bandwidth
33
+ attributes.push("#{EXT_X_STREAM_INF_ATTRS[:PROGRAM_ID]}=#{@program_id}") if @program_id
34
+ attributes.push("#{EXT_X_STREAM_INF_ATTRS[:CODECS]}=#{@codecs.join(",")}") if @codecs
35
+ attributes.push("#{EXT_X_STREAM_INF_ATTRS[:RESOLUTION]}=#{@resolution}") if @resolution
36
+ attributes.push("#{EXT_X_STREAM_INF_ATTRS[:AUDIO]}=#{@audio}") if @audio
37
+ attributes.push("#{EXT_X_STREAM_INF_ATTRS[:VIDEO]}=#{@video}") if @video
38
+
39
+ <<-EOT.remove_extra_spaces
40
+ #{@tags.join("\n")}
41
+ #{EXT_X_STREAM_INF + ":" + attributes.join(",") unless attributes.empty?}
42
+ #{@location}
43
+ EOT
44
+ end
45
+ end
46
+
47
+ class HLSMediaSegment
48
+ include HLSConstants
49
+
50
+ attr_accessor :key
51
+ attr_accessor :show_key
52
+ attr_accessor :duration
53
+ attr_accessor :title
54
+ attr_accessor :location
55
+ attr_accessor :discontinuity
56
+ attr_accessor :tags
57
+ attr_accessor :cue_out_duration
58
+ attr_accessor :is_cue_out
59
+ attr_accessor :is_cue_in
60
+ attr_accessor :sequence
61
+
62
+ def initialize
63
+ @show_key = true
64
+ @discontinuity = false
65
+ @duration = 0
66
+ @cue_out_duration = FIXNUM_MAX
67
+ @is_cue_out = false
68
+ @is_cue_in = false
69
+ end
70
+
71
+ def build
72
+ return "" unless @location
73
+
74
+ <<-EOT.remove_extra_spaces
75
+ #{@tags.join("\n")}
76
+ #{@discontinuity ? EXT_X_DISCONTINUITY : ""}
77
+ #{@key.build if @key && @show_key}
78
+ #{EXTINF}:#{@duration},#{@title}
79
+ #{@location}
80
+ EOT
81
+ end
82
+ end
83
+
84
+ class HLSKey
85
+ include HLSConstants
86
+
87
+ attr_accessor :method
88
+ attr_accessor :uri
89
+ attr_accessor :iv
90
+
91
+ def initialize(method = "NONE", uri = nil, iv = nil)
92
+ @method = method
93
+ @uri = uri
94
+ @iv = iv
95
+ end
96
+
97
+ def build
98
+ attributes = []
99
+ attributes.push("#{EXT_X_KEY_ATTRS[:METHOD]}=#{@method}") if @method
100
+ attributes.push("#{EXT_X_KEY_ATTRS[:URI]}=\"#{@uri}\"") if @uri
101
+ attributes.push("#{EXT_X_KEY_ATTRS[:IV]}=0x#{@iv.to_s(16)}") if @iv
102
+
103
+ <<-EOT.remove_extra_spaces
104
+ #{EXT_X_KEY + ":" + attributes.join(",") unless attributes.empty?}
105
+ EOT
106
+ end
107
+ end
108
+
109
+ class HLSManifest
110
+ include HLSConstants
111
+
112
+ attr_accessor :media_sequence
113
+ attr_accessor :playlist_type
114
+ attr_accessor :target_duration
115
+ attr_accessor :version
116
+ attr_accessor :line_items
117
+ attr_accessor :cue_out_items
118
+
119
+ def initialize(content = nil)
120
+ @allow_cache = nil
121
+ @media_sequence = nil
122
+ @playlist_type = nil
123
+ @target_duration = nil
124
+ @version = nil
125
+ @is_live = true
126
+ @line_items = []
127
+ @cue_out_items = []
128
+
129
+ parse(content)
130
+ end
131
+
132
+ def parse(content)
133
+ return if content.nil? || content.empty?
134
+
135
+ last_attributes = []
136
+ last_key = nil
137
+ last_playlist_or_media_segment = nil
138
+ last_unhandled_tags = []
139
+ last_sequence = 0
140
+
141
+ # Remove any blank lines (including lines with spaces just to be safe)
142
+ lines = content.split("\n").map(&:strip).reject(&:empty?)
143
+
144
+ lines.each do |line|
145
+ if line.start_with?(EXT_X_TARGETDURATION)
146
+ @target_duration = line.split(":").last.to_i
147
+ elsif line.start_with?(EXT_X_MEDIA_SEQUENCE)
148
+ @media_sequence = line.split(":").last.to_i
149
+ elsif line.start_with?(EXT_X_PLAYLIST_TYPE)
150
+ @playlist_type = line.split(":").last
151
+ elsif line.start_with?(EXT_X_ENDLIST)
152
+ @is_live = false
153
+ elsif line.start_with?(EXT_X_ALLOW_CACHE)
154
+ @allow_cache = line.split(":").last
155
+ elsif line.start_with?(EXT_X_VERSION)
156
+ @version = line.split(":").last.to_i
157
+ elsif line.start_with?(EXT_X_STREAM_INF)
158
+ attributes = {}
159
+ parse_csv(line.split(":").last).each do |value|
160
+ attribute = value.split("=")
161
+ attributes[attribute[0].strip.upcase] = attribute[1].strip
162
+ end
163
+
164
+ playlist = HLSPlaylist.new
165
+ playlist.bandwidth = attributes[EXT_X_STREAM_INF_ATTRS[:BANDWIDTH]]
166
+ playlist.program_id = attributes[EXT_X_STREAM_INF_ATTRS[:PROGRAM_ID]]
167
+ playlist.codecs = attributes[EXT_X_STREAM_INF_ATTRS[:CODECS]]
168
+ playlist.resolution = attributes[EXT_X_STREAM_INF_ATTRS[:RESOLUTION]]
169
+ playlist.audio = attributes[EXT_X_STREAM_INF_ATTRS[:AUDIO]]
170
+ playlist.video = attributes[EXT_X_STREAM_INF_ATTRS[:VIDEO]]
171
+
172
+ playlist.bandwidth = playlist.bandwidth.to_i if playlist.bandwidth
173
+ playlist.program_id = playlist.program_id if playlist.program_id
174
+ playlist.codecs = parse_csv(playlist.codecs) if playlist.codecs
175
+
176
+ last_playlist_or_media_segment = playlist
177
+ @line_items.push(playlist)
178
+ elsif line.start_with?(EXTINF)
179
+ attributes = parse_csv(line.split(":").last)
180
+
181
+ media_segment = HLSMediaSegment.new
182
+ media_segment.duration = attributes[0].to_f
183
+ media_segment.title = attributes[1]
184
+ media_segment.key = last_key
185
+ media_segment.sequence = last_sequence
186
+ last_sequence += 1
187
+ if last_playlist_or_media_segment && last_playlist_or_media_segment.key = last_key
188
+ media_segment.show_key = false
189
+ end
190
+
191
+ last_playlist_or_media_segment = media_segment
192
+ @line_items.push(media_segment)
193
+ elsif line.start_with?(EXT_X_DISCONTINUITY)
194
+ last_attributes.push({ :key => 'discontinuity', :val => true })
195
+ elsif line.start_with?(EXT_X_CUE_OUT)
196
+ last_attributes.push({ :key => 'is_cue_out', :val => true })
197
+ cue_dur = line.match('DURATION=(\d*)')
198
+ last_attributes.push({ :key => 'cue_out_duration', :val => cue_dur[1].to_i }) if cue_dur
199
+ # may or may not contain a duration.
200
+ elsif line.start_with?(EXT_X_CUE_IN)
201
+ last_attributes.push({ :key => 'is_cue_in', :val => true })
202
+ elsif line.start_with?(EXT_X_KEY)
203
+ attributes = {}
204
+ parse_csv(line.split(":", 2).last).each do |attribute|
205
+ attribute = attribute.split("=")
206
+ attributes[attribute[0].strip.upcase] = attribute[1].strip
207
+ end
208
+
209
+ key = HLSKey.new(attributes[EXT_X_KEY_ATTRS[:METHOD]],
210
+ attributes[EXT_X_KEY_ATTRS[:URI]],
211
+ attributes[EXT_X_KEY_ATTRS[:IV]])
212
+
213
+ key.uri.gsub!("\"", "") if key.uri
214
+ key.iv = key.iv.hex if key.iv
215
+ last_key = key
216
+
217
+ elsif line.start_with?("#")
218
+ next if EXTM3U == line
219
+ # Handle other unhandled tags or comments
220
+ # Note: Assume that all unhandled tags belong to the sub playlist or media segment that follow
221
+ # This means that trailing tags are not parsed
222
+ # TODO: Add support for other manifest level tags that don't belong to individual line items
223
+ last_unhandled_tags.push(line)
224
+ else
225
+ # We should be dealing with the location of a sub playlist or a media segment here
226
+ # Note: Assume that a manifest cannot contain a mixture of sub playlists and media segments
227
+ if last_playlist_or_media_segment.instance_of?(HLSMediaSegment)
228
+ last_attributes.each{|x| last_playlist_or_media_segment.send("#{x[:key]}=", x[:val]) }
229
+ @cue_out_items.push(last_playlist_or_media_segment) if last_playlist_or_media_segment.is_cue_out
230
+ end
231
+ last_playlist_or_media_segment.location = line
232
+ last_playlist_or_media_segment.tags = last_unhandled_tags
233
+ last_attributes.clear
234
+ last_unhandled_tags = []
235
+ end
236
+ end
237
+ end
238
+
239
+ def version=(version)
240
+ # TODO: Don't fail silently
241
+ @version = version if SUPPORTED_M3U8_VERSIONS.include?(version)
242
+ end
243
+
244
+ def empty?
245
+ @line_items.empty?
246
+ end
247
+
248
+ def allow_cache?
249
+ # Not raising an error here in case the EXT-X-ALLOW-CACHE tag is supported for master-level m3u8s
250
+ @allow_cache.nil? || @allow_cache == "YES"
251
+ end
252
+
253
+ def length
254
+ @line_items.length
255
+ end
256
+
257
+ def media_length
258
+ # Note(bz): Should we be raising errors here? I'm not sure what our standard or the Ruby standard is
259
+ raise NoMethodError, "A master-level manifest cannot access this method" if is_master?
260
+ @line_items.inject(0) { |sum, x| sum + x.duration }
261
+ end
262
+
263
+ def is_live?
264
+ raise NoMethodError, "A master-level manifest cannot access this method" if is_master?
265
+ @is_live
266
+ end
267
+
268
+ def is_master?
269
+ # Note: This currently assumes an empty m3u8 is NOT a master m3u8
270
+ !@line_items.empty? && @line_items[0].instance_of?(HLSPlaylist)
271
+ end
272
+
273
+ def build
274
+ if is_master?
275
+ return <<-EOT.remove_extra_spaces
276
+ #{EXTM3U}
277
+ #{@line_items.map { |item| item.build }.join("\n")}
278
+ EOT
279
+ else
280
+ return <<-EOT.remove_extra_spaces
281
+ #{EXTM3U}
282
+ #{EXT_X_ALLOW_CACHE + ":" + @allow_cache unless @allow_cache.nil?}
283
+ #{EXT_X_MEDIA_SEQUENCE + ":" + @media_sequence.to_s unless @media_sequence.nil?}
284
+ #{EXT_X_PLAYLIST_TYPE + ":" + @playlist_type unless @playlist_type.nil?}
285
+ #{EXT_X_TARGETDURATION + ":" + @target_duration.to_s unless @target_duration.nil?}
286
+ #{EXT_X_VERSION + ":" + @version.to_s unless @version.nil?}
287
+ #{@line_items.map { |item| item.build }.join("\n")}
288
+ #{EXT_X_ENDLIST unless is_live?}
289
+ EOT
290
+ end
291
+ end
292
+
293
+ private
294
+
295
+ # Intelligently split by commas, ignoring any commas between "" or ''
296
+ def parse_csv(value)
297
+ from = 0
298
+ quote = nil
299
+ result = []
300
+ (0...value.length).each do |index|
301
+ char = value[index]
302
+ if char == "," and quote.nil?
303
+ result.push(value[from...index])
304
+ from = index + 1
305
+ elsif ["\"", "'"].include?(char)
306
+ quote = (quote.nil? ? char : (quote == char ? nil : quote))
307
+ end
308
+ end
309
+ result.push(value[from...value.length])
310
+ end
311
+ end
@@ -0,0 +1,193 @@
1
+ require "base64"
2
+ require "net/http"
3
+ require "uri"
4
+
5
+ ####
6
+ #This makes sure require_relative is available in both Ruby 1.8 and 1.9
7
+ ####
8
+
9
+ unless Kernel.respond_to?(:require_relative)
10
+ module Kernel
11
+ def require_relative(path)
12
+ require File.join(File.dirname(caller[0]), path.to_str)
13
+ end
14
+ end
15
+ end
16
+
17
+ require_relative "hls_constants"
18
+ require_relative "hls_manifest"
19
+ require_relative "warpgate_utils"
20
+
21
+ class M3U8Generator
22
+
23
+ include HLSConstants
24
+
25
+ attr_accessor :old_serving_domain
26
+ attr_accessor :m3u8_serving_domain
27
+ attr_accessor :is_serving_domain_provider_default
28
+
29
+ def build_top_level_m3u8(embed_code,
30
+ streams,
31
+ min_bitrate=-1,
32
+ max_bitrate=1000000000,
33
+ target_bitrate=nil,
34
+ single_bit_rate=false,
35
+ authentication_params={},
36
+ captions=nil,
37
+ enable_resolution_tag=false)
38
+
39
+ # Check the streams object to verify the right Stream attributes are passed in
40
+ streams.each do |stream|
41
+ stream[:average_video_bitrate]
42
+ stream[:muxing_format]
43
+ stream[:thrift_path]
44
+ stream[:audio_bitrate]
45
+ stream[:video_width]
46
+ stream[:video_height]
47
+ stream[:is_encrypted]
48
+ end
49
+
50
+ file_contents = EXTM3U + "\n"
51
+
52
+ if captions
53
+ captions.each do |caption|
54
+ file_contents << "#{EXT_X_MEDIA}:TYPE=SUBTITLES,GROUP-ID=\"subs\"," +
55
+ "NAME=\"#{caption[:language_name]}\"," +
56
+ "DEFAULT=#{caption[:default] ? 'YES' : 'NO'}," +
57
+ "FORCED=NO," +
58
+ "AUTOSELECT=#{caption[:autoselect] ? 'YES' : 'NO'}," +
59
+ "LANGUAGE=\"#{caption[:language_code]}\"," +
60
+ "URI=\"#{caption[:url]}\"\n"
61
+ end
62
+ end
63
+
64
+ # We might have different resolutions per bitrate, but when generating the top level m3u8, we
65
+ # only care about unique bitrates.
66
+ unique_bitrates = {}
67
+ filtered_streams = []
68
+ streams.each do |stream|
69
+ next if stream[:average_video_bitrate] > max_bitrate || stream[:average_video_bitrate] < min_bitrate
70
+ next if unique_bitrates[stream[:average_video_bitrate]]
71
+ unique_bitrates[stream[:average_video_bitrate]] = true
72
+ filtered_streams << stream
73
+ end
74
+
75
+ # After we filter out streams above max bitrate, let's shuffle the array and put the median first.
76
+ if(target_bitrate)
77
+ final_streams = shuffle_array_min_before_target_first(filtered_streams, target_bitrate)
78
+ else
79
+ final_streams = shuffle_array_with_median_first(filtered_streams)
80
+ #if possible, do not let the first stream be audio only.
81
+ if final_streams != nil && final_streams.length > 1 && final_streams[0][:muxing_format].upcase == 'AAC'
82
+ final_streams[0], final_streams[1] = final_streams[1], final_streams[0]
83
+ end
84
+ end
85
+
86
+ if single_bit_rate
87
+ final_streams = final_streams[0..0]
88
+ end
89
+
90
+ # For Adobe Access content, we have to append a link to the key in the manifest
91
+ streams.each do |stream|
92
+ if stream[:muxing_format].to_s == "TS_FA" || stream[:muxing_format].to_s == "AAC_FA"
93
+ file_contents << (EXT_X_FAXS_CM + ":URI=\"#{@m3u8_serving_domain}/#{embed_code}/#{embed_code}.drmmeta\"\n")
94
+ break
95
+ end
96
+ end
97
+
98
+ # Now build the m3u8 file.
99
+ final_streams.each do |stream|
100
+ total_bitrate = stream[:average_video_bitrate] + stream[:audio_bitrate]
101
+ should_append_resolution = enable_resolution_tag && stream[:muxing_format].upcase != 'AAC'
102
+
103
+ stream_info = "#{EXT_X_STREAM_INF}:PROGRAM-ID=1,BANDWIDTH=#{total_bitrate * 1000}"
104
+ stream_info += ",RESOLUTION=#{stream[:video_width]}x#{stream[:video_height]}" if should_append_resolution
105
+ stream_info += ",CODECS=\"mp4a.40.2,mp4a.40.5\"" if stream[:muxing_format].upcase == 'AAC' #add audio
106
+ stream_info += ",SUBTITLES=\"subs\"" if captions #add captions group
107
+ file_contents << stream_info + "\n"
108
+
109
+ stream_url = ""
110
+ if (@is_serving_domain_provider_default && !stream[:is_encrypted]) || authentication_params['secure_ios_token']
111
+ stream_url = "#{@old_serving_domain}/#{embed_code}_#{stream[:average_video_bitrate]}.m3u8"
112
+ else
113
+ # Prepended embed code is handled by gen_serving_domain
114
+ stream_url = "#{@m3u8_serving_domain}/#{stream[:thrift_path]}/#{stream[:average_video_bitrate]}_" +
115
+ "#{stream[:audio_bitrate]}_#{stream[:video_width]}_#{stream[:video_height]}.m3u8"
116
+ end
117
+ WarpgateUtils.append_params(stream_url, authentication_params)
118
+
119
+ file_contents << "#{stream_url}\n"
120
+ end
121
+ file_contents
122
+ end
123
+
124
+ def build_bitrate_m3u8(chunks, authentication_params={})
125
+ file_contents = ""
126
+ max_chunk_length = 0
127
+ chunks.each do |chunk|
128
+ # Chunk Duration
129
+ file_contents << "#EXTINF:#{chunk[:chunk_length]},\n"
130
+ max_chunk_length = chunk[:chunk_length] if chunk[:chunk_length] > max_chunk_length
131
+
132
+ # Chunk decryption key url
133
+ if chunk[:key_url] && !chunk[:key_url].empty?
134
+ final_auth_url = WarpgateUtils.append_params(chunk[:key_url], authentication_params)
135
+ file_contents << "#EXT-X-KEY:METHOD=AES-128,URI=\"#{chunk[:key_url]}\"\n"
136
+ end
137
+
138
+ # Actual Chunk
139
+ file_contents << "#{chunk[:chunk_url]}\n"
140
+ end
141
+ file_contents << "#{EXT_X_ENDLIST}\n"
142
+ "#EXTM3U\n#EXT-X-TARGETDURATION:#{max_chunk_length}\n#EXT-X-MEDIA-SEQUENCE:0\n" + file_contents
143
+ end
144
+
145
+ def change_sub_m3u8_inside_master_m3u8(original_m3u8_url, sub_m3u8_domain, authentication_params={})
146
+ response = Net::HTTP.get(URI(original_m3u8_url))
147
+ hls_manifest = HLSManifest.new(response)
148
+ hls_manifest.line_items.each do |item|
149
+ next unless defined?(item.location)
150
+ encoded_url = Base64.encode64(item.location).gsub("\n", "")
151
+ new_url = "#{sub_m3u8_domain}?original_url=#{encoded_url}"
152
+ item.location = WarpgateUtils.append_params(new_url, authentication_params)
153
+ end
154
+ hls_manifest.build
155
+ end
156
+
157
+ def append_auth_to_key_url_in_m3u8(original_m3u8_url, authentication_params={})
158
+ response = Net::HTTP.get(URI(original_m3u8_url))
159
+ hls_manifest = HLSManifest.new(response)
160
+ hls_manifest.line_items.each do |item|
161
+ next unless defined?(item.key)
162
+ key = item.key
163
+ WarpgateUtils.append_params(key.uri, authentication_params) if key && key.uri
164
+ end
165
+ hls_manifest.build
166
+ end
167
+
168
+ def shuffle_array_min_before_target_first(array, target)
169
+ return array if array.nil? || array.length < 2
170
+ #assume a sorted array, do a binary search for closest
171
+ min = 0
172
+ max = array.length - 1
173
+ while min < max do
174
+ mid = min + (max - min) / 2
175
+ array[mid][:average_video_bitrate] < target.to_i ? min = mid + 1 : max = mid
176
+ end
177
+ t = array[min][:average_video_bitrate] <= target.to_i ? min : min - 1
178
+ t = t < 0 ? 0 : t
179
+ result = array.clone
180
+ result.delete_at(t)
181
+ result.unshift array[t]
182
+ return result
183
+ end
184
+
185
+ def shuffle_array_with_median_first(array)
186
+ return array if array.nil? || array.length < 3
187
+ median = array[array.length / 2]
188
+ result = array.clone
189
+ result.delete_at(array.length / 2)
190
+ result.unshift median
191
+ return result
192
+ end
193
+ end
@@ -0,0 +1,27 @@
1
+ require "base64"
2
+ require "cgi"
3
+ require "ooyala_encrypt"
4
+ require "openssl"
5
+
6
+ module WarpgateUtils
7
+ def self.hash_to_query_string(params)
8
+ params.map do |key, value|
9
+ "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
10
+ end.join("&")
11
+ end
12
+
13
+ def self.append_params(url, params)
14
+ if url && params && !params.empty?
15
+ url << ((url.index("?") ? "&" : "?") + hash_to_query_string(params))
16
+ end
17
+
18
+ url
19
+ end
20
+
21
+ def self.generate_signature(params, key)
22
+ # TODO(bz): Throw this into ooyala_encrypt gem
23
+ digest = OpenSSL::Digest::SHA256.new
24
+ Base64::urlsafe_encode64(OpenSSL::HMAC.digest(digest, key,
25
+ OoyalaEncrypt::querystring_params_for_signature(params, [])))
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: m3u8_generator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.4
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ryan Chen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: pathological
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Ooyala HLS M3U8 Generator is capable of generating master and slave iOS
31
+ m3u8 files.
32
+ email:
33
+ - ryanc@ooyala.com
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - lib/hls_constants.rb
39
+ - lib/warpgate_utils.rb
40
+ - lib/m3u8_generator.rb
41
+ - lib/hls_manifest.rb
42
+ homepage: http://www.ooyala.com
43
+ licenses: []
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubyforge_project:
62
+ rubygems_version: 1.8.23
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: Ooyala HLS M3U8 Generator
66
+ test_files: []