m3u8 0.1.3 → 0.2.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: 9fca5696b131d1b9c9eda4235a34cf23353716d8
4
- data.tar.gz: 0582cd52fb35119e458c373f6163be4d0eb8e4b0
3
+ metadata.gz: 862441ecee4bbc8fe057efdefe16649ba31b6276
4
+ data.tar.gz: 2f9995c6089dcb771fc6a6781936952b4ad03200
5
5
  SHA512:
6
- metadata.gz: d5b4df0be5dfea3e309644f28d2e00cd26ef9b92a9a018ff0e7714086f2c19d5f16bf40329d8e03938be20a4e829da3edc068aee820b7ab723a41e4defa5c61c
7
- data.tar.gz: 8a137600403c6564591422778cf02cc43d6cb5baa9d51a3dcbce6d526e6fa30ed66f1a185d4523e8585ad362726f3e84a5b89ed4c7b56dd015ad90803c565aa2
6
+ metadata.gz: 94a2443f80f7f7f8d780bdd2d0fbc7ef2b8fe22c79e4753a292e1e48dfa740fd02817774408658adf2545fb82aecd93d56be156a1b0affe18d302376f2ff5394
7
+ data.tar.gz: 278b7d96d867e107d693874a35ee101c10b51800379b261e6d7b7c7728c55fec0b8a37e08af6080a01bd9e303c35edfbaff496d85c0eb1b6f0c1b473368b0bbe
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![security](https://hakiri.io/github/sethdeckard/m3u8/master.svg)](https://hakiri.io/github/sethdeckard/m3u8/master)
7
7
  # m3u8
8
8
 
9
- m3u8 provides generation of m3u8 playlists used the [HTTP Live Streaming](https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008332-CH1-SW1) (HLS) specification created by Apple. This is useful if you wish to generate m3u8 playlists on the fly in your web application (to integrate authentication, do something custom, etc) while of course serving up the actual MPEG transport stream files (.ts) from a CDN. You could also use m3u8 to generate playlist files as part of an encoding pipeline.
9
+ m3u8 provides generation and parsing of m3u8 playlists used the [HTTP Live Streaming](https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008332-CH1-SW1) (HLS) specification created by Apple. This is useful if you wish to generate m3u8 playlists on the fly in your web application (to integrate authentication, do something custom, etc) while of course serving up the actual MPEG transport stream files (.ts) from a CDN. You could also use m3u8 to generate playlist files as part of an encoding pipeline. You can also parse existing playlists, add content to them and generate a new output.
10
10
 
11
11
  ## Installation
12
12
 
@@ -24,7 +24,7 @@ Or install it yourself as:
24
24
 
25
25
  $ gem install m3u8
26
26
 
27
- ## Usage
27
+ ## Usage (Generation)
28
28
 
29
29
 
30
30
  require 'm3u8'
@@ -38,10 +38,14 @@ Or install it yourself as:
38
38
  playlist = M3u8::Playlist.new
39
39
  playlist.add_segment 11.344644, "1080-7mbps00000.ts"
40
40
 
41
+ #you can also add PlaylistItem or SegmentItem instances directly to the array of items in the playlist (new in v.2.0):
42
+ item = SegmentItem.new(duration: 11, segment: 'test.ts')
43
+ playlist.items.push item
44
+
41
45
  #just get the codec string for custom use
42
46
  options = { :profile => 'baseline', :level => 3.0, :audio => 'aac-lc' }
43
47
  codecs = M3u8::Playlist.codecs options
44
- #=> "avc1.66.30,mp4a.40.2"
48
+ => "avc1.66.30,mp4a.40.2"
45
49
 
46
50
  #specify options for playlist, these are ignored if playlist becomes a master playlist (child playlist added):
47
51
  options = { :version => 1, :cache => false, :target => 12, :sequence => 1}
@@ -65,13 +69,32 @@ Or install it yourself as:
65
69
  #3.0, 3.1, 4.0, 4.1
66
70
 
67
71
  #not all Levels and Profiles can be combined, consult H.264 documentation
72
+
73
+ ## Parsing Usage (new in v.2.0)
74
+
75
+ file = File.open 'spec/fixtures/master.m3u8'
76
+ reader = M3u8::Reader.new
77
+ playlist = reader.read file
78
+ playlist.master?
79
+ => true
80
+
81
+ #acess items in playlist:
82
+ playlist.items
83
+
84
+ playlist.items.first
85
+ => #<M3u8::PlaylistItem:0x007fa569bc7698 @program_id="1", @resolution="1920x1080", @codecs="avc1.640028,mp4a.40.2", @bandwidth="5042000", @playlist="hls/1080-7mbps/1080-7mbps.m3u8">
68
86
 
69
87
  ## Features
70
- * Distinction between segment and master playlists are handled automatically (no need to use a different class)
88
+ * Distinction between segment and master playlists is handled automatically (no need to use a different class).
71
89
  * Automatically generates the audio/video codec string based on names and options you are familar with.
72
90
  * Provides validation of input when adding playlists or segments.
73
91
  * Allows all options to be configured on a playlist (caching, version, etc.)
74
92
  * Can write playlist to an IO object (StringIO/File, etc) or access string via to_s.
93
+ * Can read playlists into a model (Playlist and Items) from an IO object.
94
+
95
+ ## Missing (but planned) Features
96
+ * Support for encryption keys and DRM.
97
+ * Support for more obscure tags.
75
98
 
76
99
  ## Contributing
77
100
 
@@ -1,6 +1,9 @@
1
1
  require 'stringio'
2
2
  require 'm3u8/version'
3
3
  require 'm3u8/playlist'
4
+ require 'm3u8/playlist_item'
5
+ require 'm3u8/segment_item'
6
+ require 'm3u8/reader'
4
7
  require 'm3u8/error'
5
8
 
6
9
  module M3u8
@@ -1,25 +1,30 @@
1
1
  module M3u8
2
2
  class Playlist
3
- attr_accessor :io, :options, :header, :empty, :master
3
+ attr_accessor :items, :version, :cache, :target, :sequence
4
4
  MISSING_CODEC_MESSAGE = 'An audio or video codec should be provided.'
5
5
  NON_MASTER_ERROR_MESSAGE = 'Playlist is not a master playlist, playlist' \
6
6
  ' can not be added.'
7
7
  MASTER_ERROR_MESSAGE = 'Playlist is a master playlist, segment can not ' \
8
8
  'be added.'
9
+ MIXED_TYPE_ERROR_MESSAGE = 'Playlist contains mixed types of items'
9
10
 
10
11
  def initialize(options = {})
11
- self.options = {
12
+ assign_options options
13
+ self.items = []
14
+ end
15
+
16
+ def assign_options(options)
17
+ options = {
12
18
  version: 3,
13
19
  sequence: 0,
14
20
  cache: true,
15
21
  target: 10
16
22
  }.merge options
17
23
 
18
- self.header = false
19
- self.empty = true
20
- self.master = nil
21
- self.io = StringIO.open
22
- io.puts '#EXTM3U'
24
+ self.version = options[:version]
25
+ self.sequence = options[:sequence]
26
+ self.cache = options[:cache]
27
+ self.target = options[:target]
23
28
  end
24
29
 
25
30
  def self.codecs(options = {})
@@ -37,30 +42,24 @@ module M3u8
37
42
  }.merge options
38
43
 
39
44
  validate_playlist_type true
40
- self.master = true
41
- self.empty = false
42
45
 
43
- resolution = resolution options[:width], options[:height]
44
46
  codecs = codecs(audio: options[:audio], profile: options[:profile],
45
47
  level: options[:level])
46
48
  fail MissingCodecError, MISSING_CODEC_MESSAGE if codecs.nil?
47
- io.puts "#EXT-X-STREAM-INF:PROGRAM-ID=#{program_id},#{resolution}" +
48
- %Q{CODECS="#{codecs}",BANDWIDTH=#{bitrate}}
49
- io.puts playlist
49
+
50
+ params = { program_id: program_id, playlist: playlist, bitrate: bitrate,
51
+ width: options[:width], height: options[:height],
52
+ codecs: codecs }
53
+ item = PlaylistItem.new params
54
+ items.push item
50
55
  end
51
56
 
52
57
  def add_segment(duration, segment)
53
58
  validate_playlist_type false
54
- self.master = false
55
- self.empty = false
56
-
57
- unless header
58
- write_header
59
- self.header = true
60
- end
61
59
 
62
- io.puts "#EXTINF:#{duration},"
63
- io.puts segment
60
+ params = { duration: duration, segment: segment }
61
+ item = SegmentItem.new params
62
+ items.push item
64
63
  end
65
64
 
66
65
  def codecs(options = {})
@@ -85,26 +84,44 @@ module M3u8
85
84
  end
86
85
 
87
86
  def write(output)
88
- output.puts to_s
87
+ validate
88
+
89
+ output.puts '#EXTM3U'
90
+ write_header(output) unless master?
91
+
92
+ items.each do |item|
93
+ output.puts item.to_s
94
+ end
95
+
96
+ return if master?
97
+ output.puts '#EXT-X-ENDLIST'
89
98
  end
90
99
 
91
100
  def master?
92
- return false if empty
93
- master
101
+ return false if playlist_size == 0 && segment_size == 0
102
+ playlist_size > 0
94
103
  end
95
104
 
96
105
  def to_s
97
- if master?
98
- io.string
99
- else
100
- "#{io.string}#EXT-X-ENDLIST"
101
- end
106
+ output = StringIO.open
107
+ write output
108
+ output.string
109
+ end
110
+
111
+ def valid?
112
+ return false if playlist_size > 0 && segment_size > 0
113
+ true
102
114
  end
103
115
 
104
116
  private
105
117
 
118
+ def validate
119
+ return if valid?
120
+ fail PlaylistTypeError, MIXED_TYPE_ERROR_MESSAGE
121
+ end
122
+
106
123
  def validate_playlist_type(master)
107
- return if empty
124
+ return if items.size == 0
108
125
  if master && !master?
109
126
  fail PlaylistTypeError, NON_MASTER_ERROR_MESSAGE
110
127
  elsif !master && master?
@@ -112,15 +129,23 @@ module M3u8
112
129
  end
113
130
  end
114
131
 
115
- def write_header
116
- io.puts "#EXT-X-VERSION:#{options[:version]}"
117
- io.puts "#EXT-X-MEDIA-SEQUENCE:#{options[:sequence]}"
118
- io.puts "#EXT-X-ALLOW-CACHE:#{cache_string}"
119
- io.puts "#EXT-X-TARGETDURATION:#{options[:target]}"
132
+ def playlist_size
133
+ items.select { |item| item.is_a?(PlaylistItem) }.size
134
+ end
135
+
136
+ def segment_size
137
+ items.select { |item| item.is_a?(SegmentItem) }.size
138
+ end
139
+
140
+ def write_header(output)
141
+ output.puts "#EXT-X-VERSION:#{version}"
142
+ output.puts "#EXT-X-MEDIA-SEQUENCE:#{sequence}"
143
+ output.puts "#EXT-X-ALLOW-CACHE:#{cache_string}"
144
+ output.puts "#EXT-X-TARGETDURATION:#{target}"
120
145
  end
121
146
 
122
147
  def cache_string
123
- options[:cache] ? 'YES' : 'NO'
148
+ cache ? 'YES' : 'NO'
124
149
  end
125
150
 
126
151
  def audio_codec(audio)
@@ -143,10 +168,5 @@ module M3u8
143
168
  return 'avc1.640028' if profile == 'high' &&
144
169
  (level == 4.0 || level == 4.1)
145
170
  end
146
-
147
- def resolution(width, height)
148
- return if width.nil?
149
- "RESOLUTION=#{width}x#{height},"
150
- end
151
171
  end
152
172
  end
@@ -0,0 +1,28 @@
1
+ module M3u8
2
+ class PlaylistItem
3
+ attr_accessor :program_id, :width, :height, :codecs, :bitrate, :playlist
4
+
5
+ def initialize(params = {})
6
+ params.each do |key, value|
7
+ instance_variable_set("@#{key}", value)
8
+ end
9
+ end
10
+
11
+ def resolution
12
+ return if width.nil?
13
+ "#{width}x#{height}"
14
+ end
15
+
16
+ def to_s
17
+ "#EXT-X-STREAM-INF:PROGRAM-ID=#{program_id},#{resolution_format}" +
18
+ %(CODECS="#{codecs}",BANDWIDTH=#{bitrate}\n#{playlist})
19
+ end
20
+
21
+ private
22
+
23
+ def resolution_format
24
+ return if resolution.nil?
25
+ "RESOLUTION=#{resolution},"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,117 @@
1
+ module M3u8
2
+ class Reader
3
+ attr_accessor :playlist, :item, :open, :master
4
+ PLAYLIST_START = '#EXTM3U'
5
+ VERSION_START = '#EXT-X-VERSION:'
6
+ SEQUENCE_START = '#EXT-X-MEDIA-SEQUENCE:'
7
+ CACHE_START = '#EXT-X-ALLOW-CACHE:'
8
+ TARGET_START = '#EXT-X-TARGETDURATION:'
9
+ STREAM_START = '#EXT-X-STREAM-INF:'
10
+ SEGMENT_START = '#EXTINF:'
11
+ PROGRAM_ID = 'PROGRAM-ID'
12
+ RESOLUTION = 'RESOLUTION'
13
+ CODECS = 'CODECS'
14
+ BANDWIDTH = 'BANDWIDTH'
15
+
16
+ def initialize
17
+ self.playlist = Playlist.new
18
+ end
19
+
20
+ def read(input)
21
+ input.each_line do |line|
22
+ parse_line line
23
+ end
24
+ playlist
25
+ end
26
+
27
+ private
28
+
29
+ def parse_line(line)
30
+ return if line.start_with? PLAYLIST_START
31
+
32
+ if line.start_with? VERSION_START
33
+ parse_version line
34
+ elsif line.start_with? SEQUENCE_START
35
+ parse_sequence line
36
+ elsif line.start_with? CACHE_START
37
+ parse_cache line
38
+ elsif line.start_with? TARGET_START
39
+ parse_target line
40
+ elsif line.start_with? STREAM_START
41
+ parse_stream line
42
+ elsif line.start_with? SEGMENT_START
43
+ parse_segment line
44
+ elsif !item.nil? && open
45
+ parse_value(line)
46
+ end
47
+ end
48
+
49
+ def parse_version(line)
50
+ playlist.version = line.gsub(VERSION_START, '').to_i
51
+ end
52
+
53
+ def parse_sequence(line)
54
+ playlist.sequence = line.gsub(SEQUENCE_START, '').to_i
55
+ end
56
+
57
+ def parse_cache(line)
58
+ line = line.gsub(CACHE_START, '')
59
+ playlist.cache = parse_yes_no(line)
60
+ end
61
+
62
+ def parse_yes_no(string)
63
+ string == 'YES' ? true : false
64
+ end
65
+
66
+ def parse_target(line)
67
+ playlist.target = line.gsub(TARGET_START, '').to_i
68
+ end
69
+
70
+ def parse_stream(line)
71
+ self.master = true
72
+ self.open = true
73
+
74
+ self.item = M3u8::PlaylistItem.new
75
+ line = line.gsub STREAM_START, ''
76
+ attributes = line.scan(/([A-z-]+)\s*=\s*("[^"]*"|[^,]*)/)
77
+ attributes.each do |pair|
78
+ value = pair[1].gsub("\n", '').gsub('"', '')
79
+ case pair[0]
80
+ when PROGRAM_ID
81
+ item.program_id = value
82
+ when RESOLUTION
83
+ parse_resolution value
84
+ when CODECS
85
+ item.codecs = value
86
+ when BANDWIDTH
87
+ item.bitrate = value.to_i
88
+ end
89
+ end
90
+ end
91
+
92
+ def parse_segment(line)
93
+ self.master = false
94
+ self.open = true
95
+
96
+ self.item = M3u8::SegmentItem.new
97
+ item.duration = line.gsub(SEGMENT_START, '').gsub("\n", '').gsub(',', '')
98
+ .to_f
99
+ end
100
+
101
+ def parse_resolution(resolution)
102
+ item.width = resolution.split('x')[0].to_i
103
+ item.height = resolution.split('x')[1].to_i
104
+ end
105
+
106
+ def parse_value(line)
107
+ value = line.gsub "\n", ''
108
+ if master
109
+ item.playlist = value
110
+ else
111
+ item.segment = value
112
+ end
113
+ playlist.items.push item
114
+ self.open = false
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,15 @@
1
+ module M3u8
2
+ class SegmentItem
3
+ attr_accessor :duration, :segment
4
+
5
+ def initialize(params = {})
6
+ params.each do |key, value|
7
+ instance_variable_set("@#{key}", value)
8
+ end
9
+ end
10
+
11
+ def to_s
12
+ "#EXTINF:#{duration},\n#{segment}"
13
+ end
14
+ end
15
+ end
@@ -1,3 +1,3 @@
1
1
  module M3u8
2
- VERSION = "0.1.3"
2
+ VERSION = '0.2.0'
3
3
  end
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.version = M3u8::VERSION
9
9
  spec.authors = ["Seth Deckard"]
10
10
  spec.email = ["seth@deckard.me"]
11
- spec.summary = %q{Generate m3u8 playlists for HTTP Live Streaming (HLS).}
12
- spec.description = %q{Generate m3u8 playlists for HTTP Live Streaming (HLS).}
11
+ spec.summary = %q{Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).}
12
+ spec.description = %q{Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).}
13
13
  spec.homepage = "https://github.com/sethdeckard/m3u8"
14
14
  spec.license = "MIT"
15
15
 
@@ -0,0 +1,13 @@
1
+ #EXTM3U
2
+ #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2",BANDWIDTH=5042000
3
+ hls/1080-7mbps/1080-7mbps.m3u8
4
+ #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2",BANDWIDTH=4853000
5
+ hls/1080/1080.m3u8
6
+ #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1280x720,CODECS="avc1.4d001f,mp4a.40.2",BANDWIDTH=2387000
7
+ hls/720/720.m3u8
8
+ #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=896x504,CODECS="avc1.4d001f,mp4a.40.2",BANDWIDTH=1365000
9
+ hls/504/504.m3u8
10
+ #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=640x360,CODECS="avc1.66.30,mp4a.40.2",BANDWIDTH=861000
11
+ hls/360/360.m3u8
12
+ #EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.2",BANDWIDTH=6400
13
+ hls/64k/64k.m3u8
@@ -0,0 +1,282 @@
1
+ #EXTM3U
2
+ #EXT-X-VERSION:4
3
+ #EXT-X-MEDIA-SEQUENCE:1
4
+ #EXT-X-ALLOW-CACHE:NO
5
+ #EXT-X-TARGETDURATION:12
6
+ #EXTINF:11.344644,
7
+ 1080-7mbps00000.ts
8
+ #EXTINF:11.261233,
9
+ 1080-7mbps00001.ts
10
+ #EXTINF:7.507489,
11
+ 1080-7mbps00002.ts
12
+ #EXTINF:11.261233,
13
+ 1080-7mbps00003.ts
14
+ #EXTINF:11.261233,
15
+ 1080-7mbps00004.ts
16
+ #EXTINF:7.507489,
17
+ 1080-7mbps00005.ts
18
+ #EXTINF:11.261233,
19
+ 1080-7mbps00006.ts
20
+ #EXTINF:11.261233,
21
+ 1080-7mbps00007.ts
22
+ #EXTINF:7.507478,
23
+ 1080-7mbps00008.ts
24
+ #EXTINF:11.261233,
25
+ 1080-7mbps00009.ts
26
+ #EXTINF:11.261233,
27
+ 1080-7mbps00010.ts
28
+ #EXTINF:7.507478,
29
+ 1080-7mbps00011.ts
30
+ #EXTINF:11.261233,
31
+ 1080-7mbps00012.ts
32
+ #EXTINF:11.261233,
33
+ 1080-7mbps00013.ts
34
+ #EXTINF:7.507489,
35
+ 1080-7mbps00014.ts
36
+ #EXTINF:11.261233,
37
+ 1080-7mbps00015.ts
38
+ #EXTINF:11.261233,
39
+ 1080-7mbps00016.ts
40
+ #EXTINF:7.507489,
41
+ 1080-7mbps00017.ts
42
+ #EXTINF:11.261233,
43
+ 1080-7mbps00018.ts
44
+ #EXTINF:11.261233,
45
+ 1080-7mbps00019.ts
46
+ #EXTINF:7.507478,
47
+ 1080-7mbps00020.ts
48
+ #EXTINF:11.261233,
49
+ 1080-7mbps00021.ts
50
+ #EXTINF:11.261233,
51
+ 1080-7mbps00022.ts
52
+ #EXTINF:7.507478,
53
+ 1080-7mbps00023.ts
54
+ #EXTINF:11.261233,
55
+ 1080-7mbps00024.ts
56
+ #EXTINF:11.261233,
57
+ 1080-7mbps00025.ts
58
+ #EXTINF:7.507489,
59
+ 1080-7mbps00026.ts
60
+ #EXTINF:11.261233,
61
+ 1080-7mbps00027.ts
62
+ #EXTINF:11.261233,
63
+ 1080-7mbps00028.ts
64
+ #EXTINF:7.507489,
65
+ 1080-7mbps00029.ts
66
+ #EXTINF:11.261233,
67
+ 1080-7mbps00030.ts
68
+ #EXTINF:11.261233,
69
+ 1080-7mbps00031.ts
70
+ #EXTINF:7.507478,
71
+ 1080-7mbps00032.ts
72
+ #EXTINF:11.261233,
73
+ 1080-7mbps00033.ts
74
+ #EXTINF:11.261233,
75
+ 1080-7mbps00034.ts
76
+ #EXTINF:7.507489,
77
+ 1080-7mbps00035.ts
78
+ #EXTINF:11.261233,
79
+ 1080-7mbps00036.ts
80
+ #EXTINF:11.261222,
81
+ 1080-7mbps00037.ts
82
+ #EXTINF:7.507489,
83
+ 1080-7mbps00038.ts
84
+ #EXTINF:11.261233,
85
+ 1080-7mbps00039.ts
86
+ #EXTINF:11.261233,
87
+ 1080-7mbps00040.ts
88
+ #EXTINF:7.507489,
89
+ 1080-7mbps00041.ts
90
+ #EXTINF:11.261233,
91
+ 1080-7mbps00042.ts
92
+ #EXTINF:11.261233,
93
+ 1080-7mbps00043.ts
94
+ #EXTINF:7.507478,
95
+ 1080-7mbps00044.ts
96
+ #EXTINF:11.261233,
97
+ 1080-7mbps00045.ts
98
+ #EXTINF:11.261233,
99
+ 1080-7mbps00046.ts
100
+ #EXTINF:7.507489,
101
+ 1080-7mbps00047.ts
102
+ #EXTINF:11.261233,
103
+ 1080-7mbps00048.ts
104
+ #EXTINF:11.261222,
105
+ 1080-7mbps00049.ts
106
+ #EXTINF:7.507489,
107
+ 1080-7mbps00050.ts
108
+ #EXTINF:11.261233,
109
+ 1080-7mbps00051.ts
110
+ #EXTINF:11.261233,
111
+ 1080-7mbps00052.ts
112
+ #EXTINF:7.507489,
113
+ 1080-7mbps00053.ts
114
+ #EXTINF:11.261233,
115
+ 1080-7mbps00054.ts
116
+ #EXTINF:11.261233,
117
+ 1080-7mbps00055.ts
118
+ #EXTINF:7.507478,
119
+ 1080-7mbps00056.ts
120
+ #EXTINF:11.261233,
121
+ 1080-7mbps00057.ts
122
+ #EXTINF:11.261233,
123
+ 1080-7mbps00058.ts
124
+ #EXTINF:7.507489,
125
+ 1080-7mbps00059.ts
126
+ #EXTINF:11.261233,
127
+ 1080-7mbps00060.ts
128
+ #EXTINF:11.261222,
129
+ 1080-7mbps00061.ts
130
+ #EXTINF:7.507489,
131
+ 1080-7mbps00062.ts
132
+ #EXTINF:11.261233,
133
+ 1080-7mbps00063.ts
134
+ #EXTINF:11.261233,
135
+ 1080-7mbps00064.ts
136
+ #EXTINF:7.507489,
137
+ 1080-7mbps00065.ts
138
+ #EXTINF:11.261233,
139
+ 1080-7mbps00066.ts
140
+ #EXTINF:11.261233,
141
+ 1080-7mbps00067.ts
142
+ #EXTINF:7.507478,
143
+ 1080-7mbps00068.ts
144
+ #EXTINF:11.261233,
145
+ 1080-7mbps00069.ts
146
+ #EXTINF:11.261233,
147
+ 1080-7mbps00070.ts
148
+ #EXTINF:7.507489,
149
+ 1080-7mbps00071.ts
150
+ #EXTINF:11.261233,
151
+ 1080-7mbps00072.ts
152
+ #EXTINF:11.261233,
153
+ 1080-7mbps00073.ts
154
+ #EXTINF:7.507489,
155
+ 1080-7mbps00074.ts
156
+ #EXTINF:11.261222,
157
+ 1080-7mbps00075.ts
158
+ #EXTINF:11.261233,
159
+ 1080-7mbps00076.ts
160
+ #EXTINF:7.507489,
161
+ 1080-7mbps00077.ts
162
+ #EXTINF:11.261233,
163
+ 1080-7mbps00078.ts
164
+ #EXTINF:11.261233,
165
+ 1080-7mbps00079.ts
166
+ #EXTINF:7.507478,
167
+ 1080-7mbps00080.ts
168
+ #EXTINF:11.261233,
169
+ 1080-7mbps00081.ts
170
+ #EXTINF:11.261233,
171
+ 1080-7mbps00082.ts
172
+ #EXTINF:7.507489,
173
+ 1080-7mbps00083.ts
174
+ #EXTINF:11.261233,
175
+ 1080-7mbps00084.ts
176
+ #EXTINF:11.261233,
177
+ 1080-7mbps00085.ts
178
+ #EXTINF:7.507489,
179
+ 1080-7mbps00086.ts
180
+ #EXTINF:11.261222,
181
+ 1080-7mbps00087.ts
182
+ #EXTINF:11.261233,
183
+ 1080-7mbps00088.ts
184
+ #EXTINF:7.507489,
185
+ 1080-7mbps00089.ts
186
+ #EXTINF:11.261233,
187
+ 1080-7mbps00090.ts
188
+ #EXTINF:11.261233,
189
+ 1080-7mbps00091.ts
190
+ #EXTINF:7.507478,
191
+ 1080-7mbps00092.ts
192
+ #EXTINF:11.261233,
193
+ 1080-7mbps00093.ts
194
+ #EXTINF:11.261233,
195
+ 1080-7mbps00094.ts
196
+ #EXTINF:7.507489,
197
+ 1080-7mbps00095.ts
198
+ #EXTINF:11.261233,
199
+ 1080-7mbps00096.ts
200
+ #EXTINF:11.261233,
201
+ 1080-7mbps00097.ts
202
+ #EXTINF:7.507489,
203
+ 1080-7mbps00098.ts
204
+ #EXTINF:11.261222,
205
+ 1080-7mbps00099.ts
206
+ #EXTINF:11.261233,
207
+ 1080-7mbps00100.ts
208
+ #EXTINF:7.507489,
209
+ 1080-7mbps00101.ts
210
+ #EXTINF:11.261233,
211
+ 1080-7mbps00102.ts
212
+ #EXTINF:11.261233,
213
+ 1080-7mbps00103.ts
214
+ #EXTINF:7.507478,
215
+ 1080-7mbps00104.ts
216
+ #EXTINF:11.261233,
217
+ 1080-7mbps00105.ts
218
+ #EXTINF:11.261233,
219
+ 1080-7mbps00106.ts
220
+ #EXTINF:7.507489,
221
+ 1080-7mbps00107.ts
222
+ #EXTINF:11.261233,
223
+ 1080-7mbps00108.ts
224
+ #EXTINF:11.261233,
225
+ 1080-7mbps00109.ts
226
+ #EXTINF:7.507489,
227
+ 1080-7mbps00110.ts
228
+ #EXTINF:11.261233,
229
+ 1080-7mbps00111.ts
230
+ #EXTINF:11.261233,
231
+ 1080-7mbps00112.ts
232
+ #EXTINF:7.507478,
233
+ 1080-7mbps00113.ts
234
+ #EXTINF:11.261233,
235
+ 1080-7mbps00114.ts
236
+ #EXTINF:11.261233,
237
+ 1080-7mbps00115.ts
238
+ #EXTINF:7.507478,
239
+ 1080-7mbps00116.ts
240
+ #EXTINF:11.261233,
241
+ 1080-7mbps00117.ts
242
+ #EXTINF:7.507478,
243
+ 1080-7mbps00118.ts
244
+ #EXTINF:11.261233,
245
+ 1080-7mbps00119.ts
246
+ #EXTINF:11.261233,
247
+ 1080-7mbps00120.ts
248
+ #EXTINF:7.507489,
249
+ 1080-7mbps00121.ts
250
+ #EXTINF:11.261233,
251
+ 1080-7mbps00122.ts
252
+ #EXTINF:11.261233,
253
+ 1080-7mbps00123.ts
254
+ #EXTINF:7.507489,
255
+ 1080-7mbps00124.ts
256
+ #EXTINF:11.261222,
257
+ 1080-7mbps00125.ts
258
+ #EXTINF:11.261233,
259
+ 1080-7mbps00126.ts
260
+ #EXTINF:7.507489,
261
+ 1080-7mbps00127.ts
262
+ #EXTINF:11.261233,
263
+ 1080-7mbps00128.ts
264
+ #EXTINF:11.261233,
265
+ 1080-7mbps00129.ts
266
+ #EXTINF:7.507478,
267
+ 1080-7mbps00130.ts
268
+ #EXTINF:11.261233,
269
+ 1080-7mbps00131.ts
270
+ #EXTINF:11.261233,
271
+ 1080-7mbps00132.ts
272
+ #EXTINF:7.507489,
273
+ 1080-7mbps00133.ts
274
+ #EXTINF:11.261233,
275
+ 1080-7mbps00134.ts
276
+ #EXTINF:11.261233,
277
+ 1080-7mbps00135.ts
278
+ #EXTINF:7.507489,
279
+ 1080-7mbps00136.ts
280
+ #EXTINF:1.793444,
281
+ 1080-7mbps00137.ts
282
+ #EXT-X-ENDLIST
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ describe M3u8::PlaylistItem do
4
+ it 'should initialize with hash' do
5
+ hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
6
+ bitrate: 540, playlist: 'test.url' }
7
+ item = M3u8::PlaylistItem.new(hash)
8
+ expect(item.program_id).to eq 1
9
+ expect(item.width).to eq 1920
10
+ expect(item.height).to eq 1080
11
+ expect(item.resolution).to eq '1920x1080'
12
+ expect(item.codecs).to eq 'avc'
13
+ expect(item.bitrate).to eq 540
14
+ expect(item.playlist).to eq 'test.url'
15
+ end
16
+
17
+ it 'should provide m3u8 format representation' do
18
+ hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
19
+ bitrate: 540, playlist: 'test.url' }
20
+ item = M3u8::PlaylistItem.new(hash)
21
+ output = item.to_s
22
+ expected = '#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,' +
23
+ %(CODECS="avc",BANDWIDTH=540\ntest.url)
24
+ expect(output).to eq expected
25
+
26
+ hash = { program_id: 1, codecs: 'avc', bitrate: 540, playlist: 'test.url' }
27
+ item = M3u8::PlaylistItem.new(hash)
28
+ output = item.to_s
29
+ expected = '#EXT-X-STREAM-INF:PROGRAM-ID=1,' +
30
+ %(CODECS="avc",BANDWIDTH=540\ntest.url)
31
+ expect(output).to eq expected
32
+ end
33
+ end
@@ -123,7 +123,7 @@ describe M3u8::Playlist do
123
123
  "#EXT-X-TARGETDURATION:10\n" \
