m3u8 0.8.2 → 1.8.1

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.
Files changed (103) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +23 -0
  3. data/.gitignore +1 -1
  4. data/.rubocop.yml +31 -0
  5. data/CHANGELOG.md +107 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE.txt +1 -1
  8. data/README.md +524 -40
  9. data/Rakefile +1 -0
  10. data/bin/m3u8 +6 -0
  11. data/lib/m3u8/attribute_formatter.rb +47 -0
  12. data/lib/m3u8/bitrate_item.rb +31 -0
  13. data/lib/m3u8/builder.rb +48 -0
  14. data/lib/m3u8/byte_range.rb +10 -0
  15. data/lib/m3u8/cli/inspect_command.rb +97 -0
  16. data/lib/m3u8/cli/validate_command.rb +24 -0
  17. data/lib/m3u8/cli.rb +116 -0
  18. data/lib/m3u8/codecs.rb +89 -0
  19. data/lib/m3u8/content_steering_item.rb +45 -0
  20. data/lib/m3u8/date_range_item.rb +135 -64
  21. data/lib/m3u8/define_item.rb +54 -0
  22. data/lib/m3u8/discontinuity_item.rb +3 -0
  23. data/lib/m3u8/encryptable.rb +27 -30
  24. data/lib/m3u8/error.rb +1 -0
  25. data/lib/m3u8/gap_item.rb +14 -0
  26. data/lib/m3u8/key_item.rb +7 -0
  27. data/lib/m3u8/map_item.rb +16 -5
  28. data/lib/m3u8/media_item.rb +48 -76
  29. data/lib/m3u8/part_inf_item.rb +35 -0
  30. data/lib/m3u8/part_item.rb +67 -0
  31. data/lib/m3u8/playback_start.rb +19 -12
  32. data/lib/m3u8/playlist.rb +221 -13
  33. data/lib/m3u8/playlist_item.rb +128 -124
  34. data/lib/m3u8/preload_hint_item.rb +54 -0
  35. data/lib/m3u8/reader.rb +86 -28
  36. data/lib/m3u8/rendition_report_item.rb +48 -0
  37. data/lib/m3u8/scte35.rb +130 -0
  38. data/lib/m3u8/scte35_bit_reader.rb +51 -0
  39. data/lib/m3u8/scte35_segmentation_descriptor.rb +54 -0
  40. data/lib/m3u8/scte35_splice_insert.rb +62 -0
  41. data/lib/m3u8/scte35_splice_null.rb +8 -0
  42. data/lib/m3u8/scte35_time_signal.rb +19 -0
  43. data/lib/m3u8/segment_item.rb +37 -3
  44. data/lib/m3u8/server_control_item.rb +69 -0
  45. data/lib/m3u8/session_data_item.rb +17 -28
  46. data/lib/m3u8/session_key_item.rb +8 -1
  47. data/lib/m3u8/skip_item.rb +48 -0
  48. data/lib/m3u8/time_item.rb +10 -0
  49. data/lib/m3u8/version.rb +1 -1
  50. data/lib/m3u8/writer.rb +24 -1
  51. data/lib/m3u8.rb +30 -6
  52. data/m3u8.gemspec +12 -12
  53. data/spec/fixtures/content_steering.m3u8 +10 -0
  54. data/spec/fixtures/daterange_playlist.m3u8 +14 -0
  55. data/spec/fixtures/encrypted_discontinuity.m3u8 +17 -0
  56. data/spec/fixtures/event_playlist.m3u8 +18 -0
  57. data/spec/fixtures/gap_playlist.m3u8 +14 -0
  58. data/spec/fixtures/ll_hls_advanced.m3u8 +18 -0
  59. data/spec/fixtures/ll_hls_playlist.m3u8 +20 -0
  60. data/spec/fixtures/master_full.m3u8 +14 -0
  61. data/spec/fixtures/master_v13.m3u8 +8 -0
  62. data/spec/lib/m3u8/bitrate_item_spec.rb +26 -0
  63. data/spec/lib/m3u8/builder_spec.rb +352 -0
  64. data/spec/lib/m3u8/byte_range_spec.rb +1 -0
  65. data/spec/lib/m3u8/cli/inspect_command_spec.rb +102 -0
  66. data/spec/lib/m3u8/cli/validate_command_spec.rb +39 -0
  67. data/spec/lib/m3u8/cli_spec.rb +104 -0
  68. data/spec/lib/m3u8/content_steering_item_spec.rb +56 -0
  69. data/spec/lib/m3u8/date_range_item_spec.rb +159 -31
  70. data/spec/lib/m3u8/define_item_spec.rb +59 -0
  71. data/spec/lib/m3u8/discontinuity_item_spec.rb +1 -0
  72. data/spec/lib/m3u8/gap_item_spec.rb +12 -0
  73. data/spec/lib/m3u8/key_item_spec.rb +1 -0
  74. data/spec/lib/m3u8/map_item_spec.rb +1 -0
  75. data/spec/lib/m3u8/media_item_spec.rb +34 -0
  76. data/spec/lib/m3u8/part_inf_item_spec.rb +27 -0
  77. data/spec/lib/m3u8/part_item_spec.rb +67 -0
  78. data/spec/lib/m3u8/playback_start_spec.rb +4 -5
  79. data/spec/lib/m3u8/playlist_item_spec.rb +130 -17
  80. data/spec/lib/m3u8/playlist_spec.rb +545 -13
  81. data/spec/lib/m3u8/preload_hint_item_spec.rb +57 -0
  82. data/spec/lib/m3u8/reader_spec.rb +376 -29
  83. data/spec/lib/m3u8/rendition_report_item_spec.rb +56 -0
  84. data/spec/lib/m3u8/round_trip_spec.rb +152 -0
  85. data/spec/lib/m3u8/scte35_bit_reader_spec.rb +106 -0
  86. data/spec/lib/m3u8/scte35_segmentation_descriptor_spec.rb +143 -0
  87. data/spec/lib/m3u8/scte35_spec.rb +94 -0
  88. data/spec/lib/m3u8/scte35_splice_insert_spec.rb +185 -0
  89. data/spec/lib/m3u8/scte35_splice_null_spec.rb +12 -0
  90. data/spec/lib/m3u8/scte35_time_signal_spec.rb +50 -0
  91. data/spec/lib/m3u8/segment_item_spec.rb +47 -0
  92. data/spec/lib/m3u8/server_control_item_spec.rb +64 -0
  93. data/spec/lib/m3u8/session_data_item_spec.rb +1 -0
  94. data/spec/lib/m3u8/session_key_item_spec.rb +1 -0
  95. data/spec/lib/m3u8/skip_item_spec.rb +48 -0
  96. data/spec/lib/m3u8/time_item_spec.rb +1 -0
  97. data/spec/lib/m3u8/writer_spec.rb +69 -30
  98. data/spec/lib/m3u8_spec.rb +1 -0
  99. data/spec/spec_helper.rb +4 -87
  100. metadata +70 -129
  101. data/.hound.yml +0 -3
  102. data/.travis.yml +0 -8
  103. data/Guardfile +0 -6
