m3u8 0.5.1 → 0.5.2

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: 54599784eb3cf885ed988991d50590eb1190b63d
4
- data.tar.gz: ca8ce3a4faf778a961fd937057702ecba30a9905
3
+ metadata.gz: e5c073951f7f46336970cc6f1e16c38e89f4705d
4
+ data.tar.gz: af7de6933beb9ae49f2a88f4aad6124aafb4c88f
5
5
  SHA512:
6
- metadata.gz: db42bd190ed77c6595b8ba4f42ccbdb28a8d42a4e0f0f840ab0d5b2b7fb480fb9e99b3415dbc3a172dd29126c08647ebf2e234c8d0d47d4b56afe0f9c6e34590
7
- data.tar.gz: 5ac8f0fa3f99a83bf088435be2ff0625bc9d5db5f3159f88f575fe8c5d7a6e4561c671ec999c9359533a1c8c69685ce034c606ceae9c61acbc1a751388ff28b9
6
+ metadata.gz: f5440e1466f6f24dcf44f39e03ef44034b7aca088a81e5363e3778472b5d1d3c901d8c6cc40a78efdc9ce8c5354a789843d998606d661d3cf9f478798fd639f2
7
+ data.tar.gz: c02a202da39fd3d5611dcb3a059e56d2f64f0b036622f364fc21d27cfbf55d673fc8fc4c7ab967f4363024de3267b34f1cb916822468ff1e662e78ae8d649237
@@ -1,3 +1,5 @@
1
+ #### 0.5.2 (2/18/2015) - Fix issue where PlaylistItem.average_bandwidth would default to 0 instead of nil when not present in m3u8 being parsed.
2
+
1
3
  #### 0.5.1 (2/16/2015) - [takashisite](https://github.com/takashisite) added support for [EXT-X-DISCONTINUITY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-14#section-4.3.2.3). Added support for [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-14#section-4.3.2.4) (keys for encrypted segments).
2
4
  ***
3
5
  #### 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.
@@ -14,7 +14,11 @@ require 'm3u8/error'
14
14
  # M3u8 provides parsing, generation, and validation of m3u8 playlists
15
15
  module M3u8
16
16
  def parse_attributes(line)