124
124
  "#EXTINF:11.344644,\n" \
125
125
  "1080-7mbps00000.ts\n" \
126
- '#EXT-X-ENDLIST'
126
+ "#EXT-X-ENDLIST\n"
127
127
 
128
128
  expect(playlist.to_s).to eq output
129
129
 
@@ -138,7 +138,7 @@ describe M3u8::Playlist do
138
138
  "1080-7mbps00000.ts\n" \
139
139
  "#EXTINF:11.261233,\n" \
140
140
  "1080-7mbps00001.ts\n" \
141
- '#EXT-X-ENDLIST'
141
+ "#EXT-X-ENDLIST\n"
142
142
 
143
143
  expect(playlist.to_s).to eq output
144
144
 
@@ -153,7 +153,7 @@ describe M3u8::Playlist do
153
153
  "#EXT-X-TARGETDURATION:12\n" \
154
154
  "#EXTINF:11.344644,\n" \
155
155
  "1080-7mbps00000.ts\n" \
156
- '#EXT-X-ENDLIST'
156
+ "#EXT-X-ENDLIST\n"
157
157
 
158
158
  expect(playlist.to_s).to eq output
159
159
  end
@@ -203,6 +203,47 @@ describe M3u8::Playlist do
203
203
  .to raise_error(M3u8::PlaylistTypeError, message)