@@ -1,113 +1,184 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  # DateRangeItem represents a #EXT-X-DATERANGE tag
4
5
  class DateRangeItem
5
- include M3u8
6
+ extend M3u8
7
+ include AttributeFormatter
8
+
9
+ # @return [String, nil] unique date range identifier
10
+ # @return [String, nil] CLASS attribute
11
+ # @return [String, nil] start date (ISO 8601)
12
+ # @return [String, nil] end date (ISO 8601)
13
+ # @return [Float, nil] duration in seconds
14
+ # @return [Float, nil] planned duration in seconds
15
+ # @return [String, nil] SCTE-35 command hex string
16
+ # @return [String, nil] SCTE-35 out hex string
17
+ # @return [String, nil] SCTE-35 in hex string
18
+ # @return [String, nil] CUE attribute
19
+ # @return [Boolean, nil] END-ON-NEXT flag
20
+ # @return [Hash, nil] client-defined X- attributes
21
+ # @return [String, nil] interstitial asset URI
22
+ # @return [String, nil] interstitial asset list URI
23
+ # @return [Float, nil] interstitial resume offset
24
+ # @return [Float, nil] interstitial playout limit
25
+ # @return [String, nil] interstitial restrict value
26
+ # @return [String, nil] interstitial snap value
27
+ # @return [String, nil] interstitial timeline occupies
28
+ # @return [String, nil] interstitial timeline style
29
+ # @return [String, nil] content may vary flag
6
30
  attr_accessor :id, :class_name, :start_date, :end_date, :duration,
