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.
- data/lib/hls_constants.rb +48 -0
- data/lib/hls_manifest.rb +311 -0
- data/lib/m3u8_generator.rb +193 -0
- data/lib/warpgate_utils.rb +27 -0
- metadata +66 -0
@@ -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
|
data/lib/hls_manifest.rb
ADDED
@@ -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: []
|