204
204
  end
205
205
 
206
+ it 'should raise error on write if item types are mixed' do
207
+ playlist = M3u8::Playlist.new
208
+
209
+ hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
210
+ bitrate: 540, playlist: 'test.url' }
211
+ item = M3u8::PlaylistItem.new(hash)
212
+ playlist.items.push item
213
+
214
+ hash = { duration: 10.991, segment: 'test.ts' }
215
+ item = M3u8::SegmentItem.new(hash)
216
+ playlist.items.push item
217
+
218
+ message = 'Playlist contains mixed types of items'
219
+ io = StringIO.new
220
+ expect { playlist.write io }
221
+ .to raise_error(M3u8::PlaylistTypeError, message)
222
+ end
223
+
224
+ it 'should return valid status' do
225
+ playlist = M3u8::Playlist.new
226
+ expect(playlist.valid?).to be true
227
+
228
+ hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
229
+ bitrate: 540, playlist: 'test.url' }
230
+ item = M3u8::PlaylistItem.new(hash)
231
+ playlist.items.push item
232
+ expect(playlist.valid?).to be true
233
+
234
+ hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
235
+ bitrate: 540, playlist: 'test.url' }
236
+ item = M3u8::PlaylistItem.new(hash)
237
+ playlist.items.push item
238
+ expect(playlist.valid?).to be true
239
+
240
+ hash = { duration: 10.991, segment: 'test.ts' }
241
+ item = M3u8::SegmentItem.new(hash)
242
+ playlist.items.push item
243
+
244
+ expect(playlist.valid?).to be false
245
+ end
246
+
206
247
  it 'should raise error if codecs are missing' do