7
31
  :planned_duration, :scte35_cmd, :scte35_out, :scte35_in,
8
- :end_on_next, :client_attributes
9
-
32
+ :cue, :end_on_next, :client_attributes,
33
+ :asset_uri, :asset_list, :resume_offset,
34
+ :playout_limit, :restrict, :snap,
35
+ :timeline_occupies, :timeline_style,
36
+ :content_may_vary
37
+
38
+ INTERSTITIAL_KEYS = %w[
39
+ X-ASSET-URI X-ASSET-LIST X-RESUME-OFFSET X-PLAYOUT-LIMIT
40
+ X-RESTRICT X-SNAP X-TIMELINE-OCCUPIES X-TIMELINE-STYLE
41
+ X-CONTENT-MAY-VARY
42
+ ].freeze
43
+
44
+ # @param options [Hash] attribute key-value pairs
10
45
  def initialize(options = {})
11
46
  options.each do |key, value|
12
47
  instance_variable_set("@#{key}", value)
13
48
  end
14
49
  end
15
50
 
16
- def parse(text)
51
+ # Parse an EXT-X-DATERANGE tag.
52
+ # @param text [String] raw tag line
53
+ # @return [DateRangeItem]
54
+ def self.parse(text)
17
55
  attributes = parse_attributes(text)
18
- @id = attributes['ID']
19
- @class_name = attributes['CLASS']
20
- @start_date = attributes['START-DATE']
21
- @end_date = attributes['END-DATE']
22
- @duration = parse_float(attributes['DURATION'])
23
- @planned_duration = parse_float(attributes['PLANNED-DURATION'])
24
- @scte35_cmd = attributes['SCTE35-CMD']
25
- @scte35_out = attributes['SCTE35-OUT']
26
- @scte35_in = attributes['SCTE35-IN']
27
- @end_on_next = attributes.key?('END-ON-NEXT') ? true : false
28
- @client_attributes = parse_client_attributes(attributes)
56
+ options = parse_base_attributes(attributes)
57
+ .merge(parse_interstitials(attributes))
58
+ .merge(client_attributes:
59
+ parse_client_attributes(attributes))
60
+ DateRangeItem.new(options)
29
61
  end
30
62
 
63
+ def self.parse_base_attributes(attributes)
64
+ { id: attributes['ID'],
65
+ class_name: attributes['CLASS'],
66
+ start_date: attributes['START-DATE'],
67
+ end_date: attributes['END-DATE'],
68
+ duration: parse_float(attributes['DURATION']),
69
+ planned_duration:
70
+ parse_float(attributes['PLANNED-DURATION']),
71
+ scte35_cmd: attributes['SCTE35-CMD'],
72
+ scte35_out: attributes['SCTE35-OUT'],
73
+ scte35_in: attributes['SCTE35-IN'],
74
+ cue: attributes['CUE'],
75
+ end_on_next: attributes.key?('END-ON-NEXT') }
76
+ end
77
+ private_class_method :parse_base_attributes
78
+
79
+ def self.parse_interstitials(attributes)
80
+ { asset_uri: attributes['X-ASSET-URI'],
81
+ asset_list: attributes['X-ASSET-LIST'],
82
+ resume_offset:
83
+ parse_float(attributes['X-RESUME-OFFSET']),
84
+ playout_limit:
85
+ parse_float(attributes['X-PLAYOUT-LIMIT']),
86
+ restrict: attributes['X-RESTRICT'],
87
+ snap: attributes['X-SNAP'],
88
+ timeline_occupies:
89
+ attributes['X-TIMELINE-OCCUPIES'],
90
+ timeline_style: attributes['X-TIMELINE-STYLE'],
91
+ content_may_vary:
92
+ attributes['X-CONTENT-MAY-VARY'] }
93
+ end
94
+ private_class_method :parse_interstitials
95
+
96
+ # Render as an m3u8 EXT-X-DATERANGE tag.
97
+ # @return [String]
31
98
  def to_s