17
- array = line.scan(/([A-z-]+)\s*=\s*("[^"]*"|[^,]*)/)
17
+ array = line.gsub("\n", '').scan(/([A-z-]+)\s*=\s*("[^"]*"|[^,]*)/)
18
18
  Hash[array.map { |key, value| [key, value.gsub('"', '')] }]
19
19
  end
20
+
21
+ def parse_yes_no(value)
22
+ value == 'YES' ? true : false
23
+ end
20
24
  end
@@ -1,6 +1,7 @@
1
1
  module M3u8
2
2
  # MediaItem represents a set of EXT-X-MEDIA attributes
3
3
  class MediaItem
4
+ include M3u8
4
5
  attr_accessor :type, :group_id, :language, :assoc_language, :name,
5
6
  :autoselect, :default, :uri, :forced
6
7
 
@@ -10,6 +11,19 @@ module M3u8
10
11
  end
11
12
  end
12
13
 
14
+ def parse(text)
15
+ attributes = parse_attributes text
16
+ options = { type: attributes['TYPE'], group_id: attributes['GROUP-ID'],
17
+ language: attributes['LANGUAGE'],
18
+ assoc_language: attributes['ASSOC-LANGUAGE'],
19
+ name: attributes['NAME'],
20
+ autoselect: parse_yes_no(attributes['AUTOSELECT']),
21
+ default: parse_yes_no(attributes['DEFAULT']),
22
+ forced: parse_yes_no(attributes['FORCED']),
23
+ uri: attributes['URI'] }
24
+ initialize options
25
+ end
26
+
13
27
  def to_s
14
28
  attributes = [type_format,
15
29
  group_id_format,
@@ -2,6 +2,7 @@ module M3u8
2
2
  # PlaylistItem represents a set of EXT-X-STREAM-INF or
3
3
  # EXT-X-I-FRAME-STREAM-INF attributes
4
4
  class PlaylistItem
5
+ include M3u8
5
6
  attr_accessor :program_id, :width, :height, :codecs, :bandwidth,
6
7
  :audio_codec, :level, :profile, :video, :audio, :uri,
7
8
  :average_bandwidth, :subtitles, :closed_captions, :iframe
@@ -14,6 +15,20 @@ module M3u8
14
15
  end
15
16
  end
16
17
 
18
+ def parse(text)
19
+ attributes = parse_attributes text
20
+ average = parse_average_bandwidth attributes['AVERAGE-BANDWIDTH']
21
+ options = { program_id: attributes['PROGRAM-ID'],
22
+ codecs: attributes['CODECS'],
23
+ bandwidth: attributes['BANDWIDTH'].to_i,
24
+ average_bandwidth: average,
25
+ video: attributes['VIDEO'], audio: attributes['AUDIO'],
26
+ uri: attributes['URI'], subtitles: attributes['SUBTITLES'],
27
+ closed_captions: attributes['CLOSED-CAPTIONS'] }
28
+ initialize options
29
+ parse_resolution attributes['RESOLUTION']
30
+ end
31
+
17
32
  def resolution
18
33
  return if width.nil?
19
34
  "#{width}x#{height}"
@@ -43,6 +58,16 @@ module M3u8
43
58
 
44
59
  private
45
60
 
61
+ def parse_average_bandwidth(value)
62
+ value.to_i unless value.nil?
63
+ end
64
+
65
+ def parse_resolution(resolution)
66
+ return if resolution.nil?
67
+ self.width = resolution.split('x')[0].to_i
68
+ self.height = resolution.split('x')[1].to_i
69
+ end
70
+
46
71
  def validate
47
72
  fail MissingCodecError, MISSING_CODEC_MESSAGE if codecs.nil?
48
73
  end
@@ -1,28 +1,15 @@
1
1
  module M3u8
2
2
  # Reader provides parsing of m3u8 playlists
3
3
  class Reader
4
- attr_accessor :playlist, :item, :open, :master
5
- PLAYLIST_START = '#EXTM3U'
6
- PLAYLIST_TYPE_START = '#EXT-X-PLAYLIST-TYPE:'
7
- VERSION_START = '#EXT-X-VERSION:'
8
- SEQUENCE_START = '#EXT-X-MEDIA-SEQUENCE:'
9
- CACHE_START = '#EXT-X-ALLOW-CACHE:'
10
- TARGET_START = '#EXT-X-TARGETDURATION:'
11
- IFRAME_START = '#EXT-X-I-FRAMES-ONLY'
12
- STREAM_START = '#EXT-X-STREAM-INF:'
13
- STREAM_IFRAME_START = '#EXT-X-I-FRAME-STREAM-INF:'
14
- MEDIA_START = '#EXT-X-MEDIA:'
15
- SESSION_DATA_START = '#EXT-X-SESSION-DATA:'
16
- KEY_START = '#EXT-X-KEY:'
17
- SEGMENT_START = '#EXTINF:'
18
- SEGMENT_DISCONTINUITY_TAG_START = '#EXT-X-DISCONTINUITY'
19
- BYTERANGE_START = '#EXT-X-BYTERANGE:'
20
- RESOLUTION = 'RESOLUTION'
21
- BANDWIDTH = 'BANDWIDTH'
22
- AVERAGE_BANDWIDTH = 'AVERAGE-BANDWIDTH'
23
- AUTOSELECT = 'AUTOSELECT'
24
- DEFAULT = 'DEFAULT'
25
- FORCED = 'FORCED'
4
+ include M3u8
5
+ attr_accessor :playlist, :item, :open, :master, :tags
6
+
7
+ def initialize(*)
8
+ @tags = [basic_tags,
9
+ media_segment_tags,
10
+ media_playlist_tags,
11
+ master_playlist_tags].inject(:merge)
12
+ end
26
13
 
27
14
  def read(input)
28
15
  self.playlist = Playlist.new
@@ -34,79 +21,64 @@ module M3u8
34
21
 
35
22
  private
36
23
 
37
- def parse_line(line)
38
- return if line.start_with? PLAYLIST_START
39
- return if parse_master_playlist_tags line
40
- return if parse_segment_tags line
41
- return if parse_header_tags line
42
- parse_next_line line if !item.nil? && open
24
+ def basic_tags
25
+ { '#EXT-X-VERSION' => proc { |line| parse_version line } }
43
26
  end
44
27
 
45
- def parse_header_tags(line)
46
- if line.start_with? PLAYLIST_TYPE_START
47
- parse_playlist_type line
48
- elsif line.start_with? VERSION_START
49
- parse_version line
50
- elsif line.start_with? SEQUENCE_START
51
- parse_sequence line
52
- elsif line.start_with? CACHE_START
53
- parse_cache line
54
- elsif line.start_with? TARGET_START
55
- parse_target line
56
- elsif line.start_with? IFRAME_START
57
- playlist.iframes_only = true
58
- else
59
- return false
60
- end
28
+ def media_segment_tags
29
+ { '#EXTINF' => proc { |line| parse_segment line },
30
+ '#EXT-X-DISCONTINUITY' => proc { |line| parse_discontinuity line },
31
+ '#EXT-X-BYTERANGE' => proc { |line| parse_byterange line },
32
+ '#EXT-X-KEY' => proc { |line| parse_key line }
33
+ }
61
34
  end
62
35
 
63
- def parse_master_playlist_tags(line)
64
- if line.start_with? STREAM_START
65
- parse_stream line
66
- elsif line.start_with? STREAM_IFRAME_START
67
- parse_iframe_stream line
68
- elsif line.start_with? MEDIA_START
69
- parse_media line
70
- elsif line.start_with? SEGMENT_DISCONTINUITY_TAG_START
71
- parse_segment_discontinuity_tag line
72
- elsif line.start_with? SESSION_DATA_START
73
- parse_session_data line
74
- else
75
- return false
76
- end
36
+ def media_playlist_tags
37
+ { '#EXT-X-MEDIA-SEQUENCE' => proc { |line| parse_sequence line },
38
+ '#EXT-X-ALLOW-CACHE' => proc { |line| parse_cache line },
39
+ '#EXT-X-TARGETDURATION' => proc { |line| parse_target line },
40
+ '#EXT-X-I-FRAMES-ONLY' => proc { playlist.iframes_only = true },
41
+ '#EXT-X-PLAYLIST-TYPE' => proc { |line| parse_playlist_type line }
42
+ }
77
43
  end
78
44
 
79
- def parse_segment_tags(line)
80
- if line.start_with? KEY_START
81
- parse_key line
82
- elsif line.start_with? SEGMENT_START
83
- parse_segment line
84
- elsif line.start_with? BYTERANGE_START
85
- parse_byterange line
86
- else
87
- return false
88
- end
45
+ def master_playlist_tags
46
+ { '#EXT-X-MEDIA' => proc { |line| parse_media line },
47
+ '#EXT-X-SESSION-DATA' => proc { |line| parse_session_data line },
48
+ '#EXT-X-STREAM-INF' => proc { |line| parse_stream line },
49
+ '#EXT-X-I-FRAME-STREAM-INF' => proc { |line| parse_iframe_stream line }
50
+ }
51
+ end
52
+
53
+ def parse_line(line)
54
+ return if match_tag(line)
55
+ parse_next_line line if !item.nil? && open
56
+ end
57
+
58
+ def match_tag(line)
59
+ tag = @tags.select { |key| line.start_with? key }
60
+ tag.values.first.call line unless tag.empty?
89
61
  end
90
62
 
91
63
  def parse_playlist_type(line)
92
- playlist.type = line.gsub(PLAYLIST_TYPE_START, '').delete!("\n")
64
+ playlist.type = line.gsub('#EXT-X-PLAYLIST-TYPE:', '').delete!("\n")
93
65
  end
94
66
 
95
67
  def parse_version(line)
96
- playlist.version = line.gsub(VERSION_START, '').to_i
68
+ playlist.version = line.gsub('#EXT-X-VERSION:', '').to_i
97
69
  end
98
70
 
99
71
  def parse_sequence(line)
100
- playlist.sequence = line.gsub(SEQUENCE_START, '').to_i
72
+ playlist.sequence = line.gsub('#EXT-X-MEDIA-SEQUENCE:', '').to_i
101
73
  end
102
74
 
103
75
  def parse_cache(line)
104
- line = line.gsub(CACHE_START, '')
76
+ line = line.gsub('#EXT-X-ALLOW-CACHE:', '')
105
77
  playlist.cache = parse_yes_no(line)
106
78
  end
107
79
 
108
80
  def parse_target(line)
109
- playlist.target = line.gsub(TARGET_START, '').to_i
81
+ playlist.target = line.gsub('#EXT-X-TARGETDURATION:', '').to_i
110
82
  end
111
83
 
112
84
  def parse_stream(line)
@@ -114,9 +86,7 @@ module M3u8
114
86
  self.open = true
115
87
 
116
88
  self.item = M3u8::PlaylistItem.new
117
- line = line.gsub STREAM_START, ''
118
- attributes = parse_attributes line
119
- parse_stream_attributes attributes
89
+ item.parse line
120
90
  end
121
91
 
122
92
  def parse_iframe_stream(line)
@@ -124,31 +94,12 @@ module M3u8
124
94
  self.open = false
125
95
 
126
96
  self.item = M3u8::PlaylistItem.new
97
+ item.parse line
127
98
  item.iframe = true
128
- line = line.gsub STREAM_IFRAME_START, ''
129
- attributes = parse_attributes line
130
- parse_stream_attributes attributes
131
99
  playlist.items.push item
132
100
  end
133
101
 
134
- def parse_stream_attributes(attributes)
135
- attributes.each do |pair|
136
- name = pair[0]
137
- value = parse_value pair[1]
138
- case name
139
- when RESOLUTION
140
- parse_resolution value
141
- when BANDWIDTH
142
- item.bandwidth = value.to_i
143
- when AVERAGE_BANDWIDTH
144
- item.average_bandwidth = value.to_i
145
- else
146
- set_value name, value
147
- end
148
- end
149
- end
150
-
151
- def parse_segment_discontinuity_tag(*)
102
+ def parse_discontinuity(*)
152
103
  self.master = false
153
104
  self.open = false
154
105
 
@@ -156,11 +107,6 @@ module M3u8
156
107
  playlist.items.push item
157
108
  end
158
109
 
159
- def parse_resolution(resolution)
160
- item.width = resolution.split('x')[0].to_i
161
- item.height = resolution.split('x')[1].to_i
162
- end
163
-
164
110
  def parse_key(line)
165
111
  item = M3u8::KeyItem.parse line
166
112
  playlist.items.push item
@@ -168,7 +114,7 @@ module M3u8
168
114
 
169
115
  def parse_segment(line)
170
116
  self.item = M3u8::SegmentItem.new
171
- values = line.gsub(SEGMENT_START, '').gsub("\n", ',').split(',')
117
+ values = line.gsub('#EXTINF:', '').gsub("\n", ',').split(',')
172
118
  item.duration = values[0].to_f
173
119
  item.comment = values[1] unless values[1].nil?
174
120
 
@@ -177,7 +123,7 @@ module M3u8
177
123
  end
178
124
 
179
125
  def parse_byterange(line)
180
- values = line.gsub(BYTERANGE_START, '').gsub("\n", ',').split '@'
126
+ values = line.gsub('#EXT-X-BYTERANGE:', '').gsub("\n", ',').split '@'
181
127
  item.byterange_length = values[0].to_i
182
128
  item.byterange_start = values[1].to_i unless values[1].nil?
183
129
  end
@@ -190,29 +136,10 @@ module M3u8
190
136
  def parse_media(line)
191
137
  self.open = false
192
138
  self.item = M3u8::MediaItem.new
193
- line = line.gsub MEDIA_START, ''
194
- attributes = parse_attributes line
195
- parse_media_attributes attributes
139
+ item.parse line
196
140
  playlist.items.push item
197
141
  end
198
142
 
199
- def parse_media_attributes(attributes)
200
- attributes.each do |pair|
201
- name = pair[0]
202
- value = parse_value pair[1]
203
- case name
204
- when AUTOSELECT
205
- item.autoselect = parse_yes_no value
206
- when DEFAULT
207
- item.default = parse_yes_no value
208
- when FORCED
209
- item.forced = parse_yes_no value
210
- else
211
- set_value name, value
212
- end
213
- end
214
- end
215
-
216
143
  def parse_next_line(line)
217
144
  value = line.gsub "\n", ''
218
145
  if master
@@ -223,22 +150,5 @@ module M3u8
223
150
  playlist.items.push item
224
151
  self.open = false
225
152
  end
226
-
227
- def parse_yes_no(string)
228
- string == 'YES' ? true : false
229
- end
230
-
231
- def parse_attributes(line)
232
- line.scan(/([A-z-]+)\s*=\s*("[^"]*"|[^,]*)/)
233
- end
234
-
235
- def parse_value(value)
236
- value.gsub("\n", '').gsub('"', '')
237
- end
238
-
239
- def set_value(name, value)
240
- name = name.downcase.gsub('-', '_')
241
- item.instance_variable_set("@#{name}", value)
242
- end
243
153
  end
244
154
  end
@@ -1,4 +1,4 @@
1
1
  # M3u8 provides parsing, generation, and validation of m3u8 playlists
2
2
  module M3u8
3
- VERSION = '0.5.1'
3
+ VERSION = '0.5.2'
4
4
  end
@@ -4,10 +4,17 @@ describe do
4
4
  let(:test_class) { Class.new { extend M3u8 } }
5
5
 
6
6
  it 'should parse attributes to hash' do
7
- line = %(TEST-ID="Help",URI="http://test",ID=33)
7
+ line = %(TEST-ID="Help",URI="http://test",ID=33\n)
8
8
  hash = test_class.parse_attributes line
9
9
  expect(hash['TEST-ID']).to eq 'Help'
10
10
  expect(hash['URI']).to eq 'http://test'
11
11
  expect(hash['ID']).to eq '33'
12
12
  end
13
+
14
+ it 'should parse yes/no string' do
15
+ value = 'YES'
16
+ expect(test_class.parse_yes_no value).to be true
17
+ value = 'NO'
18
+ expect(test_class.parse_yes_no value).to be false
19
+ end
13
20
  end
@@ -22,4 +22,22 @@ describe M3u8::MediaItem do
22
22
  'DEFAULT=NO,URI="frelo/prog_index.m3u8",FORCED=YES'
23
23
  expect(output).to eq expected
24
24
  end
25
+
26
+ it 'should parse m3u8 text into instance' do
27
+ format = '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="fre",' \
28
+ 'ASSOC-LANGUAGE="spoken",NAME="Francais",AUTOSELECT=YES,' +
29
+ %("DEFAULT=NO,URI="frelo/prog_index.m3u8",FORCED=YES\n")
30
+ item = M3u8::MediaItem.new
31
+ item.parse format
32
+
33
+ expect(item.type).to eq 'AUDIO'
34
+ expect(item.group_id).to eq 'audio-lo'
35
+ expect(item.language).to eq 'fre'
36
+ expect(item.assoc_language).to eq 'spoken'
37
+ expect(item.name).to eq 'Francais'
38
+ expect(item.autoselect).to be true
39
+ expect(item.default).to be false
40
+ expect(item.uri).to eq 'frelo/prog_index.m3u8'
41
+ expect(item.forced).to be true
42
+ end
25
43
  end
@@ -15,6 +15,43 @@ describe M3u8::PlaylistItem do
15
15
  expect(item.iframe).to be false
16
16
  end
17
17
 
18
+ it 'should parse m3u8 text into instance' do
19
+ format = %(#EXT-X-STREAM-INF:CODECS="avc",BANDWIDTH=540,) +
20
+ %(PROGRAM-ID=1,RESOLUTION=1920x1080,) +
21
+ %(AVERAGE-BANDWIDTH=550,AUDIO="test",VIDEO="test2",) +
22
+ %(SUBTITLES="subs",CLOSED-CAPTIONS="caps",URI="test.url")
23
+ item = M3u8::PlaylistItem.new
24
+ item.parse(format)
25
+ expect(item.program_id).to eq '1'
26
+ expect(item.codecs).to eq 'avc'
27
+ expect(item.bandwidth).to eq 540
28
+ expect(item.average_bandwidth).to eq 550
29
+ expect(item.width).to eq 1920
30
+ expect(item.height).to eq 1080
31
+ expect(item.audio).to eq 'test'
32
+ expect(item.video).to eq 'test2'
33
+ expect(item.subtitles).to eq 'subs'
34
+ expect(item.closed_captions).to eq 'caps'
35
+ expect(item.uri).to eq 'test.url'
36
+
37
+ format = %(#EXT-X-STREAM-INF:CODECS="avc",BANDWIDTH=540,) +
38
+ %(PROGRAM-ID=1,AUDIO="test",VIDEO="test2",) +
39
+ %(SUBTITLES="subs",CLOSED-CAPTIONS="caps",URI="test.url")
40
+ item = M3u8::PlaylistItem.new
41
+ item.parse(format)
42
+ expect(item.program_id).to eq '1'
43
+ expect(item.codecs).to eq 'avc'
44
+ expect(item.bandwidth).to eq 540
45
+ expect(item.average_bandwidth).to be_nil
46
+ expect(item.width).to be_nil
47
+ expect(item.height).to be_nil
48
+ expect(item.audio).to eq 'test'
49
+ expect(item.video).to eq 'test2'
50
+ expect(item.subtitles).to eq 'subs'
51
+ expect(item.closed_captions).to eq 'caps'
52
+ expect(item.uri).to eq 'test.url'
53
+ end
54
+
18
55
  it 'should provide m3u8 format representation' do
19
56
  hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
20
57
  bandwidth: 540, uri: 'test.url' }
@@ -17,6 +17,7 @@ describe M3u8::Reader do
17
17
  expect(item.codecs).to eq('avc1.640028,mp4a.40.2')
18
18
  expect(item.bandwidth).to eq(5_042_000)
19
19
  expect(item.iframe).to be false
20
+ expect(item.average_bandwidth).to be_nil
20
21
 
21
22
  expect(playlist.items.size).to eq 6
22
23
 
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.5.1
4
+ version: 0.5.2
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-02-16 00:00:00.000000000 Z
11
+ date: 2015-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler