m3u8 0.7.1 → 0.8.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: 75266714b1670b4990419287c300fa920604bf1c
4
- data.tar.gz: af15a6e305fcbf82517d0b2858aed3fa848a80b2
3
+ metadata.gz: 266fa2220bfc0c9acdc04144747da7d7a01d9b5f
4
+ data.tar.gz: ed5b06cd8baccf5eec7a4fd592a4715a367fa791
5
5
  SHA512:
6
- metadata.gz: 787197f4b3f4807c17125928e3b976c46f66b914b4bf7566c34e5748dd9200d84492c7fa858874d21cbbb9b7dbbfc9b91f04bac8954760a8e18689514459589e
7
- data.tar.gz: 7d279886bd83eacfcd8e2e71463f6b96075dd5e3ed8d742d3b0c5ee7c0dc97932a7c07943fe5b49bad9e44c6631023f916479c5b0a7936c035d9d37adec79e74
6
+ metadata.gz: 9942efab7468eaa317280e6b465251d972700e43ce3b7b0ca281c4fe3084ad3743acbe24074a324fdaa4f9421be0da732a3128e00311b468caf257814337ba1e
7
+ data.tar.gz: d02d9f3c6853f68557ff89d5a6a7d3d4f3a2c77a5d1ea535fa3e5c89911ec0c724e8646dfaa2da6f951a8672494fb32cfc1b9dd0502da95d29ed0a82e94e3d94
data/.gitignore CHANGED
@@ -1,15 +1,6 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
10
- *.bundle
11
- *.so
12
- *.o
13
- *.a
14
- mkmf.log
1
+ /.bundle
2
+ /coverage
3
+ /pkg
4
+ /tmp
5
+ .Gemfile.lock
15
6
  .ruby-version
data/.travis.yml CHANGED
@@ -1,8 +1,8 @@
1
1
  language: ruby
2
2
  sudo: true
3
3
  rvm:
4
- - 2.4.0
5
- - 2.3.3
6
- - 2.2.6
4
+ - 2.4.1
5
+ - 2.3.4
6
+ - 2.2.7
7
7
 
8
8
  script: rspec spec
data/CHANGELOG.md CHANGED
@@ -1,8 +1,17 @@
1
- 0.7.1 (2/23/2017) - Added support for the EXT-X-DATERANGE tag. Minor refactoring of existing classes.
1
+ **0.8.0 (4/13/2017)**
2
+
3
+ * Added several improvements for writing playlists: Allow playlist to be initalized as master, expose `write_header`/`write_footer` on `Writer` class.
4
+ * Added support for the `#EXT-X-DISCONTINUITY-SEQUENCE` tag. Added missing attributes to `MediaItem`: `INSTREAM-ID`, `CHARACTERISTICS`, `CHANNELS`, Added missing `HDCP-LEVEL` attribute to `PlaylistItem`.
5
+ * Fixed issue [#20](https://github.com/sethdeckard/m3u8/issues/20).
6
+ * Merged pull request #21 from [rmberg](https://github.com/rmberg), adding support for live playlists to `Writer`.
7
+
8
+ ***
9
+
10
+ 0.7.1 (2/23/2017) - Added support for the `#EXT-X-DATERANGE` tag. Minor refactoring of existing classes.
2
11
 
3
12
  ***
4
13
 
5
- 0.7.0 (2/3/2017) - Added support for EXT-X-INDEPENDENT-SEGMENTS and EXT-X-START tags. Minor version bumped due to changes of default values for new playlists.
14
+ 0.7.0 (2/3/2017) - Added support for `#EXT-X-INDEPENDENT-SEGMENTS` and `#EXT-X-START` tags. Minor version bumped due to changes of default values for new playlists.
6
15
 
7
16
  ***
8
17
 
data/README.md CHANGED
@@ -6,7 +6,13 @@
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 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.
9
+ m3u8 provides easy generation and parsing of m3u8 playlists defined in the [HTTP Live Streaming (HLS)](https://tools.ietf.org/html/draft-pantos-http-live-streaming-20) Internet Draft published by Apple.
10
+
11
+ * The library completely implements version 20 of the HLS Internet Draft.
12
+ * Provides parsing of an m3u8 playlist into an object model from any File, StringIO, or string.
13
+ * Provides ability to write playlist to a File or StringIO or expose as string via to_s.
14
+ * Distinction between a master and media playlist is handled automatically (single Playlist class).
15
+ * Optionally, the library can automatically generate the audio/video codecs string used in the CODEC attribute based on specified H.264, AAC, or MP3 options (such as Profile/Level).
10
16
 
11
17
  ## Installation
12
18
 
@@ -86,23 +92,8 @@ options = { width: 1920, height: 1080, codecs: 'avc1.66.30,mp4a.40.2',
86
92
  item = M3u8::PlaylistItem.new(options)
87
93
  ```
88
94
 
89
- Just get the codec string for custom use:
90
-
91
- ```ruby
92
- options = { profile: 'baseline', level: 3.0, audio_codec: 'aac-lc' }
93
- codecs = M3u8::Playlist.codecs(options)
94
- # => "avc1.66.30,mp4a.40.2"
95
- ```
96
-
97
- Values for audio_codec (codec name): aac-lc, he-aac, mp3
98
-
99
- Possible values for profile (H.264 Profile): baseline, main, high.
100
-
101
- Possible values for level (H.264 Level): 3.0, 3.1, 4.0, 4.1.
102
-
103
- Not all Levels and Profiles can be combined, consult H.264 documentation
104
95
 
105
- ## Parsing Usage
96
+ ## Usage (parsing playlists)
106
97
 
107
98
  ```ruby
108
99
  file = File.open 'spec/fixtures/master.m3u8'
@@ -131,49 +122,26 @@ playlist.items.unshift(item)
131
122
  ```
132
123
 
133
124
  M3u8::Reader is the class handles parsing if you want more control over the process.
134
-
135
- ## Features
136
- * Distinction between segment and master playlists is handled automatically (no need to use a different class).
137
- * Automatically generates the audio/video codec string based on names and options you are familiar with.
138
- * Provides validation of input when adding playlists or segments.
139
- * Allows all options to be configured on a playlist (caching, version, etc.)
140
- * Can write playlist to an IO object (StringIO/File, etc) or access string via to_s.
141
- * Can read playlists into a model (Playlist and Items) from an IO object.
142
- * Any tag or attribute supported by the object model is supported both parsing and generation of m3u8 playlists.
143
- * Supports I-frames (Intra frames) and byte ranges in Segments.
144
- * Supports subtitles, closed captions, alternate audio and video, and comments.
145
- * Supports session data in master playlists.
146
- * Supports keys for encrypted media segments (EXT-X-KEY, EXT-SESSION-KEY).
147
-
148
- ## HLS Spec Status (version 20)
149
- ### Implemented:
150
- * EXTM3U
151
- * EXT-X-VERSION
152
- * EXTINF
153
- * EXT-X-BYTERANGE
154
- * EXT-X-DISCONTINUITY
155
- * EXT-X-KEY
156
- * EXT-X-MAP
157
- * EXT-X-PROGRAM-DATE-TIME
158
- * EXT-X-TARGETDURATION
159
- * EXT-X-MEDIA-SEQUENCE
160
- * EXT-X-ENDLIST
161
- * EXT-X-PLAYLIST-TYPE
162
- * EXT-X-I-FRAMES-ONLY
163
- * EXT-X-MEDIA
164
- * EXT-X-STREAM-INF
165
- * EXT-X-I-FRAME-STREAM-INF
166
- * EXT-X-SESSION-DATA
167
- * EXT-X-SESSION-KEY
168
- * EXT-X-START
169
-
170
- ### TODO:
171
- * EXT-X-DATERANGE
125
+
126
+ ## Usage (misc)
127
+ Generate the codec string based on audio and video codec options without dealing a playlist instance:
128
+
129
+ ```ruby
130
+ options = { profile: 'baseline', level: 3.0, audio_codec: 'aac-lc' }
131
+ codecs = M3u8::Playlist.codecs(options)
132
+ # => "avc1.66.30,mp4a.40.2"
133
+ ```
134
+
135
+ * Values for audio_codec (codec name): aac-lc, he-aac, mp3
136
+ * Values for profile (H.264 Profile): baseline, main, high.
137
+ * Values for level (H.264 Level): 3.0, 3.1, 4.0, 4.1.
138
+
139
+ Not all Levels and Profiles can be combined and validation is not currently implemented, consult H.264 documentation for further details.
140
+
172
141
 
173
142
  ## Roadmap
174
- * Add the last remaining tags for latest version of the spec.
175
- * Validation of all attributes and their values to match the rules defined in the spec.
176
- * Support for different versions of spec, defaulting to latest.
143
+ * Implement validation of all tags, attributes, and values per HLS I-D.
144
+ * Perhaps support for different versions of HLS I-D, defaulting to latest.
177
145
 
178
146
  ## Contributing
179
147
 
@@ -11,11 +11,11 @@ module M3u8
11
11
  end
12
12
 
13
13
  def self.parse(text)
14
- values = text.split '@'
14
+ values = text.split('@')
15
15
  length_value = values[0].to_i
16
16
  start_value = values[1].to_i unless values[1].nil?
17
17
  options = { length: length_value, start: start_value }
18
- ByteRange.new options
18
+ ByteRange.new(options)
19
19
  end
20
20
 
21
21
  def to_s
@@ -3,8 +3,6 @@ module M3u8
3
3
  # DiscontinuityItem represents a EXT-X-DISCONTINUITY tag to indicate a
4
4
  # discontinuity between the SegmentItems that proceed and follow it.
5
5
  class DiscontinuityItem
6
- attr_accessor :tag
7
-
8
6
  def to_s
9
7
  "#EXT-X-DISCONTINUITY\n"
10
8
  end
data/lib/m3u8/error.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
  module M3u8
3
- class PlaylistTypeError < StandardError
3
+ class InvalidPlaylistError < StandardError
4
4
  end
5
5
 
6
6
  class MissingCodecError < StandardError
7
7
  end
8
+
9
+ class PlaylistTypeError < StandardError
10
+ end
8
11
  end
data/lib/m3u8/map_item.rb CHANGED
@@ -12,11 +12,11 @@ module M3u8
12
12
  end
13
13
 
14
14
  def self.parse(text)
15
- attributes = parse_attributes text
15
+ attributes = parse_attributes(text)
16
16
  range_value = attributes['BYTERANGE']
17
17
  range = ByteRange.parse(range_value) unless range_value.nil?
18
18
  options = { uri: attributes['URI'], byterange: range }
19
- MapItem.new options
19
+ MapItem.new(options)
20
20
  end
21
21
 
22
22
  def to_s
@@ -4,7 +4,8 @@ module M3u8
4
4
  class MediaItem
5
5
  extend M3u8
6
6
  attr_accessor :type, :group_id, :language, :assoc_language, :name,
7
- :autoselect, :default, :uri, :forced
7
+ :autoselect, :default, :uri, :forced, :instream_id,
8
+ :characteristics, :channels
8
9
 
9
10
  def initialize(params = {})
10
11
  params.each do |key, value|
@@ -13,7 +14,7 @@ module M3u8
13
14
  end
14
15
 
15
16
  def self.parse(text)
16
- attributes = parse_attributes text
17
+ attributes = parse_attributes(text)
17
18
  options = { type: attributes['TYPE'], group_id: attributes['GROUP-ID'],
18
19
  language: attributes['LANGUAGE'],
19
20
  assoc_language: attributes['ASSOC-LANGUAGE'],
@@ -21,25 +22,34 @@ module M3u8
21
22
  autoselect: parse_yes_no(attributes['AUTOSELECT']),
22
23
  default: parse_yes_no(attributes['DEFAULT']),
23
24
  forced: parse_yes_no(attributes['FORCED']),
24
- uri: attributes['URI'] }
25
- MediaItem.new options
25
+ uri: attributes['URI'],
26
+ instream_id: attributes['INSTREAM-ID'],
27
+ characteristics: attributes['CHARACTERISTICS'],
28
+ channels: attributes['CHANNELS'] }
29
+ MediaItem.new(options)
26
30
  end
27
31
 
28
32
  def to_s
29
- attributes = [type_format,
30
- group_id_format,
31
- language_format,
32
- assoc_language_format,
33
- name_format,
34
- autoselect_format,
35
- default_format,
36
- uri_format,
37
- forced_format].compact.join(',')
38
- "#EXT-X-MEDIA:#{attributes}"
33
+ "#EXT-X-MEDIA:#{formatted_attributes.join(',')}"
39
34
  end
40
35
 
41
36
  private
42
37
 
38
+ def formatted_attributes
39
+ [type_format,
40
+ group_id_format,
41
+ language_format,
42
+ assoc_language_format,
43
+ name_format,
44
+ autoselect_format,
45
+ default_format,
46
+ uri_format,
47
+ forced_format,
48
+ instream_id_format,
49
+ characteristics_format,
50
+ channels_format].compact
51
+ end
52
+
43
53
  def type_format
44
54
  "TYPE=#{type}"
45
55
  end
@@ -82,6 +92,21 @@ module M3u8
82
92
  "FORCED=#{to_yes_no forced}"
83
93
  end
84
94
 
95
+ def instream_id_format
96
+ return if instream_id.nil?
97
+ %(INSTREAM-ID="#{instream_id}")
98
+ end
99
+
100
+ def characteristics_format
101
+ return if characteristics.nil?
102
+ %(CHARACTERISTICS="#{characteristics}")
103
+ end
104
+
105
+ def channels_format
106
+ return if channels.nil?
107
+ %(CHANNELS="#{channels}")
108
+ end
109
+
85
110
  def to_yes_no(boolean)
86
111
  boolean == true ? 'YES' : 'NO'
87
112
  end
data/lib/m3u8/playlist.rb CHANGED
@@ -3,22 +3,23 @@ module M3u8
3
3
  # Playlist represents an m3u8 playlist, it can be a master playlist or a set
4
4
  # of media segments
5
5
  class Playlist
6
- attr_accessor :items, :version, :cache, :target, :sequence, :type,
7
- :iframes_only, :independent_segments
6
+ attr_accessor :items, :version, :cache, :target, :sequence,
7
+ :discontinuity_sequence, :type, :iframes_only,
8
+ :independent_segments, :live
8
9
 
9
10
  def initialize(options = {})
10
- assign_options options
11
- self.items = []
11
+ assign_options(options)
12
+ @items = []
12
13
  end
13
14
 
14
15
  def self.codecs(options = {})
15
- item = PlaylistItem.new options
16
+ item = PlaylistItem.new(options)
16
17
  item.codecs
17
18
  end
18
19
 
19
20
  def self.read(input)
20
21
  reader = Reader.new
21
- reader.read input
22
+ reader.read(input)
22
23
  end
23
24
 
24
25
  def write(output)
@@ -26,8 +27,14 @@ module M3u8
26
27
  writer.write(self)
27
28
  end
28
29
 
30
+ def live?
31
+ return false if master?
32
+ @live
33
+ end
34
+
29
35
  def master?
30
- return false if playlist_size == 0 && segment_size == 0
36
+ return @master unless @master.nil?
37
+ return false if playlist_size.zero? && segment_size.zero?
31
38
  playlist_size > 0
32
39
  end
33
40
 
@@ -55,13 +62,16 @@ module M3u8
55
62
  def assign_options(options)
56
63
  options = defaults.merge(options)
57
64
 
58
- self.version = options[:version]
59
- self.sequence = options[:sequence]
60
- self.cache = options[:cache]
61
- self.target = options[:target]
62
- self.type = options[:type]
63
- self.iframes_only = options[:iframes_only]
64
- self.independent_segments = options[:independent_segments]
65
+ @version = options[:version]
66
+ @sequence = options[:sequence]
67
+ @discontinuity_sequence = options[:discontinuity_sequence]
68
+ @cache = options[:cache]
69
+ @target = options[:target]
70
+ @type = options[:type]
71
+ @iframes_only = options[:iframes_only]
72
+ @independent_segments = options[:independent_segments]
73
+ @master = options[:master]
74
+ @live = options[:live]
65
75
  end
66
76
 
67
77
  def defaults
@@ -69,7 +79,8 @@ module M3u8
69
79
  sequence: 0,
70
80
  target: 10,
71
81
  iframes_only: false,
72
- independent_segments: false
82
+ independent_segments: false,
83
+ live: false
73
84
  }
74
85
  end
75
86
 
@@ -7,7 +7,7 @@ module M3u8
7
7
  attr_accessor :program_id, :width, :height, :codecs, :bandwidth,
8
8
  :audio_codec, :level, :profile, :video, :audio, :uri,
9
9
  :average_bandwidth, :subtitles, :closed_captions, :iframe,
10
- :frame_rate, :name
10
+ :frame_rate, :name, :hdcp_level
11
11
  MISSING_CODEC_MESSAGE = 'Audio or video codec info should be provided.'
12
12
 
13
13
  def initialize(params = {})
@@ -37,11 +37,11 @@ module M3u8
37
37
  def codecs
38
38
  return @codecs unless @codecs.nil?
39
39
 
40
- video = video_codec(profile, level)
41
- return audio_codec if video.nil?
42
- return video if audio_codec.nil?
40
+ video_code = video_codec(profile, level)
41
+ return audio_codec_code if video_code.nil?
42
+ return video_code if audio_codec_code.nil?
43
43
 
44
- "#{video},#{audio_codec}"
44
+ "#{video_code},#{audio_codec_code}"
45
45
  end
46
46
 
47
47
  def to_s
@@ -65,7 +65,7 @@ module M3u8
65
65
  video: attributes['VIDEO'], audio: attributes['AUDIO'],
66
66
  uri: attributes['URI'], subtitles: attributes['SUBTITLES'],
67
67
  closed_captions: attributes['CLOSED-CAPTIONS'],
68
- name: attributes['NAME'] }
68
+ name: attributes['NAME'], hdcp_level: attributes['HDCP-LEVEL'] }
69
69
  end
70
70
 
71
71
  def parse_average_bandwidth(value)
@@ -105,6 +105,7 @@ module M3u8
105
105
  bandwidth_format,
106
106
  average_bandwidth_format,
107
107
  frame_rate_format,
108
+ hdcp_level_format,
108
109
  audio_format,
109
110
  video_format,
110
111
  subtitles_format,
@@ -127,6 +128,11 @@ module M3u8
127
128
  "FRAME-RATE=#{format('%.3f', frame_rate)}"
128
129
  end
129
130
 
131
+ def hdcp_level_format
132
+ return if hdcp_level.nil?
133
+ "HDCP-LEVEL=#{hdcp_level}"
134
+ end
135
+
130
136
  def codecs_format
131
137
  %(CODECS="#{codecs}")
132
138
  end
@@ -170,7 +176,7 @@ module M3u8
170
176
  %(NAME="#{name}")
171
177
  end
172
178
 
173
- def audio_codec
179
+ def audio_codec_code
174
180
  return if @audio_codec.nil?
175
181
  return 'mp4a.40.2' if @audio_codec.casecmp('aac-lc').zero?
176
182
  return 'mp4a.40.5' if @audio_codec.casecmp('he-aac').zero?