32
99
  "#EXT-X-DATERANGE:#{formatted_attributes}"
33
100
  end
34
101
 
35
- private
36
-
37
- def formatted_attributes
38
- [%(ID="#{id}"),
39
- class_name_format,
40
- %(START-DATE="#{start_date}"),
41
- end_date_format,
42
- duration_format,
43
- planned_duration_format,
44
- client_attributes_format,
45
- scte35_cmd_format,
46
- scte35_out_format,
47
- scte35_in_format,
48
- end_on_next_format].compact.join(',')
102
+ # Parse SCTE-35 command data.
103
+ # @return [Scte35, nil]
104
+ def scte35_cmd_info
105
+ Scte35.parse(scte35_cmd) unless scte35_cmd.nil?
49
106
  end
50
107
 
51
- def class_name_format
52
- return if class_name.nil?
53
- %(CLASS="#{class_name}")
108
+ # Parse SCTE-35 out data.
109
+ # @return [Scte35, nil]
110
+ def scte35_out_info
111
+ Scte35.parse(scte35_out) unless scte35_out.nil?
54
112
  end
55
113
 
56
- def end_date_format
57
- return if end_date.nil?
58
- %(END-DATE="#{end_date}")
114
+ # Parse SCTE-35 in data.
115
+ # @return [Scte35, nil]
116
+ def scte35_in_info
117
+ Scte35.parse(scte35_in) unless scte35_in.nil?
59
118
  end
60
119
 
61
- def duration_format
62
- return if duration.nil?
63
- "DURATION=#{duration}"
120
+ def self.parse_client_attributes(attributes)
121
+ attributes.select do |key|
122
+ key.start_with?('X-') && !INTERSTITIAL_KEYS.include?(key)
123
+ end
64
124
  end
125
+ private_class_method :parse_client_attributes
126
+
127
+ private
65
128
 
66
- def planned_duration_format
67
- return if planned_duration.nil?
68
- "PLANNED-DURATION=#{planned_duration}"
129
+ def formatted_attributes
130
+ [%(ID="#{id}"),
131
+ quoted_format('CLASS', class_name),
132
+ %(START-DATE="#{start_date}"),
133
+ quoted_format('END-DATE', end_date),
134
+ unquoted_format('DURATION', decimal_format(duration)),
135
+ unquoted_format('PLANNED-DURATION', decimal_format(planned_duration)),
136
+ client_attributes_format,
137
+ interstitial_formats,
138
+ unquoted_format('SCTE35-CMD', scte35_cmd),
139
+ unquoted_format('SCTE35-OUT', scte35_out),
140
+ unquoted_format('SCTE35-IN', scte35_in),
141
+ quoted_format('CUE', cue),
142
+ end_on_next_format].flatten.compact.join(',')
69
143
  end
70
144
 
71
145
  def client_attributes_format
72
- return if client_attributes.nil?
146
+ return if client_attributes.nil? || client_attributes.empty?
147
+
73
148
  client_attributes.map do |attribute|
74
149
  value = attribute.last
75
- value_format = decimal?(value) ? value : %("#{value}")
76
- "#{attribute.first}=#{value_format}"
150
+ fmt = decimal?(value) ? decimal_format(value) : %("#{value}")
151
+ "#{attribute.first}=#{fmt}"
77
152
  end
78
153
  end
79
154
 
80
155
  def decimal?(value)
81
- return true if value =~ /\A\d+\Z/
156
+ val = value.to_s
157
+ return true if val =~ /\A\d+\Z/
158
+
82
159
  begin
83
- return true if Float(value)
84
- rescue
160
+ true if Float(val)
161
+ rescue StandardError
85
162
  false
86
163
  end
87
164
  end
88
165
 
89
- def scte35_cmd_format
90
- return if scte35_cmd.nil?
91
- "SCTE35-CMD=#{scte35_cmd}"
92
- end
93
-
94
- def scte35_out_format
95
- return if scte35_out.nil?
96
- "SCTE35-OUT=#{scte35_out}"
97
- end
98
-
99
- def scte35_in_format
100
- return if scte35_in.nil?
101
- "SCTE35-IN=#{scte35_in}"
166
+ def interstitial_formats
167
+ [quoted_format('X-ASSET-URI', asset_uri),
168
+ quoted_format('X-ASSET-LIST', asset_list),
169
+ unquoted_format('X-RESUME-OFFSET', decimal_format(resume_offset)),
170
+ unquoted_format('X-PLAYOUT-LIMIT', decimal_format(playout_limit)),
171
+ quoted_format('X-RESTRICT', restrict),
172
+ quoted_format('X-SNAP', snap),
173
+ quoted_format('X-TIMELINE-OCCUPIES', timeline_occupies),
174
+ quoted_format('X-TIMELINE-STYLE', timeline_style),
175
+ quoted_format('X-CONTENT-MAY-VARY', content_may_vary)]
102
176
  end
103
177
 
104
178
  def end_on_next_format
105
179
  return unless end_on_next
106
- 'END-ON-NEXT=YES'
107
- end
108
180
 
109
- def parse_client_attributes(attributes)
110
- attributes.select { |key| key.start_with?('X-') }
181
+ 'END-ON-NEXT=YES'
111
182
  end
112
183
  end
113
184
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # DefineItem represents an EXT-X-DEFINE tag which provides variable
5
+ # definitions for variable substitution. Supports three mutually
6
+ # exclusive modes: NAME/VALUE, IMPORT, or QUERYPARAM.
7
+ class DefineItem
8
+ extend M3u8
9
+
10
+ # @return [String, nil] variable name
11
+ # @return [String, nil] variable value
12
+ # @return [String, nil] imported variable name
13
+ # @return [String, nil] query parameter name
14
+ attr_accessor :name, :value, :import, :queryparam
15
+
16
+ # @param params [Hash] attribute key-value pairs
17
+ def initialize(params = {})
18
+ params.each do |key, val|
19
+ instance_variable_set("@#{key}", val)
20
+ end
21
+ end
22
+
23
+ # Parse an EXT-X-DEFINE tag.
24
+ # @param text [String] raw tag line
25
+ # @return [DefineItem]
26
+ def self.parse(text)
27
+ attributes = parse_attributes(text)
28
+ DefineItem.new(
29
+ name: attributes['NAME'],
30
+ value: attributes['VALUE'],
31
+ import: attributes['IMPORT'],
32
+ queryparam: attributes['QUERYPARAM']
33
+ )
34
+ end
35
+
36
+ # Render as an m3u8 EXT-X-DEFINE tag.
37
+ # @return [String]
38
+ def to_s
39
+ "#EXT-X-DEFINE:#{formatted_attributes}"
40
+ end
41
+
42
+ private
43
+
44
+ def formatted_attributes
45
+ if import
46
+ %(IMPORT="#{import}")
47
+ elsif queryparam
48
+ %(QUERYPARAM="#{queryparam}")
49
+ else
50
+ %(NAME="#{name}",VALUE="#{value}")
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  # DiscontinuityItem represents a EXT-X-DISCONTINUITY tag to indicate a
4
5
  # discontinuity between the SegmentItems that proceed and follow it.
5
6
  class DiscontinuityItem
7
+ # Render as an m3u8 EXT-X-DISCONTINUITY tag.
8
+ # @return [String]
6
9
  def to_s
7
10
  "#EXT-X-DISCONTINUITY\n"
8
11
  end
@@ -1,7 +1,22 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
- # Encapsulates logic common to encryption key tags
4
+ # Encapsulates logic common to encryption key tags.
5
+ # Adds :method, :uri, :iv, :key_format, and
6
+ # :key_format_versions accessors when included.
4
7
  module Encryptable
