m3u8 0.5.1 → 0.5.2

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.
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