m3u8 0.4.0 → 0.5.0

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