8
+ include AttributeFormatter
9
+
10
+ # @!attribute [rw] method
11
+ # @return [String, nil] encryption method
12
+ # @!attribute [rw] uri
13
+ # @return [String, nil] key URI
14
+ # @!attribute [rw] iv
15
+ # @return [String, nil] initialization vector
16
+ # @!attribute [rw] key_format
17
+ # @return [String, nil] KEYFORMAT value
18
+ # @!attribute [rw] key_format_versions
19
+ # @return [String, nil] KEYFORMATVERSIONS value
5
20
  def self.included(base)
6
21
  base.send :attr_accessor, :method
7
22
  base.send :attr_accessor, :uri
@@ -10,42 +25,24 @@ module M3u8
10
25
  base.send :attr_accessor, :key_format_versions
11
26
  end
12
27
 
28
+ # Render encryption attributes as a comma-separated string.
29
+ # @return [String]
13
30
  def attributes_to_s
14
- [method_format,
15
- uri_format,
16
- iv_format,
17
- key_format_format,
18
- key_format_versions_format].compact.join(',')
31
+ [unquoted_format('METHOD', method),
32
+ quoted_format('URI', uri),
33
+ unquoted_format('IV', iv),
34
+ quoted_format('KEYFORMAT', key_format),
35
+ quoted_format('KEYFORMATVERSIONS',
36
+ key_format_versions)].compact.join(',')
19
37
  end
20
38
 
39
+ # Map HLS attribute names to Ruby symbol keys.
40
+ # @param attributes [Hash] raw attribute hash
41
+ # @return [Hash] symbolized options
21
42
  def convert_key_names(attributes)
22
43
  { method: attributes['METHOD'], uri: attributes['URI'],
23
44
  iv: attributes['IV'], key_format: attributes['KEYFORMAT'],
24
45
  key_format_versions: attributes['KEYFORMATVERSIONS'] }
25
46
  end
26
-
27
- private
28
-
29
- def method_format
30
- "METHOD=#{method}"
31
- end
32
-
33
- def uri_format
34
- %(URI="#{uri}") unless uri.nil?
35
- end
36
-
37
- def iv_format
38
- "IV=#{iv}" unless iv.nil?
39
- end
40
-
41
- def key_format_format
42
- %(KEYFORMAT="#{key_format}") unless key_format.nil?
43
- end
44
-
45
- def key_format_versions_format
46
- return if key_format_versions.nil?
47
-
48
- %(KEYFORMATVERSIONS="#{key_format_versions}")
49
- end
50
47
  end
51
48
  end
data/lib/m3u8/error.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  class InvalidPlaylistError < StandardError
4
5
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # GapItem represents an EXT-X-GAP tag to indicate that the segment URI
5
+ # to which it applies does not contain media data and should not be
6
+ # loaded by clients.
7
+ class GapItem
8
+ # Render as an m3u8 EXT-X-GAP tag.
9
+ # @return [String]
10
+ def to_s
11
+ '#EXT-X-GAP'
12
+ end
13
+ end
14
+ end
data/lib/m3u8/key_item.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  # KeyItem represents a set of EXT-X-KEY attributes
4
5
  class KeyItem
5
6
  include Encryptable
6
7
  extend M3u8
7
8
 
9
+ # @param params [Hash] attribute key-value pairs
8
10
  def initialize(params = {})
9
11
  options = convert_key_names(params)
10
12
  options.merge(params).each do |key, value|
@@ -12,11 +14,16 @@ module M3u8
12
14
  end
13
15
  end
14
16
 
17
+ # Parse an EXT-X-KEY tag.
18
+ # @param text [String] raw tag line
19
+ # @return [KeyItem]
15
20
  def self.parse(text)
16
21
  attributes = parse_attributes(text)
17
22
  KeyItem.new(attributes)
18
23
  end
19
24
 
25
+ # Render as an m3u8 EXT-X-KEY tag.
26
+ # @return [String]
20
27
  def to_s
21
28
  "#EXT-X-KEY:#{attributes_to_s}"
22
29
  end
