m3u8_generator 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []