m3u8 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3b76853323b76f301feb3e6c92d787b8b961d22a
4
- data.tar.gz: 1edfa96734f62f346022af27459486337eeb8e85
3
+ metadata.gz: 3797c5f6de3b71d0ea5974be7aef7dc416cf0977
4
+ data.tar.gz: cf67c30e9e26e611a55d5908de78497453181947
5
5
  SHA512:
6
- metadata.gz: 5f7687b04510afe4354969801d1367b2a7ac403895f42d17a8990a2eb79fbad59a1c07e2a1594976d55656a264ef81d1712ac51038eff8cd2f5b7d03451759df
7
- data.tar.gz: ff17446d9b23074e4db0cf8b6aa735f7f5c76f8e406aaef17172c1ebb83e7ad25a94f9cf9e6ee8bb222f068150bcd89eaa3c1ae826d2c902e11ac756ed42795e
6
+ metadata.gz: 000f3f17395510b0af051895793cb0a48019e5848b5419dc6c771c987f597cc9da68aab90e0ae7eb031fbde014ab032a78361e3ae638c567797ab2cd1acd6b27
7
+ data.tar.gz: b1320f06d435c05b174aa19113fe41bc75b11150f6ca097e6dd38bf518ebe3a94b273e2fd8ae59fc305e361610862a315d0b36a72e6b1948cc435c7dc8412aeb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,5 @@
1
+ ### 0.5.0 (2/10/2015) - BREAKING: renamed PlaylistItem.playlist to PlaylistItem.uri, MediaItem.group to MediaItem.group_id, and PlaylistItem.bitrate to PlaylistItem.bandwidth so attributes more closely match the spec. Added support for EXT-X-I-FRAME-STREAM-INF, EXT-X-I-FRAMES-ONLY, EXT-X-BYTERANGE, and EXT-X-SESSION-DATA.
2
+
1
3
  ### 0.4.0 (1/20/2015) - BREAKING: Playlist.audio is now Playlist.audio_codec, audio now represents the newly supported AUDIO attribute, please update your code accordingly. Added support for all EXT-X-MEDIA attributes as well as the following EXT-X-STREAM-INF attributes: AVERAGE-BANDWIDTH, AUDIO, VIDEO, SUBTILES, and CLOSED-CAPTIONS. This means the library now supports alternate audio / camera angles as well as subtitles and closed captions. The EXT-X-PLAYLIST-TYPE attribute is now supported as Playlist.type. SegmentItems now support comments (Thanks @elsurudo). A bug was also fixed in Reader that prevented reuse of the instance.
2
4
 
3
5
  ### 0.3.2 (1/16/2015) - PROGRAM-ID was removed in protocol version 6, if not provided it will now be omitted. Thanks @elsurudo
data/README.md CHANGED
@@ -24,100 +24,116 @@ Or install it yourself as:
24
24
 
25
25
  $ gem install m3u8
26
26
 
27
- ## Usage (Generation)
28
-
29
-
30
- require 'm3u8'
31
-
32
- #create a master playlist and add child playlists for adaptive bitrate streaming:
33
- playlist = M3u8::Playlist.new
34
- #create a new playlist item with options
35
- options = { program_id: 1, width: 1920, height: 1080, width: 1920, height: 1080,
36
- profile: 'high', level: 4.1, audio_codec: 'aac-lc', bitrate: 540,
37
- playlist: 'test.url' }
38
- item = M3u8::PlaylistItem.new options
39
- playlist.items.push item
40
-
41
- #alternatively you can set codecs rather than having it generated automatically:
42
- options = { program_id: 1, width: 1920, height: 1080, width: 1920, height: 1080,
43
- codecs: 'avc1.66.30,mp4a.40.2', bitrate: 540, playlist: 'test.url' }
44
- item = M3u8::PlaylistItem.new options
27
+ ## Usage (creating playlists)
45
28
 
46
- #create a standard playlist and add MPEG-TS segments:
47
- playlist = M3u8::Playlist.new
48
- #create a new segment item with options
49
- item = M3u8::SegmentItem.new duration: 11, segment: 'test.ts'
50
- playlist.items.push item
51
-
52
- #just get the codec string for custom use
53
- options = { profile: 'baseline', level: 3.0, audio_codec: 'aac-lc' }
54
- codecs = M3u8::Playlist.codecs options
55
- # => "avc1.66.30,mp4a.40.2"
56
-
57
- #specify options for playlist, these are ignored if playlist becomes a master playlist
58
- # (child playlist added):
59
- options = { version: 1, cache: false, target: 12, sequence: 1 }
60
- playlist = M3u8::Playlist.new options
61
-
62
- #You can pass an IO object to the write method
63
- require 'tempfile'
64
- f = Tempfile.new 'test'
65
- playlist.write f
66
-
67
- #You can also access the playlist as a string
68
- playlist.to_s
69
-
70
- #There is a M3u8::Writer class if you want more control over the write process
71
-
72
- #values for :audio_codec (Codec name)
73
- #aac-lc, he-aac, mp3
74
-
75
- #values for :profile (H.264 Profile)
76
- #baseline, main, high
77
-
78
- #values for :level
79
- #3.0, 3.1, 4.0, 4.1
80
-
81
- #not all Levels and Profiles can be combined, consult H.264 documentation
29
+ Create a master playlist and add child playlists for adaptive bitrate streaming:
30
+ ```ruby
31
+ require 'm3u8'
32
+ playlist = M3u8::Playlist.new
33
+ ```
82
34
 
83
- ## Parsing Usage (new in v.2.0)
35
+ Create a new playlist item with options:
36
+ ```ruby
37
+ options = { program_id: 1, width: 1920, height: 1080, width: 1920, height: 1080,
38
+ profile: 'high', level: 4.1, audio_codec: 'aac-lc', bandwidth: 540,
39
+ playlist: 'test.url' }
40
+ item = M3u8::PlaylistItem.new options
41
+ playlist.items.push item
42
+ ```
43
+
44
+ Add alternate audio, camera angles, closed captions and subtitles by creating MediaItem instances and adding them to the Playlist:
84
45
 
85
- file = File.open 'spec/fixtures/master.m3u8'
86
- playlist = M3u8::Playlist.read file
87
- playlist.master?
88
- # => true
46
+ ```ruby
47
+ hash = { type: 'AUDIO', group_id: 'audio-lo', language: 'fre',
48
+ assoc_language: 'spoken', name: 'Francais', autoselect: true,
49
+ default: false, forced: true, uri: 'frelo/prog_index.m3u8' }
50
+ item = M3u8::MediaItem.new(hash)
51
+ playlist.items.push item
52
+ ```
53
+
54
+ Create a standard playlist and add MPEG-TS segments via SegmentItem. You can also specify options for this type of playlist, however these options are ignored if playlist becomes a master playlist (anything but segments added):
55
+ ```ruby
56
+ options = { version: 1, cache: false, target: 12, sequence: 1 }
57
+ playlist = M3u8::Playlist.new options
58
+
59
+ item = M3u8::SegmentItem.new duration: 11, segment: 'test.ts'
60
+ playlist.items.push item
61
+ ```
62
+
63
+ You can pass an IO object to the write method:
64
+ ```ruby
65
+ require 'tempfile'
66
+ f = Tempfile.new 'test'
67
+ playlist.write f
68
+ ```
69
+ You can also access the playlist as a string:
70
+ ```ruby
71
+ playlist.to_s
72
+ ```
73
+ M3u8::Writer is the class that handles generating the playlist output.
89
74
 
90
- #acess items in playlist:
91
- playlist.items
75
+ Alternatively you can set codecs rather than having it generated automatically:
76
+ ```ruby
77
+ options = { program_id: 1, width: 1920, height: 1080, width: 1920, height: 1080,
78
+ codecs: 'avc1.66.30,mp4a.40.2', bandwidth: 540, playlist: 'test.url' }
79
+ item = M3u8::PlaylistItem.new options
80
+ ```
81
+ Just get the codec string for custom use:
82
+ ```ruby
83
+ options = { profile: 'baseline', level: 3.0, audio_codec: 'aac-lc' }
84
+ codecs = M3u8::Playlist.codecs options
85
+ # => "avc1.66.30,mp4a.40.2"
86
+ ```
87
+ Values for audio_codec (codec name): aac-lc, he-aac, mp3
88
+
89
+ Possible values for profile (H.264 Profile): baseline, main, high.
90
+
91
+ Possible values for level (H.264 Level): 3.0, 3.1, 4.0, 4.1.
92
92
 
93
- playlist.items.first
94
- # => #<M3u8::PlaylistItem:0x007fa569bc7698 @program_id="1", @resolution="1920x1080",
95
- # @codecs="avc1.640028,mp4a.40.2", @bandwidth="5042000",
96
- # @playlist="hls/1080-7mbps/1080-7mbps.m3u8">
93
+ Not all Levels and Profiles can be combined, consult H.264 documentation
97
94
 
98
- #create a new playlist item with options
99
- options = { program_id: 1, width: 1920, height: 1080, width: 1920, height: 1080,
100
- profile: 'high', level: 4.1, audio_codec: 'aac-lc', bitrate: 540,
101
- playlist: 'test.url' }
102
- item = M3u8::PlaylistItem.new options
103
- #add it to the top of the playlist
104
- playlist.items.insert 0, item
95
+ ## Parsing Usage
105
96
 
106
- #There is a M3u8::Reader class if you want more control over parsing
107
-
97
+ ```ruby
98
+ file = File.open 'spec/fixtures/master.m3u8'
99
+ playlist = M3u8::Playlist.read file
100
+ playlist.master?
101
+ # => true
102
+ ```
103
+ Acess items in playlist:
104
+ ```ruby
105
+ playlist.items.first
106
+ # => #<M3u8::PlaylistItem:0x007fa569bc7698 @program_id="1", @resolution="1920x1080",
107
+ # @codecs="avc1.640028,mp4a.40.2", @bandwidth="5042000",
108
+ # @playlist="hls/1080-7mbps/1080-7mbps.m3u8">
109
+ ```
110
+ Create a new playlist item with options:
111
+ ```ruby
112
+ options = { program_id: 1, width: 1920, height: 1080, width: 1920, height: 1080,
113
+ profile: 'high', level: 4.1, audio_codec: 'aac-lc', bandwidth: 540,
114
+ playlist: 'test.url' }
115
+ item = M3u8::PlaylistItem.new options
116
+ #add it to the top of the playlist
117
+ playlist.items.insert 0, item
118
+ ```
119
+ M3u8::Reader is the class handles parsing if you want more control over the process.
120
+
108
121
  ## Features
109
122
  * Distinction between segment and master playlists is handled automatically (no need to use a different class).
110
123
  * Automatically generates the audio/video codec string based on names and options you are familar with.
111
124
  * Provides validation of input when adding playlists or segments.
112
125
  * Allows all options to be configured on a playlist (caching, version, etc.)
126
+ * Supports I-Frames (Intra frames) and Byte Ranges in Segments.
127
+ * Supports subtitles, closed captions, alternate audio and video, and comments.
128
+ # Supports Session Data in master playlists.
113
129
  * Can write playlist to an IO object (StringIO/File, etc) or access string via to_s.
114
130
  * Can read playlists into a model (Playlist and Items) from an IO object.
131
+ * Any tag or attribute supported by the object model is supported both parsing and generation of m3u8 playlists.
115
132
 
116
133
  ## Missing (but planned) Features
117
- * Support for encryption keys and DRM.
118
- * Support I-Frame and Byte-Range.
119
- * Validation of all attributes to match the spec completely.
120
- * Support for all additional attributes in the latest version of the spec.
134
+ * Support for encrypted segments.
135
+ * Validation of all attributes and their values to match the rules defined in the spec.
136
+ * Still missing support for a few tags and attributes.
121
137
 
122
138
  ## Contributing
123
139
 
data/lib/m3u8.rb CHANGED
@@ -4,9 +4,15 @@ require 'm3u8/playlist'
4
4
  require 'm3u8/playlist_item'
5
5
  require 'm3u8/segment_item'
6
6
  require 'm3u8/media_item'
7
+ require 'm3u8/session_data_item'
7
8
  require 'm3u8/reader'
8
9
  require 'm3u8/writer'
9
10
  require 'm3u8/error'
10
11
 
11
12
  module M3u8