207
248
  playlist = M3u8::Playlist.new
208
249
  message = 'An audio or video codec should be provided.'
@@ -210,4 +251,12 @@ describe M3u8::Playlist do
210
251
  .to raise_error(M3u8::MissingCodecError, message)
211
252
  end
212
253
 
254
+ it 'should expose options as attributes' do
255
+ options = { version: 1, cache: false, target: 12, sequence: 1 }
256
+ playlist = M3u8::Playlist.new options
257
+ expect(playlist.version).to be 1
258
+ expect(playlist.cache).to be false
259
+ expect(playlist.target).to be 12
260
+ expect(playlist.sequence).to be 1
261
+ end
213
262
  end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe M3u8::Reader do
4
+ it 'should parse master playlist' do
5
+ file = File.open 'spec/fixtures/master.m3u8'
6
+ reader = M3u8::Reader.new
7
+ playlist = reader.read file
8
+ expect(playlist.master?).to be true
9
+
10
+ item = playlist.items[0]
11
+ expect(item).to be_a(M3u8::PlaylistItem)
12
+ expect(item.playlist).to eq('hls/1080-7mbps/1080-7mbps.m3u8')
13
+ expect(item.program_id).to eq('1')
14
+ expect(item.width).to eq(1920)
15
+ expect(item.height).to eq(1080)
16
+ expect(item.resolution).to eq('1920x1080')
17
+ expect(item.codecs).to eq('avc1.640028,mp4a.40.2')
18
+ expect(item.bitrate).to eq(5_042_000)
19
+
20
+ expect(playlist.items.size).to eq 6
21
+
22
+ item = playlist.items.last
23
+ expect(item.resolution).to be nil
24
+ end
25
+
26
+ it 'should parse segment playlist' do
27
+ file = File.open 'spec/fixtures/playlist.m3u8'
28
+ reader = M3u8::Reader.new
29
+ playlist = reader.read file
30
+ expect(playlist.master?).to be false
31
+ expect(playlist.version).to be 4
32
+ expect(playlist.sequence).to be 1
33
+ expect(playlist.cache).to be false
34
+ expect(playlist.target).to be 12
35
+
36
+ item = playlist.items[0]
37
+ expect(item).to be_a(M3u8::SegmentItem)
38
+ expect(item.duration).to eq 11.344644
39
+
40
+ expect(playlist.items.size).to eq 138
41
+ end
42
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe M3u8::SegmentItem do
4
+ it 'should initialize with hash' do
5
+ hash = { duration: 10.991, segment: 'test.ts' }
6
+ item = M3u8::SegmentItem.new(hash)
7
+ expect(item.duration).to eq 10.991
8
+ expect(item.segment).to eq 'test.ts'
9
+ end
10
+
11
+ it 'should provide m3u8 format representation' do
12
+ hash = { duration: 10.991, segment: 'test.ts' }
13
+ item = M3u8::SegmentItem.new(hash)
14
+ output = item.to_s
15
+ expected = "#EXTINF:10.991,\ntest.ts"
16
+ expect(output).to eq expected
17
+ end
18
+ end
@@ -1,4 +1,7 @@
1
1
  require 'm3u8/playlist'