data/lib/m3u8/map_item.rb CHANGED
@@ -1,16 +1,25 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  # MapItem represents a EXT-X-MAP tag which specifies how to obtain the Media
4
5
  # Initialization Section
5
6
  class MapItem
6
7
  extend M3u8
7
8
  include M3u8
9
+ include AttributeFormatter
10
+
11
+ # @return [String, nil] URI of the initialization section
12
+ # @return [ByteRange, nil] byte range within the resource
8
13
  attr_accessor :uri, :byterange
9
14
 
15
+ # @param params [Hash] attribute key-value pairs
10
16
  def initialize(params = {})
11
- intialize_with_byterange(params)
17
+ initialize_with_byterange(params)
12
18
  end
13
19
 
20
+ # Parse an EXT-X-MAP tag.
21
+ # @param text [String] raw tag line
22
+ # @return [MapItem]
14
23
  def self.parse(text)
15
24
  attributes = parse_attributes(text)
16
25
  range_value = attributes['BYTERANGE']
@@ -19,15 +28,17 @@ module M3u8
19
28
  MapItem.new(options)
20
29
  end
21
30
 
31
+ # Render as an m3u8 EXT-X-MAP tag.
32
+ # @return [String]
22
33
  def to_s
23
- %(#EXT-X-MAP:URI="#{uri}"#{byterange_format})
34
+ "#EXT-X-MAP:#{formatted_attributes.compact.join(',')}"
24
35
  end
25
36
 
26
37
  private
27
38
 
28
- def byterange_format
29
- return if byterange.nil?
30
- %(,BYTERANGE="#{byterange}")
39
+ def formatted_attributes
40
+ [%(URI="#{uri}"),
41
+ quoted_format('BYTERANGE', byterange)]
31
42
  end
32
43
  end
33
44
  end
@@ -1,21 +1,45 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  # MediaItem represents a set of EXT-X-MEDIA attributes
4
5
  class MediaItem
5
6
  extend M3u8
7
+ include AttributeFormatter
8
+
9
+ # @return [String, nil] media type (AUDIO, VIDEO, etc.)
10
+ # @return [String, nil] group ID
11
+ # @return [String, nil] language tag (RFC 5646)
12
+ # @return [String, nil] associated language tag
13
+ # @return [String, nil] rendition name
14
+ # @return [Boolean, nil] AUTOSELECT flag
15
+ # @return [Boolean, nil] DEFAULT flag
16
+ # @return [String, nil] rendition URI
17
+ # @return [Boolean, nil] FORCED flag
18
+ # @return [String, nil] instream ID for CC
19
+ # @return [String, nil] media characteristics
20
+ # @return [String, nil] audio channels
21
+ # @return [String, nil] stable rendition ID
22
+ # @return [Integer, nil] audio bit depth
23
+ # @return [Integer, nil] audio sample rate
6
24
  attr_accessor :type, :group_id, :language, :assoc_language, :name,
7
25
  :autoselect, :default, :uri, :forced, :instream_id,
8
- :characteristics, :channels
26
+ :characteristics, :channels, :stable_rendition_id,
27
+ :bit_depth, :sample_rate
9
28
 
29
+ # @param params [Hash] attribute key-value pairs
10
30
  def initialize(params = {})
11
31
  params.each do |key, value|
12
32
  instance_variable_set("@#{key}", value)
13
33
  end
14
34
  end
15
35
 
36
+ # Parse an EXT-X-MEDIA tag.
37
+ # @param text [String] raw tag line
38
+ # @return [MediaItem]
16
39
  def self.parse(text)
17
40
  attributes = parse_attributes(text)
18
- options = { type: attributes['TYPE'], group_id: attributes['GROUP-ID'],
41
+ options = { type: attributes['TYPE'],
42
+ group_id: attributes['GROUP-ID'],
19
43
  language: attributes['LANGUAGE'],
20
44
  assoc_language: attributes['ASSOC-LANGUAGE'],
21
45
  name: attributes['NAME'],
@@ -25,10 +49,16 @@ module M3u8
25
49
  uri: attributes['URI'],
26
50
  instream_id: attributes['INSTREAM-ID'],
27
51
  characteristics: attributes['CHARACTERISTICS'],
28
- channels: attributes['CHANNELS'] }
52
+ channels: attributes['CHANNELS'],
53
+ stable_rendition_id:
54
+ attributes['STABLE-RENDITION-ID'],
55
+ bit_depth: parse_int(attributes['BIT-DEPTH']),
56
+ sample_rate: parse_int(attributes['SAMPLE-RATE']) }
29
57
  MediaItem.new(options)
30
58
  end
31
59
 
60
+ # Render as an m3u8 EXT-X-MEDIA tag.
61
+ # @return [String]
32
62
  def to_s
33
63
  "#EXT-X-MEDIA:#{formatted_attributes.join(',')}"
34
64
  end
@@ -36,79 +66,21 @@ module M3u8
36
66
  private
37
67
 
38
68
  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
-
53
- def type_format
54
- "TYPE=#{type}"
55
- end
56
-
57
- def group_id_format
58
- %(GROUP-ID="#{group_id}")
59
- end
60
-
61
- def language_format
62
- return if language.nil?
63
- %(LANGUAGE="#{language}")
64
- end
65
-
66
- def assoc_language_format
67
- return if assoc_language.nil?
68
- %(ASSOC-LANGUAGE="#{assoc_language}")
69
- end
70
-
71
- def name_format
72
- %(NAME="#{name}")
73
- end
74
-
75
- def autoselect_format
76
- return if autoselect.nil?
77
- "AUTOSELECT=#{to_yes_no autoselect}"
78
- end
79
-
80
- def default_format
81
- return if default.nil?
82
- "DEFAULT=#{to_yes_no default}"
83
- end
84
-
85
- def uri_format
86
- return if uri.nil?
87
- %(URI="#{uri}")
88
- end
89
-
90
- def forced_format
91
- return if forced.nil?
92
- "FORCED=#{to_yes_no forced}"
93
- end
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
-
110
- def to_yes_no(boolean)
111
- boolean == true ? 'YES' : 'NO'
69
+ ["TYPE=#{type}",
70
+ %(GROUP-ID="#{group_id}"),
71
+ quoted_format('LANGUAGE', language),
72
+ quoted_format('ASSOC-LANGUAGE', assoc_language),
73
+ %(NAME="#{name}"),
74
+ boolean_format('AUTOSELECT', autoselect),
75
+ boolean_format('DEFAULT', default),
76
+ quoted_format('URI', uri),
77
+ boolean_format('FORCED', forced),
78
+ quoted_format('INSTREAM-ID', instream_id),
79
+ quoted_format('CHARACTERISTICS', characteristics),
80
+ quoted_format('CHANNELS', channels),
81
+ quoted_format('STABLE-RENDITION-ID', stable_rendition_id),
82
+ unquoted_format('BIT-DEPTH', bit_depth),
83
+ unquoted_format('SAMPLE-RATE', sample_rate)].compact
112
84
  end
113
85
  end
114
86
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # PartInfItem represents an EXT-X-PART-INF tag which provides
5
+ # information about partial segments in the playlist.
6
+ class PartInfItem
7
+ extend M3u8
8
+
9
+ # @return [Float, nil] partial segment target duration
10
+ attr_accessor :part_target
11
+
12
+ # @param params [Hash] attribute key-value pairs
13
+ def initialize(params = {})
14
+ params.each do |key, value|
15
+ instance_variable_set("@#{key}", value)
16
+ end
17
+ end
18
+
19
+ # Parse an EXT-X-PART-INF tag.
20
+ # @param text [String] raw tag line
21
+ # @return [PartInfItem]
22
+ def self.parse(text)
23
+ attributes = parse_attributes(text)
24
+ PartInfItem.new(
25
+ part_target: attributes['PART-TARGET'].to_f
26
+ )
27
+ end
28
+
29
+ # Render as an m3u8 EXT-X-PART-INF tag.
30
+ # @return [String]
31
+ def to_s
32
+ "#EXT-X-PART-INF:PART-TARGET=#{part_target}"
33
+ end
34
+ end
35
+ end