13
+
14
+ def parse_attributes(line)
15
+ array = line.scan(/([A-z-]+)\s*=\s*("[^"]*"|[^,]*)/)
16
+ Hash[array.map { |key, value| [key, value.gsub('"', '')] }]
17
+ end
12
18
  end
@@ -1,7 +1,7 @@
1
1
  module M3u8
2
2
  # MediaItem represents a set of EXT-X-MEDIA attributes
3
3
  class MediaItem
4
- attr_accessor :type, :group, :language, :assoc_language, :name,
4
+ attr_accessor :type, :group_id, :language, :assoc_language, :name,
5
5
  :autoselect, :default, :uri, :forced
6
6
 
7
7
  def initialize(params = {})
@@ -12,7 +12,7 @@ module M3u8
12
12
 
13
13
  def to_s
14
14
  attributes = [type_format,
15
- group_format,
15
+ group_id_format,
16
16
  language_format,
17
17
  assoc_language_format,
18
18
  name_format,
@@ -29,8 +29,8 @@ module M3u8
29
29
  "TYPE=#{type}"
30
30
  end
31
31
 
32
- def group_format
33
- %(GROUP-ID="#{group}")
32
+ def group_id_format
33
+ %(GROUP-ID="#{group_id}")
34
34
  end
35
35
 
36
36
  def language_format
data/lib/m3u8/playlist.rb CHANGED
@@ -2,7 +2,8 @@ module M3u8
2
2
  # Playlist represents an m3u8 playlist, it can be a master playlist or a set
3
3
  # of media segments
4
4
  class Playlist
5
- attr_accessor :items, :version, :cache, :target, :sequence, :type
5
+ attr_accessor :items, :version, :cache, :target, :sequence, :type,
6
+ :iframes_only
6
7
 
7
8
  def initialize(options = {})
8
9
  assign_options options
@@ -55,7 +56,8 @@ module M3u8
55
56
  version: 3,
56
57
  sequence: 0,
57
58
  cache: true,
58
- target: 10
59
+ target: 10,
60
+ iframes_only: false
59
61
  }.merge options
60
62
 
61
63
  self.version = options[:version]
@@ -63,6 +65,7 @@ module M3u8
63
65
  self.cache = options[:cache]
64
66
  self.target = options[:target]
65
67
  self.type = options[:type]
68
+ self.iframes_only = options[:iframes_only]
66
69
  end
67
70
 
68
71
  def playlist_size
@@ -1,12 +1,14 @@
1
1
  module M3u8
2
- # PlaylistItem represents a set of EXT-X-STREAM-INF attributes
2
+ # PlaylistItem represents a set of EXT-X-STREAM-INF or
3
+ # EXT-X-I-FRAME-STREAM-INF attributes
3
4
  class PlaylistItem
4
- attr_accessor :program_id, :width, :height, :codecs, :bitrate, :playlist,
5
- :audio_codec, :level, :profile, :video, :audio,
6
- :average_bandwidth, :subtitles, :closed_captions
5
+ attr_accessor :program_id, :width, :height, :codecs, :bandwidth,
6
+ :audio_codec, :level, :profile, :video, :audio, :uri,
7
+ :average_bandwidth, :subtitles, :closed_captions, :iframe
7
8
  MISSING_CODEC_MESSAGE = 'Audio or video codec info should be provided.'
8
9
 
9
10
  def initialize(params = {})
11
+ self.iframe = false
10
12
  params.each do |key, value|
11
13
  instance_variable_set("@#{key}", value)
12
14
  end
@@ -36,16 +38,7 @@ module M3u8
36
38
  def to_s
37
39
  validate
38
40
 
39
- attributes = [program_id_format,
40
- resolution_format,
41
- codecs_format,
42
- bandwidth_format,
43
- average_bandwidth_format,
44
- audio_format,
45
- video_format,
46
- subtitles_format,
47
- closed_captions_format].compact.join(',')
48
- "#EXT-X-STREAM-INF:#{attributes}\n#{playlist}"
41
+ m3u8_format
49
42
  end
50
43
 
51
44
  private
@@ -54,6 +47,24 @@ module M3u8
54
47
  fail MissingCodecError, MISSING_CODEC_MESSAGE if codecs.nil?
55
48
  end
56
49
 
50
+ def m3u8_format
51
+ return %(#EXT-X-I-FRAME-STREAM-INF:#{attributes},URI="#{uri}") if iframe
52
+
53
+ "#EXT-X-STREAM-INF:#{attributes}\n#{uri}"
54
+ end
55
+
56
+ def attributes
57
+ [program_id_format,
58
+ resolution_format,
59
+ codecs_format,
60
+ bandwidth_format,
61
+ average_bandwidth_format,
62
+ audio_format,
63
+ video_format,
64
+ subtitles_format,
65
+ closed_captions_format].compact.join(',')
66
+ end
67
+
57
68
  def program_id_format
58
69
  return if program_id.nil?
59
70
  "PROGRAM-ID=#{program_id}"
@@ -69,7 +80,7 @@ module M3u8
69
80
  end
70
81
 
71
82
  def bandwidth_format
72
- "BANDWIDTH=#{bitrate}"
83
+ "BANDWIDTH=#{bandwidth}"
73
84
  end
74
85
 
75
86
  def average_bandwidth_format
data/lib/m3u8/reader.rb CHANGED
@@ -7,28 +7,18 @@ module M3u8
7
7
  SEQUENCE_START = '#EXT-X-MEDIA-SEQUENCE:'
8
8
  CACHE_START = '#EXT-X-ALLOW-CACHE:'
9
9
  TARGET_START = '#EXT-X-TARGETDURATION:'
10
+ IFRAME_START = '#EXT-X-I-FRAMES-ONLY'
10
11
  STREAM_START = '#EXT-X-STREAM-INF:'
12
+ STREAM_IFRAME_START = '#EXT-X-I-FRAME-STREAM-INF:'
11
13
  MEDIA_START = '#EXT-X-MEDIA:'
14
+ SESSION_DATA_START = '#EXT-X-SESSION-DATA:'
12
15
  SEGMENT_START = '#EXTINF:'
13
- # EXT-X-STREAM-INF:
14
- PROGRAM_ID = 'PROGRAM-ID'
16
+ BYTERANGE_START = '#EXT-X-BYTERANGE:'
15
17
  RESOLUTION = 'RESOLUTION'
16
- CODECS = 'CODECS'
17
18
  BANDWIDTH = 'BANDWIDTH'
18
19
  AVERAGE_BANDWIDTH = 'AVERAGE-BANDWIDTH'
19
- VIDEO = 'VIDEO'
20
- AUDIO = 'AUDIO'
21
- SUBTITLES = 'SUBTITLES'
22
- CLOSED_CAPTIONS = 'CLOSED-CAPTIONS'
23
- # EXT-X-MEDIA:
24
- TYPE = 'TYPE'
25
- GROUP_ID = 'GROUP-ID'
26
- LANGUAGE = 'LANGUAGE'
27
- ASSOC_LANGUAGE = 'ASSOC-LANGUAGE'
28
- NAME = 'NAME'
29
20
  AUTOSELECT = 'AUTOSELECT'
30
21
  DEFAULT = 'DEFAULT'
31
- URI = 'URI'
32
22
  FORCED = 'FORCED'
33
23
 
34
24
  def read(input)
@@ -44,6 +34,26 @@ module M3u8
44
34
  def parse_line(line)
45
35
  return if line.start_with? PLAYLIST_START
46
36
 
37
+ if line.start_with? STREAM_START
38
+ parse_stream line
39
+ elsif line.start_with? STREAM_IFRAME_START
40
+ parse_iframe_stream line
41
+ elsif line.start_with? SEGMENT_START
42
+ parse_segment line
43
+ elsif line.start_with? MEDIA_START
44
+ parse_media line
45
+ elsif line.start_with? BYTERANGE_START
46
+ parse_byterange line
47
+ elsif line.start_with? SESSION_DATA_START
48
+ parse_session_data line
49
+ elsif !item.nil? && open
50
+ parse_next_line line
51
+ else
52
+ parse_header line
53
+ end
54
+ end
55
+
56
+ def parse_header(line)
47
57
  if line.start_with? PLAYLIST_TYPE_START
48
58
  parse_playlist_type line
49
59
  elsif line.start_with? VERSION_START
@@ -54,14 +64,8 @@ module M3u8
54
64
  parse_cache line
55
65
  elsif line.start_with? TARGET_START
56
66
  parse_target line
57
- elsif line.start_with? STREAM_START
58
- parse_stream line
59
- elsif line.start_with? SEGMENT_START
60
- parse_segment line
61
- elsif line.start_with? MEDIA_START
62
- parse_media line
63
- elsif !item.nil? && open
64
- parse_value(line)
67
+ elsif line.start_with? IFRAME_START
68
+ playlist.iframes_only = true
65
69
  end
66
70
  end
67
71
 
@@ -82,10 +86,6 @@ module M3u8
82
86
  playlist.cache = parse_yes_no(line)
83
87
  end
84
88
 
85
- def parse_yes_no(string)
86
- string == 'YES' ? true : false
87
- end
88
-
89
89
  def parse_target(line)
90
90
  playlist.target = line.gsub(TARGET_START, '').to_i
91
91
  end
@@ -96,88 +96,117 @@ module M3u8
96
96
 
97
97
  self.item = M3u8::PlaylistItem.new
98
98
  line = line.gsub STREAM_START, ''
99
- attributes = line.scan(/([A-z-]+)\s*=\s*("[^"]*"|[^,]*)/)
99
+ attributes = parse_attributes line
100
+ parse_stream_attributes attributes
101
+ end
102
+
103
+ def parse_iframe_stream(line)
104
+ self.master = true
105
+ self.open = false
106
+
107
+ self.item = M3u8::PlaylistItem.new
108
+ item.iframe = true
109
+ line = line.gsub STREAM_IFRAME_START, ''
110
+ attributes = parse_attributes line
111
+ parse_stream_attributes attributes
112
+ playlist.items.push item
113
+ end
114
+
115
+ def parse_stream_attributes(attributes)
100
116
  attributes.each do |pair|
101
- value = pair[1].gsub("\n", '').gsub('"', '')
102
- case pair[0]
103
- when PROGRAM_ID
104
- item.program_id = value
117
+ name = pair[0]
118
+ value = parse_value pair[1]
119
+ case name
105
120
  when RESOLUTION
106
121
  parse_resolution value
107
- when CODECS
108
- item.codecs = value
109
122
  when BANDWIDTH
110
- item.bitrate = value.to_i
123
+ item.bandwidth = value.to_i
111
124
  when AVERAGE_BANDWIDTH
112
125
  item.average_bandwidth = value.to_i
113
- when AUDIO
114
- item.audio = value
115
- when VIDEO
116
- item.video = value
117
- when SUBTITLES
118
- item.subtitles = value
119
- when CLOSED_CAPTIONS
120
- item.closed_captions = value
126
+ else
127
+ set_value name, value
121
128
  end
122
129
  end
123
130
  end
124
131
 
132
+ def parse_resolution(resolution)
133
+ item.width = resolution.split('x')[0].to_i
134
+ item.height = resolution.split('x')[1].to_i
135
+ end
136
+
125
137
  def parse_segment(line)
138
+ self.item = M3u8::SegmentItem.new
139
+ values = line.gsub(SEGMENT_START, '').gsub("\n", ',').split(',')
140
+ item.duration = values[0].to_f
141
+ item.comment = values[1] unless values[1].nil?
142
+
126
143
  self.master = false
127
144
  self.open = true
145
+ end
128
146
 
129
- values = line.gsub(SEGMENT_START, '').gsub("\n", ',').split(',')
147
+ def parse_byterange(line)
148
+ values = line.gsub(BYTERANGE_START, '').gsub("\n", ',').split '@'
149
+ item.byterange_length = values[0].to_i
150
+ item.byterange_start = values[1].to_i unless values[1].nil?
151
+ end
130
152
 
131
- self.item = M3u8::SegmentItem.new
132
- item.duration = values[0].to_f
133
- item.comment = values[1] unless values[1].nil?
153
+ def parse_session_data(line)
154
+ item = M3u8::SessionDataItem.parse line
155
+ playlist.items.push item
134
156
  end
135
157
 
136
158
  def parse_media(line)
137
159
  self.open = false
138
160
  self.item = M3u8::MediaItem.new
139
161
  line = line.gsub MEDIA_START, ''
140
- attributes = line.scan(/([A-z-]+)\s*=\s*("[^"]*"|[^,]*)/)
162
+ attributes = parse_attributes line
163
+ parse_media_attributes attributes
164
+ playlist.items.push item
165
+ end
166
+
167
+ def parse_media_attributes(attributes)
141
168
  attributes.each do |pair|
142
- value = pair[1].gsub("\n", '').gsub('"', '')
143
- case pair[0]
144
- when TYPE
145
- item.type = value
146
- when GROUP_ID
147
- item.group = value
148
- when LANGUAGE
149
- item.language = value
150
- when ASSOC_LANGUAGE
151
- item.assoc_language = value
152
- when NAME
153
- item.name = value
169
+ name = pair[0]
170
+ value = parse_value pair[1]
171
+ case name
154
172
  when AUTOSELECT
155
173
  item.autoselect = parse_yes_no value
156
174
  when DEFAULT
157
175
  item.default = parse_yes_no value
158
- when URI
159
- item.uri = value
160
176
  when FORCED
161
177
  item.forced = parse_yes_no value
178
+ else
179
+ set_value name, value
162
180
  end
163
181
  end
164
- playlist.items.push item
165
- end
166
-
167
- def parse_resolution(resolution)
168
- item.width = resolution.split('x')[0].to_i
169
- item.height = resolution.split('x')[1].to_i
170
182
  end
171
183
 
172
- def parse_value(line)
184
+ def parse_next_line(line)
173
185
  value = line.gsub "\n", ''
174
186
  if master
175
- item.playlist = value
187
+ item.uri = value
176
188
  else
177
189
  item.segment = value
178
190
  end
179
191
  playlist.items.push item
180
192
  self.open = false
181
193
  end
194
+
195
+ def parse_yes_no(string)
196
+ string == 'YES' ? true : false
197
+ end
198
+
199
+ def parse_attributes(line)
200
+ line.scan(/([A-z-]+)\s*=\s*("[^"]*"|[^,]*)/)
201
+ end
202
+
203
+ def parse_value(value)
204
+ value.gsub("\n", '').gsub('"', '')
205
+ end
206
+
207
+ def set_value(name, value)
208
+ name = name.downcase.gsub('-', '_')
209
+ item.instance_variable_set("@#{name}", value)
210
+ end
182
211
  end
183
212
  end
@@ -1,6 +1,7 @@
1
1
  module M3u8
2
2
  class SegmentItem
3
- attr_accessor :duration, :segment, :comment
3
+ attr_accessor :duration, :segment, :comment, :byterange_length,
4
+ :byterange_start
4
5
 
5
6
  def initialize(params = {})
6
7
  params.each do |key, value|
@@ -9,7 +10,19 @@ module M3u8
9
10
  end
10
11
 
11
12
  def to_s
12
- "#EXTINF:#{duration},#{comment}\n#{segment}"
13
+ "#EXTINF:#{duration},#{comment}#{byterange_format}\n#{segment}"
14
+ end
15
+
16
+ private
17
+
18
+ def byterange_format
19
+ return if byterange_length.nil?
20
+ "\n#EXT-X-BYTERANGE:#{byterange_length}#{byterange_start_format}"
21
+ end
22
+
23
+ def byterange_start_format
24
+ return if byterange_start.nil?
25
+ "@#{byterange_start}"
13
26
  end
14
27
  end
15
28
  end
@@ -0,0 +1,52 @@
1
+ module M3u8
2
+ # SessionDataItem represents a set of EXT-X-SESSION-DATA attributes
3
+ class SessionDataItem
4
+ extend M3u8
5
+ attr_accessor :data_id, :value, :uri, :language
6
+
7
+ def initialize(params = {})
8
+ params.each do |key, value|
9
+ instance_variable_set("@#{key}", value)
10
+ end
11
+ end
12
+
13
+ def self.parse(text)
14
+ attributes = parse_attributes text
15
+ options = { data_id: attributes['DATA-ID'], value: attributes['VALUE'],
16
+ uri: attributes['URI'], language: attributes['LANGUAGE'] }
17
+ M3u8::SessionDataItem.new options
18
+ end
19
+
20
+ def to_s
21
+ attributes = [data_id_format,
22
+ value_format,
23
+ uri_format,
24
+ language_format].compact.join(',')
25
+ "#EXT-X-SESSION-DATA:#{attributes}"
26
+ end
27
+
28
+ private
29
+
30
+ def data_id_format
31
+ %(DATA-ID="#{data_id}")
32
+ end
33
+
34
+ def value_format
35
+ return if value.nil?
36
+
37
+ %(VALUE="#{value}")
38
+ end
39
+
40
+ def uri_format
41
+ return if uri.nil?
42
+
43
+ %(URI="#{uri}")
44
+ end
45
+
46
+ def language_format
47
+ return if language.nil?
48
+
49
+ %(LANGUAGE="#{language}")
50
+ end
51
+ end
52
+ end
data/lib/m3u8/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module M3u8
2
- VERSION = '0.4.0'
2
+ VERSION = '0.5.0'
3
3
  end
data/lib/m3u8/writer.rb CHANGED
@@ -30,6 +30,7 @@ module M3u8
30
30
  def write_header(playlist)
31
31
  io.puts "#EXT-X-PLAYLIST-TYPE:#{playlist.type}" unless playlist.type.nil?
32
32
  io.puts "#EXT-X-VERSION:#{playlist.version}"
33
+ io.puts '#EXT-X-I-FRAMES-ONLY' if playlist.iframes_only
33
34
  io.puts "#EXT-X-MEDIA-SEQUENCE:#{playlist.sequence}"
34
35
  io.puts "#EXT-X-ALLOW-CACHE:#{cache(playlist)}"
35
36
  io.puts "#EXT-X-TARGETDURATION:#{playlist.target}"
@@ -0,0 +1,12 @@
1
+ #EXTM3U
2
+ #EXT-X-VERSION:4
3
+ #EXT-X-I-FRAMES-ONLY
4
+ #EXTINF:4.12,
5
+ #EXT-X-BYTERANGE:9400@376
6
+ segment1.ts
7
+ #EXTINF:3.56,
8
+ #EXT-X-BYTERANGE:7144
9
+ segment1.ts
10
+ #EXTINF:3.82,
11
+ #EXT-X-BYTERANGE:10340@1880
12
+ segment2.ts
@@ -0,0 +1,12 @@
1
+ #EXTM3U
2
+ #EXT-X-STREAM-INF:BANDWIDTH=1280000
3
+ low/audio-video.m3u8
4
+ #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="low/iframe.m3u8"
5
+ #EXT-X-STREAM-INF:BANDWIDTH=2560000
6
+ mid/audio-video.m3u8
7
+ #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8"
8
+ #EXT-X-STREAM-INF:BANDWIDTH=7680000
9
+ hi/audio-video.m3u8
10
+ #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI="hi/iframe.m3u8"
11
+ #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"
12
+ audio-only.m3u8
@@ -0,0 +1,4 @@
1
+ #EXT-X-SESSION-DATA:DATA-ID="com.example.lyrics",URI="lyrics.json"
2
+
3
+ #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example"
4
+ #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="sp",VALUE="Este es un ejemplo"
data/spec/m3u8_spec.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe do
4
+ let(:test_class) { Class.new { extend M3u8 } }
5
+
6
+ it 'should parse attributes to hash' do
7
+ line = %(TEST-ID="Help",URI="http://test",ID=33)
8
+ hash = test_class.parse_attributes line
9
+ expect(hash['TEST-ID']).to eq 'Help'
10
+ expect(hash['URI']).to eq 'http://test'
11
+ expect(hash['ID']).to eq '33'
12
+ end
13
+ end
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe M3u8::MediaItem do
4
4
  it 'should provide m3u8 format representation' do
5
- hash = { type: 'AUDIO', group: 'audio-lo', language: 'fre',
5
+ hash = { type: 'AUDIO', group_id: 'audio-lo', language: 'fre',
6
6
  name: 'Francais', autoselect: true, default: false,
7
7
  uri: 'frelo/prog_index.m3u8' }
8
8
  item = M3u8::MediaItem.new(hash)
@@ -12,7 +12,7 @@ describe M3u8::MediaItem do
12
12
  'URI="frelo/prog_index.m3u8"'
13
13
  expect(output).to eq expected
14
14
 
15
- hash = { type: 'AUDIO', group: 'audio-lo', language: 'fre',
15
+ hash = { type: 'AUDIO', group_id: 'audio-lo', language: 'fre',
16
16
  assoc_language: 'spoken', name: 'Francais', autoselect: true,
17
17
  default: false, forced: true, uri: 'frelo/prog_index.m3u8' }
18
18
  item = M3u8::MediaItem.new(hash)
@@ -3,34 +3,36 @@ require 'spec_helper'
3
3
  describe M3u8::PlaylistItem do
4
4
  it 'should initialize with hash' do
5
5
  hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
6
- bitrate: 540, playlist: 'test.url' }
6
+ bandwidth: 540, uri: 'test.url' }
7
7
  item = M3u8::PlaylistItem.new(hash)
8
8
  expect(item.program_id).to eq 1
9
9
  expect(item.width).to eq 1920
10
10
  expect(item.height).to eq 1080
11
11
  expect(item.resolution).to eq '1920x1080'
12
12
  expect(item.codecs).to eq 'avc'
13
- expect(item.bitrate).to eq 540
14
- expect(item.playlist).to eq 'test.url'
13
+ expect(item.bandwidth).to eq 540
14
+ expect(item.uri).to eq 'test.url'
15
+ expect(item.iframe).to be false
15
16
  end
16
17
 
17
18
  it 'should provide m3u8 format representation' do
18
19
  hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
19
- bitrate: 540, playlist: 'test.url' }
20
+ bandwidth: 540, uri: 'test.url' }
20
21
  item = M3u8::PlaylistItem.new(hash)
21
22
  output = item.to_s
22
23
  expected = '#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,' +
23
24
  %(CODECS="avc",BANDWIDTH=540\ntest.url)
24
25
  expect(output).to eq expected
25
26
 
26
- hash = { program_id: 1, codecs: 'avc', bitrate: 540, playlist: 'test.url' }
27
+ hash = { program_id: 1, codecs: 'avc', bandwidth: 540,
28
+ uri: 'test.url' }
27
29
  item = M3u8::PlaylistItem.new(hash)
28
30
  output = item.to_s
29
31
  expected = '#EXT-X-STREAM-INF:PROGRAM-ID=1,' +
30
32
  %(CODECS="avc",BANDWIDTH=540\ntest.url)
31
33
  expect(output).to eq expected
32
34
 
33
- hash = { codecs: 'avc', bitrate: 540, playlist: 'test.url', audio: 'test',
35
+ hash = { codecs: 'avc', bandwidth: 540, uri: 'test.url', audio: 'test',
34
36
  video: 'test2', average_bandwidth: 550, subtitles: 'subs',
35
37
  closed_captions: 'caps' }
36
38
  item = M3u8::PlaylistItem.new(hash)
@@ -41,6 +43,16 @@ describe M3u8::PlaylistItem do
41
43
  expect(output).to eq expected
42
44
  end
43
45
 
46
+ it 'should provided m3u8 format with I-Frame option' do
47
+ hash = { codecs: 'avc', bandwidth: 540, uri: 'test.url', iframe: true,
48
+ video: 'test2', average_bandwidth: 550 }
49
+ item = M3u8::PlaylistItem.new(hash)
50
+ output = item.to_s
51
+ expected = %(#EXT-X-I-FRAME-STREAM-INF:CODECS="avc",BANDWIDTH=540,) +
52
+ %(AVERAGE-BANDWIDTH=550,VIDEO="test2",URI="test.url")
53
+ expect(output).to eq expected
54
+ end
55
+
44
56
  it 'should generate codecs string' do
45
57
  item = M3u8::PlaylistItem.new
46
58
  expect(item.codecs).to be_nil
@@ -119,7 +131,7 @@ describe M3u8::PlaylistItem do
119
131
  end
120
132
 
121
133
  it 'should raise error if codecs are missing' do
122
- params = { program_id: 1, bitrate: 540, playlist: 'test.url' }
134
+ params = { program_id: 1, bandwidth: 540, uri: 'test.url' }
123
135
  item = M3u8::PlaylistItem.new params
124
136
  message = 'Audio or video codec info should be provided.'
125
137
  expect { item.to_s }.to raise_error(M3u8::MissingCodecError, message)
@@ -8,7 +8,7 @@ describe M3u8::Playlist do
8
8
  end
9
9
 
10
10
  it 'should render master playlist' do
11
- options = { playlist: 'playlist_url', bitrate: 6400,
11
+ options = { uri: 'playlist_url', bandwidth: 6400,
12
12
  audio_codec: 'mp3' }
13
13
  item = M3u8::PlaylistItem.new options
14
14
  playlist = M3u8::Playlist.new
@@ -19,7 +19,7 @@ describe M3u8::Playlist do
19
19
  ",BANDWIDTH=6400\nplaylist_url\n"
20
20
  expect(playlist.to_s).to eq output
21
21
 
22
- options = { program_id: '1', playlist: 'playlist_url', bitrate: 6400,
22
+ options = { program_id: '1', uri: 'playlist_url', bandwidth: 6400,
23
23
  audio_codec: 'mp3' }
24
24
  item = M3u8::PlaylistItem.new options
25
25
  playlist = M3u8::Playlist.new
@@ -30,7 +30,7 @@ describe M3u8::Playlist do
30
30
  ",BANDWIDTH=6400\nplaylist_url\n"
31
31
  expect(playlist.to_s).to eq output
32
32
 
33
- options = { program_id: '2', playlist: 'playlist_url', bitrate: 50_000,
33
+ options = { program_id: '2', uri: 'playlist_url', bandwidth: 50_000,
34
34
  width: 1920, height: 1080, profile: 'high', level: 4.1,
35
35
  audio_codec: 'aac-lc' }
36
36
  item = M3u8::PlaylistItem.new options
@@ -45,11 +45,11 @@ describe M3u8::Playlist do
45
45
  expect(playlist.to_s).to eq output
46
46
 
47
47
  playlist = M3u8::Playlist.new
48
- options = { program_id: '1', playlist: 'playlist_url', bitrate: 6400,
48
+ options = { program_id: '1', uri: 'playlist_url', bandwidth: 6400,
49
49
  audio_codec: 'mp3' }
50
50
  item = M3u8::PlaylistItem.new options
51
51
  playlist.items.push item
52
- options = { program_id: '2', playlist: 'playlist_url', bitrate: 50_000,
52
+ options = { program_id: '2', uri: 'playlist_url', bandwidth: 50_000,
53
53
  width: 1920, height: 1080, profile: 'high', level: 4.1,
54
54
  audio_codec: 'aac-lc' }
55
55
  item = M3u8::PlaylistItem.new options
@@ -118,7 +118,7 @@ describe M3u8::Playlist do
118
118
  it 'should write playlist to io' do
119
119
  test_io = StringIO.new
120
120
  playlist = M3u8::Playlist.new
121
- options = { program_id: '1', playlist: 'playlist_url', bitrate: 6400,
121
+ options = { program_id: '1', uri: 'playlist_url', bandwidth: 6400,
122
122
  audio_codec: 'mp3' }
123
123
  item = M3u8::PlaylistItem.new options
124
124
  playlist.items.push item
@@ -143,7 +143,7 @@ describe M3u8::Playlist do
143
143
  it 'should report if it is a master playlist' do
144
144
  playlist = M3u8::Playlist.new
145
145
  expect(playlist.master?).to be false
146
- options = { program_id: '1', playlist: 'playlist_url', bitrate: 6400,
146
+ options = { program_id: '1', uri: 'playlist_url', bandwidth: 6400,
147
147
  audio_codec: 'mp3' }
148
148
  item = M3u8::PlaylistItem.new options
149
149
  playlist.items.push item
@@ -155,7 +155,7 @@ describe M3u8::Playlist do
155
155
  playlist = M3u8::Playlist.new
156
156
 
157
157
  hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
158
- bitrate: 540, playlist: 'test.url' }
158
+ bandwidth: 540, uri: 'test.url' }
159
159
  item = M3u8::PlaylistItem.new(hash)
160
160
  playlist.items.push item
161
161
 
@@ -174,13 +174,13 @@ describe M3u8::Playlist do
174
174
  expect(playlist.valid?).to be true
175
175
 
176
176
  hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
177
- bitrate: 540, playlist: 'test.url' }
177
+ bandwidth: 540, uri: 'test.url' }
178
178
  item = M3u8::PlaylistItem.new(hash)
179
179
  playlist.items.push item
180
180
  expect(playlist.valid?).to be true
181
181
 
182
182
  hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
183
- bitrate: 540, playlist: 'test.url' }
183
+ bandwidth: 540, uri: 'test.url' }
184
184
  item = M3u8::PlaylistItem.new(hash)
185
185
  playlist.items.push item
186
186
  expect(playlist.valid?).to be true
@@ -201,6 +201,7 @@ describe M3u8::Playlist do
201
201
  expect(playlist.target).to be 12
202
202
  expect(playlist.sequence).to be 1
203
203
  expect(playlist.type).to eq('VOD')
204
+ expect(playlist.iframes_only).to be false
204
205
  end
205
206
 
206
207
  it 'should allow reading of playlists' do
data/spec/reader_spec.rb CHANGED
@@ -9,13 +9,14 @@ describe M3u8::Reader do
9
9
 
10
10
  item = playlist.items[0]
11
11
  expect(item).to be_a(M3u8::PlaylistItem)
12
- expect(item.playlist).to eq('hls/1080-7mbps/1080-7mbps.m3u8')
12
+ expect(item.uri).to eq('hls/1080-7mbps/1080-7mbps.m3u8')
13
13
  expect(item.program_id).to eq('1')
14
14
  expect(item.width).to eq(1920)
15
15
  expect(item.height).to eq(1080)
16
16
  expect(item.resolution).to eq('1920x1080')
17
17
  expect(item.codecs).to eq('avc1.640028,mp4a.40.2')
18
- expect(item.bitrate).to eq(5_042_000)
18
+ expect(item.bandwidth).to eq(5_042_000)
19
+ expect(item.iframe).to be false
19
20
 
20
21
  expect(playlist.items.size).to eq 6
21
22
 
@@ -23,6 +24,21 @@ describe M3u8::Reader do
23
24
  expect(item.resolution).to be_nil
24
25
  end
25
26
 
27
+ it 'should parse master playlist with I-Frames' do
28
+ file = File.open 'spec/fixtures/master_iframes.m3u8'
29
+ reader = M3u8::Reader.new
30
+ playlist = reader.read file
31
+ expect(playlist.master?).to be true
32
+
33
+ expect(playlist.items.size).to eq 7
34
+
35
+ item = playlist.items[1]
36
+ expect(item).to be_a(M3u8::PlaylistItem)
37
+ expect(item.bandwidth).to eq(86_000)
38
+ expect(item.iframe).to be true
39
+ expect(item.uri).to eq 'low/iframe.m3u8'
40
+ end
41
+
26
42
  it 'should parse segment playlist' do
27
43
  file = File.open 'spec/fixtures/playlist.m3u8'
28
44
  reader = M3u8::Reader.new
@@ -42,6 +58,26 @@ describe M3u8::Reader do
42
58
  expect(playlist.items.size).to eq 138
43
59
  end
44
60
 
61
+ it 'should parse I-Frame playlist' do
62
+ file = File.open 'spec/fixtures/iframes.m3u8'
63
+ reader = M3u8::Reader.new
64
+ playlist = reader.read file
65
+
66
+ expect(playlist.iframes_only).to be true
67
+ expect(playlist.items.size).to eq 3
68
+
69
+ item = playlist.items[0]
70
+ expect(item).to be_a(M3u8::SegmentItem)
71
+ expect(item.duration).to eq 4.12
72
+ expect(item.byterange_length).to eq 9400
73
+ expect(item.byterange_start).to eq 376
74
+ expect(item.segment).to eq 'segment1.ts'
75
+
76
+ item = playlist.items[1]
77
+ expect(item.byterange_length).to eq 7144
78
+ expect(item.byterange_start).to be_nil
79
+ end
80
+
45
81
  it 'should parse segment playlist with comments' do
46
82
  file = File.open 'spec/fixtures/playlist_with_comments.m3u8'
47
83
  reader = M3u8::Reader.new
@@ -72,7 +108,7 @@ describe M3u8::Reader do
72
108
  item = playlist.items[0]
73
109
  expect(item).to be_a M3u8::MediaItem
74
110
  expect(item.type).to eq 'AUDIO'
75
- expect(item.group).to eq 'audio-lo'
111
+ expect(item.group_id).to eq 'audio-lo'
76
112
  expect(item.language).to eq 'eng'
77
113
  expect(item.assoc_language).to eq 'spoken'
78
114
  expect(item.name).to eq 'English'
@@ -93,7 +129,7 @@ describe M3u8::Reader do
93
129
  item = playlist.items[1]
94
130
  expect(item).to be_a M3u8::MediaItem
95
131
  expect(item.type).to eq 'VIDEO'
96
- expect(item.group).to eq '200kbs'
132
+ expect(item.group_id).to eq '200kbs'
97
133
  expect(item.language).to be_nil
98
134
  expect(item.name).to eq 'Angle2'
99
135
  expect(item.autoselect).to be true
@@ -120,4 +156,17 @@ describe M3u8::Reader do
120
156
 
121
157
  expect(playlist.items.size).to eq 6
122
158
  end
159
+
160
+ it 'should parse playlist with session data' do
161
+ file = File.open 'spec/fixtures/session_data.m3u8'
162
+ reader = M3u8::Reader.new
163
+ playlist = reader.read file
164
+
165
+ expect(playlist.items.size).to eq 3
166
+
167
+ item = playlist.items[0]
168
+ expect(item).to be_a M3u8::SessionDataItem
169
+ expect(item.data_id).to eq 'com.example.lyrics'
170
+ expect(item.uri).to eq 'lyrics.json'
171
+ end
123
172
  end
@@ -8,9 +8,12 @@ describe M3u8::SegmentItem do
8
8
  expect(item.segment).to eq 'test.ts'
9
9
  expect(item.comment).to be_nil
10
10
 
11
- hash = { duration: 10.991, segment: 'test.ts', comment: 'anything' }
11
+ hash = { duration: 10.991, segment: 'test.ts', comment: 'anything',
12
+ byterange_length: 4500, byterange_start: 600 }
12
13
  item = M3u8::SegmentItem.new(hash)
13
14
  expect(item.duration).to eq 10.991
15
+ expect(item.byterange_length).to eq 4500
16
+ expect(item.byterange_start).to eq 600
14
17
  expect(item.segment).to eq 'test.ts'
15
18
  expect(item.comment).to eq 'anything'
16
19
  end
@@ -27,5 +30,19 @@ describe M3u8::SegmentItem do
27
30
  output = item.to_s
28
31
  expected = "#EXTINF:10.991,anything\ntest.ts"
29
32
  expect(output).to eq expected
33
+
34
+ hash = { duration: 10.991, segment: 'test.ts', comment: 'anything',
35
+ byterange_length: 4500, byterange_start: 600 }
36
+ item = M3u8::SegmentItem.new(hash)
37
+ output = item.to_s
38
+ expected = "#EXTINF:10.991,anything\n#EXT-X-BYTERANGE:4500@600\ntest.ts"
39
+ expect(output).to eq expected
40
+
41
+ hash = { duration: 10.991, segment: 'test.ts', comment: 'anything',
42
+ byterange_length: 4500 }
43
+ item = M3u8::SegmentItem.new(hash)
44
+ output = item.to_s
45
+ expected = "#EXTINF:10.991,anything\n#EXT-X-BYTERANGE:4500\ntest.ts"
46
+ expect(output).to eq expected
30
47
  end
31
48
  end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ describe M3u8::SessionDataItem do
4
+ it 'should initialize with hash' do
5
+ hash = { data_id: 'com.test.movie.title', value: 'Test',
6
+ uri: 'http://test', language: 'en' }
7
+ item = M3u8::SessionDataItem.new(hash)
8
+ expect(item.data_id).to eq 'com.test.movie.title'
9
+ expect(item.value).to eq 'Test'
10
+ expect(item.uri).to eq 'http://test'
11
+ expect(item.language).to eq 'en'
12
+ end
13
+
14
+ it 'should provide m3u8 format representation' do
15
+ hash = { data_id: 'com.test.movie.title', value: 'Test',
16
+ language: 'en' }
17
+ item = M3u8::SessionDataItem.new(hash)
18
+ output = item.to_s
19
+ expected = %(#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",) +
20
+ %(VALUE="Test",LANGUAGE="en")
21
+ expect(output).to eq expected
22
+
23
+ hash = { data_id: 'com.test.movie.title', uri: 'http://test',
24
+ language: 'en' }
25
+ item = M3u8::SessionDataItem.new(hash)
26
+ output = item.to_s
27
+ expected = %(#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",) +
28
+ %(URI="http://test",LANGUAGE="en")
29
+ expect(output).to eq expected
30
+ end
31
+
32
+ it 'should parse m3u8 format into instance' do
33
+ format = %(#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",) +
34
+ %(VALUE="Test",LANGUAGE="en")
35
+ item = M3u8::SessionDataItem.parse format
36
+ expect(item.data_id).to eq 'com.test.movie.title'
37
+ expect(item.value).to eq 'Test'
38
+ expect(item.uri).to be_nil
39
+ expect(item.language).to eq 'en'
40
+
41
+ format = %(#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",) +
42
+ %(URI="http://test",LANGUAGE="en")
43
+ item = M3u8::SessionDataItem.parse format
44
+ expect(item.data_id).to eq 'com.test.movie.title'
45
+ expect(item.value).to be_nil
46
+ expect(item.uri).to eq 'http://test'
47
+ expect(item.language).to eq 'en'
48
+ end
49
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,10 +1,4 @@
1
- require 'm3u8/playlist'
2
- require 'm3u8/playlist_item'
3
- require 'm3u8/segment_item'
4
- require 'm3u8/media_item'
5
- require 'm3u8/reader'
6
- require 'm3u8/writer'
7
- require 'm3u8/error'
1
+ require 'm3u8'
8
2
 
9
3
  require 'coveralls'
10
4
  Coveralls.wear!
data/spec/writer_spec.rb CHANGED
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe M3u8::Writer do
4
4
  it 'should render master playlist' do
5
- options = { playlist: 'playlist_url', bitrate: 6400,
5
+ options = { uri: 'playlist_url', bandwidth: 6400,
6
6
  audio_codec: 'mp3' }
7
7
  item = M3u8::PlaylistItem.new options
8
8
  playlist = M3u8::Playlist.new
@@ -17,7 +17,7 @@ describe M3u8::Writer do
17
17
  writer.write playlist
18
18
  expect(io.string).to eq output
19
19
 
20
- options = { program_id: '1', playlist: 'playlist_url', bitrate: 6400,
20
+ options = { program_id: '1', uri: 'playlist_url', bandwidth: 6400,
21
21
  audio_codec: 'mp3' }
22
22
  item = M3u8::PlaylistItem.new options
23
23
  playlist = M3u8::Playlist.new
@@ -32,7 +32,7 @@ describe M3u8::Writer do
32
32
  writer.write playlist
33
33
  expect(io.string).to eq output
34
34
 
35
- options = { program_id: '2', playlist: 'playlist_url', bitrate: 50_000,
35
+ options = { program_id: '2', uri: 'playlist_url', bandwidth: 50_000,
36
36
  width: 1920, height: 1080, profile: 'high', level: 4.1,
37
37
  audio_codec: 'aac-lc' }
38
38
  item = M3u8::PlaylistItem.new options
@@ -50,21 +50,27 @@ describe M3u8::Writer do
50
50
  expect(io.string).to eq output
51
51
 
52
52
  playlist = M3u8::Playlist.new
53
- options = { program_id: '1', playlist: 'playlist_url', bitrate: 6400,
53
+ options = { program_id: '1', uri: 'playlist_url', bandwidth: 6400,
54
54
  audio_codec: 'mp3' }
55
55
  item = M3u8::PlaylistItem.new options
56
56
  playlist.items.push item
57
- options = { program_id: '2', playlist: 'playlist_url', bitrate: 50_000,
57
+ options = { program_id: '2', uri: 'playlist_url', bandwidth: 50_000,
58
58
  width: 1920, height: 1080, profile: 'high', level: 4.1,
59
59
  audio_codec: 'aac-lc' }
60
60
  item = M3u8::PlaylistItem.new options
61
61
  playlist.items.push item
62
+ options = { data_id: 'com.test.movie.title', value: 'Test',
63
+ uri: 'http://test', language: 'en' }
64
+ item = M3u8::SessionDataItem.new(options)
65
+ playlist.items.push item
62
66
 
63
67
  output = "#EXTM3U\n" +
64
68
  %(#EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.34") +
65
69
  ",BANDWIDTH=6400\nplaylist_url\n#EXT-X-STREAM-INF:PROGRAM-ID=2," +
66
70
  %(RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2") +
67
- ",BANDWIDTH=50000\nplaylist_url\n"
71
+ ",BANDWIDTH=50000\nplaylist_url\n" +
72
+ %(#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",) +
73
+ %(VALUE="Test",URI="http://test",LANGUAGE="en"\n)
68
74
 
69
75
  io = StringIO.open
70
76
  writer = M3u8::Writer.new io
@@ -110,8 +116,8 @@ describe M3u8::Writer do
110
116
  writer.write playlist
111
117
  expect(io.string).to eq output
112
118
 
113
- options = { version: 1, cache: false, target: 12, sequence: 1,
114
- type: 'EVENT' }
119
+ options = { version: 4, cache: false, target: 12, sequence: 1,
120
+ type: 'EVENT', iframes_only: true }
115
121
  playlist = M3u8::Playlist.new options
116
122
  options = { duration: 11.344644, segment: '1080-7mbps00000.ts' }
117
123
  item = M3u8::SegmentItem.new options
@@ -119,7 +125,8 @@ describe M3u8::Writer do
119
125
 
120
126
  output = "#EXTM3U\n" \
121
127
  "#EXT-X-PLAYLIST-TYPE:EVENT\n" \
122
- "#EXT-X-VERSION:1\n" \
128
+ "#EXT-X-VERSION:4\n" \
129
+ "#EXT-X-I-FRAMES-ONLY\n" \
123
130
  "#EXT-X-MEDIA-SEQUENCE:1\n" \
124
131
  "#EXT-X-ALLOW-CACHE:NO\n" \
125
132
  "#EXT-X-TARGETDURATION:12\n" \
@@ -136,7 +143,7 @@ describe M3u8::Writer do
136
143
  playlist = M3u8::Playlist.new
137
144
 
138
145
  hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
139
- bitrate: 540, playlist: 'test.url' }
146
+ bandwidth: 540, playlist: 'test.url' }
140
147
  item = M3u8::PlaylistItem.new(hash)
141
148
  playlist.items.push item
142
149
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: m3u8
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Seth Deckard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-01-20 00:00:00.000000000 Z
11
+ date: 2015-02-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -76,19 +76,25 @@ files:
76
76
  - lib/m3u8/playlist_item.rb
77
77
  - lib/m3u8/reader.rb
78
78
  - lib/m3u8/segment_item.rb
79
+ - lib/m3u8/session_data_item.rb
79
80
  - lib/m3u8/version.rb
80
81
  - lib/m3u8/writer.rb
81
82
  - m3u8.gemspec
83
+ - spec/fixtures/iframes.m3u8
82
84
  - spec/fixtures/master.m3u8
85
+ - spec/fixtures/master_iframes.m3u8
83
86
  - spec/fixtures/playlist.m3u8
84
87
  - spec/fixtures/playlist_with_comments.m3u8
88
+ - spec/fixtures/session_data.m3u8
85
89
  - spec/fixtures/variant_angles.m3u8
86
90
  - spec/fixtures/variant_audio.m3u8
91
+ - spec/m3u8_spec.rb
87
92
  - spec/media_item_spec.rb
88
93
  - spec/playlist_item_spec.rb
89
94
  - spec/playlist_spec.rb
90
95
  - spec/reader_spec.rb
91
96
  - spec/segment_item_spec.rb
97
+ - spec/session_data_item_spec.rb
92
98
  - spec/spec_helper.rb
93
99
  - spec/writer_spec.rb
94
100
  homepage: https://github.com/sethdeckard/m3u8
@@ -116,15 +122,20 @@ signing_key:
116
122
  specification_version: 4
117
123
  summary: Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).
118
124
  test_files:
125
+ - spec/fixtures/iframes.m3u8
119
126
  - spec/fixtures/master.m3u8
127
+ - spec/fixtures/master_iframes.m3u8
120
128
  - spec/fixtures/playlist.m3u8
121
129
  - spec/fixtures/playlist_with_comments.m3u8
130
+ - spec/fixtures/session_data.m3u8
122
131
  - spec/fixtures/variant_angles.m3u8
123
132
  - spec/fixtures/variant_audio.m3u8
133
+ - spec/m3u8_spec.rb
124
134
  - spec/media_item_spec.rb
125
135
  - spec/playlist_item_spec.rb
126
136
  - spec/playlist_spec.rb
127
137
  - spec/reader_spec.rb
128
138
  - spec/segment_item_spec.rb
139
+ - spec/session_data_item_spec.rb
129
140
  - spec/spec_helper.rb
130
141
  - spec/writer_spec.rb