2
+ require 'm3u8/playlist_item'
3
+ require 'm3u8/segment_item'
4
+ require 'm3u8/reader'
2
5
  require 'm3u8/error'
3
6
 
4
7
  require 'coveralls'
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.1.3
4
+ version: 0.2.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: 2014-11-23 00:00:00.000000000 Z
11
+ date: 2014-11-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,7 +52,7 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '2.11'
55
- description: Generate m3u8 playlists for HTTP Live Streaming (HLS).
55
+ description: Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).
56
56
  email:
57
57
  - seth@deckard.me
58
58
  executables: []
@@ -69,9 +69,17 @@ files:
69
69
  - lib/m3u8.rb
70
70
  - lib/m3u8/error.rb
71
71
  - lib/m3u8/playlist.rb
72
+ - lib/m3u8/playlist_item.rb
73
+ - lib/m3u8/reader.rb
74
+ - lib/m3u8/segment_item.rb
72
75
  - lib/m3u8/version.rb
73
76
  - m3u8.gemspec
77
+ - spec/fixtures/master.m3u8
78
+ - spec/fixtures/playlist.m3u8
79
+ - spec/playlist_item_spec.rb
74
80
  - spec/playlist_spec.rb
81
+ - spec/reader_spec.rb
82
+ - spec/segment_item_spec.rb
75
83
  - spec/spec_helper.rb
76
84
  homepage: https://github.com/sethdeckard/m3u8
77
85
  licenses:
@@ -96,7 +104,12 @@ rubyforge_project:
96
104
  rubygems_version: 2.4.4
97
105
  signing_key:
98
106
  specification_version: 4
99
- summary: Generate m3u8 playlists for HTTP Live Streaming (HLS).
107
+ summary: Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).
100
108
  test_files:
109
+ - spec/fixtures/master.m3u8
110
+ - spec/fixtures/playlist.m3u8
111
+ - spec/playlist_item_spec.rb
101
112
  - spec/playlist_spec.rb
113
+ - spec/reader_spec.rb
114
+ - spec/segment_item_spec.rb
102
115
  - spec/spec_helper.rb