m3u8 1.7.0 → 1.8.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.gitignore +1 -1
  4. data/.rubocop.yml +2 -2
  5. data/CHANGELOG.md +18 -0
  6. data/Gemfile +6 -0
  7. data/LICENSE.txt +1 -1
  8. data/README.md +1 -1
  9. data/lib/m3u8/attribute_formatter.rb +30 -0
  10. data/lib/m3u8/bitrate_item.rb +7 -0
  11. data/lib/m3u8/builder.rb +1 -0
  12. data/lib/m3u8/byte_range.rb +8 -0
  13. data/lib/m3u8/codecs.rb +89 -0
  14. data/lib/m3u8/content_steering_item.rb +11 -12
  15. data/lib/m3u8/date_range_item.rb +103 -127
  16. data/lib/m3u8/define_item.rb +10 -0
  17. data/lib/m3u8/discontinuity_item.rb +2 -0
  18. data/lib/m3u8/encryptable.rb +26 -30
  19. data/lib/m3u8/gap_item.rb +2 -0
  20. data/lib/m3u8/key_item.rb +6 -0
  21. data/lib/m3u8/map_item.rb +14 -6
  22. data/lib/m3u8/media_item.rb +42 -110
  23. data/lib/m3u8/part_inf_item.rb +7 -0
  24. data/lib/m3u8/part_item.rb +16 -18
  25. data/lib/m3u8/playback_start.rb +17 -13
  26. data/lib/m3u8/playlist.rb +65 -6
  27. data/lib/m3u8/playlist_item.rb +113 -220
  28. data/lib/m3u8/preload_hint_item.rb +16 -28
  29. data/lib/m3u8/reader.rb +19 -24
  30. data/lib/m3u8/rendition_report_item.rb +13 -23
  31. data/lib/m3u8/scte35.rb +2 -1
  32. data/lib/m3u8/scte35_segmentation_descriptor.rb +3 -1
  33. data/lib/m3u8/segment_item.rb +20 -1
  34. data/lib/m3u8/server_control_item.rb +15 -21
  35. data/lib/m3u8/session_data_item.rb +15 -28
  36. data/lib/m3u8/session_key_item.rb +7 -1
  37. data/lib/m3u8/skip_item.rb +12 -12
  38. data/lib/m3u8/time_item.rb +7 -0
  39. data/lib/m3u8/version.rb +1 -1
  40. data/lib/m3u8/writer.rb +5 -0
  41. data/lib/m3u8.rb +24 -1
  42. data/m3u8.gemspec +10 -13
  43. data/spec/lib/m3u8/date_range_item_spec.rb +4 -7
  44. data/spec/lib/m3u8/playback_start_spec.rb +3 -5
  45. data/spec/lib/m3u8/playlist_item_spec.rb +11 -9
  46. data/spec/lib/m3u8/segment_item_spec.rb +23 -0
  47. data/spec/spec_helper.rb +3 -1
  48. metadata +10 -119
  49. data/.hound.yml +0 -3
  50. data/Guardfile +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c38180f01d16f00854720acaf6fc8359f2362417e19b8e45248a1573597716ca
4
- data.tar.gz: b5192ed27a57a1f3a06d32f09d5d1fdbcd3c1e788d6bf515c68f739531ba8f23
3
+ metadata.gz: 395231edf55669bc7358629d116d5f7486a74bf1e7c5f6f9287497b758178511
4
+ data.tar.gz: f1592e2cd9cc9d175e3f06c7b330baffea7271ff1c153895865c35ed96e1c17e
5
5
  SHA512:
6
- metadata.gz: be03dc5ca38934401073972dfab62b7ec35e4733eda199a91320b1b668e4046f04eae8518a720795d945bf7439da70ebe2d15d9bb53db3cd65cffedd3f70c0d9
7
- data.tar.gz: a4faf2090ab7d3cd6c5788723a842d795903748e42a8ced6726af626ae81dc4373262074c2eeff70baae56c3b73c02d5a4a93d6caf84646d530092202c54dda1
6
+ metadata.gz: 7e0c36b5d0afbd29e1d822c020e0302c052c1051c2637e90795772165f9ca5f2a452ad9e4da593d895f88dbf3527d831829dc741dfbb9d2b88b9f8223f02b3c0
7
+ data.tar.gz: d7631c7d2cf13392f4a317c00f0cf7f90097695da58d20cdb5a76d66db01ce2e93643c48cfbf30d40f9b20260c793ffb9e430f0aea0f99c5c25d8c9c253fca93
@@ -9,7 +9,7 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  strategy:
11
11
  matrix:
12
- ruby-version: ['3.0', '3.1', '3.2', '3.3']
12
+ ruby-version: ['3.1', '3.2', '3.3', '3.4']
13
13
  steps:
14
14
  - uses: actions/checkout@v4
15
15
  - name: Set up Ruby ${{ matrix.ruby-version }}
data/.gitignore CHANGED
@@ -2,5 +2,5 @@
2
2
  /coverage
3
3
  /pkg
4
4
  /tmp
5
- .Gemfile.lock
5
+ Gemfile.lock
6
6
  .ruby-version
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 3.0
2
+ TargetRubyVersion: 3.1
3
3
  NewCops: enable
4
4
  SuggestExtensions: false
5
5
  Exclude:
@@ -24,7 +24,7 @@ Metrics/CyclomaticComplexity:
24
24
  Metrics/PerceivedComplexity:
25
25
  Enabled: false
26
26
  Layout/LineLength:
27
- Max: 120
27
+ Max: 80
28
28
  Exclude:
29
29
  - 'spec/**/*'
30
30
  Style/StringLiterals:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ **1.8.0**
2
+
3
+ * Standardized all item classes to use class-level `self.parse` methods,
4
+ converting `PlaybackStart`, `DateRangeItem`, `PlaylistItem`, and
5
+ `SegmentItem` from instance-level parse.
6
+ * Applied `AttributeFormatter` to `MapItem` and `PartItem`, replacing
7
+ manual format helpers with `quoted_format` and `unquoted_format`.
8
+ * Extracted `tag_value` helper in `Reader`, replacing repeated `gsub`
9
+ tag-prefix patterns.
10
+ * Normalized `Reader` lambda syntax, converting `proc` blocks to
11
+ lambdas.
12
+ * Removed `include M3u8` from `Reader` since all parsing now uses
13
+ class-level methods.
14
+ * Added YARD `@param`/`@return` documentation to all public methods
15
+ and attributes across the entire codebase.
16
+
17
+ ***
18
+
1
19
  **1.7.0**
2
20
 
3
21
  * Added HLS Interstitials first-class `DateRangeItem` accessors:
data/Gemfile CHANGED
@@ -4,3 +4,9 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in m3u8.gemspec
6
6
  gemspec
7
+
8
+ gem 'rake'
9
+ gem 'rspec', '>= 3.0'
10
+ gem 'rubocop', require: false
11
+ gem 'rubocop-rake', require: false
12
+ gem 'simplecov', require: false
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2016 Seth Deckard
1
+ Copyright (c) 2014-2026 Seth Deckard
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -12,7 +12,7 @@ m3u8 provides easy generation and parsing of m3u8 playlists defined in [RFC 8216
12
12
 
13
13
  ## Requirements
14
14
 
15
- Ruby 3.0+
15
+ Ruby 3.1+
16
16
 
17
17
  ## Installation
18
18
 
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # Shared helpers for formatting HLS tag attributes
5
+ module AttributeFormatter
6
+ # Format a quoted attribute (e.g. KEY="value").
7
+ # @param key [String] attribute name
8
+ # @param value [Object, nil] attribute value
9
+ # @return [String, nil] formatted string or nil when value is nil
10
+ def quoted_format(key, value)
11
+ %(#{key}="#{value}") unless value.nil?
12
+ end
13
+
14
+ # Format an unquoted attribute (e.g. KEY=value).
15
+ # @param key [String] attribute name
16
+ # @param value [Object, nil] attribute value
17
+ # @return [String, nil] formatted string or nil when value is nil
18
+ def unquoted_format(key, value)
19
+ "#{key}=#{value}" unless value.nil?
20
+ end
21
+
22
+ # Format a YES/NO boolean attribute (e.g. KEY=YES).
23
+ # @param key [String] attribute name
24
+ # @param value [Boolean, nil] attribute value
25
+ # @return [String, nil] formatted string or nil when value is nil
26
+ def boolean_format(key, value)
27
+ "#{key}=#{value == true ? 'YES' : 'NO'}" unless value.nil?
28
+ end
29
+ end
30
+ end
@@ -4,19 +4,26 @@ module M3u8
4
4
  # BitrateItem represents an EXT-X-BITRATE tag that indicates the
5
5
  # approximate bitrate of the following media segments in kbps.
6
6
  class BitrateItem
7
+ # @return [Integer, nil] approximate bitrate in kbps
7
8
  attr_accessor :bitrate
8
9
 
10
+ # @param params [Hash] attribute key-value pairs
9
11
  def initialize(params = {})
10
12
  params.each do |key, value|
11
13
  instance_variable_set("@#{key}", value)
12
14
  end
13
15
  end
14
16
 
17
+ # Parse an EXT-X-BITRATE tag.
18
+ # @param text [String] raw tag line
19
+ # @return [BitrateItem]
15
20
  def self.parse(text)
16
21
  value = text.gsub('#EXT-X-BITRATE:', '').strip
17
22
  BitrateItem.new(bitrate: value.to_i)
18
23
  end
19
24
 
25
+ # Render as an m3u8 EXT-X-BITRATE tag.
26
+ # @return [String]
20
27
  def to_s
21
28
  "#EXT-X-BITRATE:#{bitrate}"
22
29
  end
data/lib/m3u8/builder.rb CHANGED
@@ -28,6 +28,7 @@ module M3u8
28
28
  gap: 'GapItem'
29
29
  }.freeze
30
30
 
31
+ # @param playlist [Playlist] playlist to build into
31
32
  def initialize(playlist)
32
33
  @playlist = playlist
33
34
  end
@@ -3,14 +3,20 @@
3
3
  module M3u8
4
4
  # ByteRange represents sub range of a resource
5
5
  class ByteRange
6
+ # @return [Integer, nil] number of bytes
7
+ # @return [Integer, nil] start offset in bytes
6
8
  attr_accessor :length, :start
7
9
 
10
+ # @param params [Hash] :length and optional :start
8
11
  def initialize(params = {})
9
12
  params.each do |key, value|
10
13
  instance_variable_set("@#{key}", value)
11
14
  end
12
15
  end
13
16
 
17
+ # Parse a byte range string (e.g. "4500@600").
18
+ # @param text [String] byte range string
19
+ # @return [ByteRange]
14
20
  def self.parse(text)
15
21
  values = text.split('@')
16
22
  length_value = values[0].to_i
@@ -19,6 +25,8 @@ module M3u8
19
25
  ByteRange.new(options)
20
26
  end
21
27
 
28
+ # Render as a byte range string (e.g. "4500@600").
29
+ # @return [String]
22
30
  def to_s
23
31
  "#{length}#{start_format}"
24
32
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # Codec lookup tables for HLS playlist items
5
+ module Codecs
6
+ AUDIO_CODECS = {
7
+ 'aac-lc' => 'mp4a.40.2',
8
+ 'he-aac' => 'mp4a.40.5',
9
+ 'mp3' => 'mp4a.40.34',
10
+ 'ac-3' => 'ac-3',
11
+ 'ec-3' => 'ec-3',
12
+ 'e-ac-3' => 'ec-3',
13
+ 'flac' => 'fLaC',
14
+ 'opus' => 'Opus'
15
+ }.freeze
16
+
17
+ BASELINE_CODECS = {
18
+ 3.0 => 'avc1.66.30',
19
+ 3.1 => 'avc1.42001f'
20
+ }.freeze
21
+
22
+ MAIN_CODECS = {
23
+ 3.0 => 'avc1.77.30',
24
+ 3.1 => 'avc1.4d001f',
25
+ 4.0 => 'avc1.4d0028',
26
+ 4.1 => 'avc1.4d0029'
27
+ }.freeze
28
+
29
+ HIGH_LEVELS = [3.0, 3.1, 3.2, 4.0, 4.1, 4.2,
30
+ 5.0, 5.1, 5.2].freeze
31
+
32
+ HEVC_CODECS = {
33
+ ['hevc-main', 3.1] => 'hvc1.1.6.L93.B0',
34
+ ['hevc-main', 4.0] => 'hvc1.1.6.L120.B0',
35
+ ['hevc-main', 5.0] => 'hvc1.1.6.L150.B0',
36
+ ['hevc-main', 5.1] => 'hvc1.1.6.L153.B0',
37
+ ['hevc-main-10', 3.1] => 'hvc1.2.4.L93.B0',
38
+ ['hevc-main-10', 4.0] => 'hvc1.2.4.L120.B0',
39
+ ['hevc-main-10', 5.0] => 'hvc1.2.4.L150.B0',
40
+ ['hevc-main-10', 5.1] => 'hvc1.2.4.L153.B0'
41
+ }.freeze
42
+
43
+ AV1_CODECS = {
44
+ ['av1-main', 3.1] => 'av01.0.04M.08',
45
+ ['av1-main', 4.0] => 'av01.0.08M.08',
46
+ ['av1-main', 5.0] => 'av01.0.12M.08',
47
+ ['av1-main', 5.1] => 'av01.0.13M.08',
48
+ ['av1-high', 3.1] => 'av01.1.04H.10',
49
+ ['av1-high', 4.0] => 'av01.1.08H.10',
50
+ ['av1-high', 5.0] => 'av01.1.12H.10',
51
+ ['av1-high', 5.1] => 'av01.1.13H.10'
52
+ }.freeze
53
+
54
+ # Look up the codec string for an audio codec name.
55
+ # @param codec [String, nil] audio codec name
56
+ # @return [String, nil] codec string
57
+ def self.audio_codec(codec)
58
+ return if codec.nil?
59
+
60
+ AUDIO_CODECS[codec.downcase]
61
+ end
62
+
63
+ # Look up the codec string for a video profile and level.
64
+ # @param profile [String, nil] video profile name
65
+ # @param level [Float, Integer, nil] video level
66
+ # @return [String, nil] codec string
67
+ def self.video_codec(profile, level)
68
+ return if profile.nil? || level.nil?
69
+
70
+ level = level.to_f
71
+ name = profile.downcase
72
+ return BASELINE_CODECS[level] if name == 'baseline'
73
+ return MAIN_CODECS[level] if name == 'main'
74
+ return high_codec_string(level) if name == 'high'
75
+ return HEVC_CODECS[[profile, level]] if name.start_with?('hevc-')
76
+
77
+ AV1_CODECS[[profile, level]] if name.start_with?('av1-')
78
+ end
79
+
80
+ def self.high_codec_string(level)
81
+ return nil unless HIGH_LEVELS.include?(level)
82
+
83
+ hex = level.to_s.sub('.', '').to_i.to_s(16)
84
+ "avc1.6400#{hex}"
85
+ end
86
+
87
+ private_class_method :high_codec_string
88
+ end
89
+ end
@@ -5,15 +5,22 @@ module M3u8
5
5
  # indicates a Content Steering Manifest for dynamic pathway selection.
6
6
  class ContentSteeringItem
7
7
  extend M3u8
8
+ include AttributeFormatter
8
9
 
10
+ # @return [String, nil] steering manifest server URI
11
+ # @return [String, nil] default pathway ID
9
12
  attr_accessor :server_uri, :pathway_id
10
13
 
14
+ # @param params [Hash] attribute key-value pairs
11
15
  def initialize(params = {})
12
16
  params.each do |key, value|
13
17
  instance_variable_set("@#{key}", value)
14
18
  end
15
19
  end
16
20
 
21
+ # Parse an EXT-X-CONTENT-STEERING tag.
22
+ # @param text [String] raw tag line
23
+ # @return [ContentSteeringItem]
17
24
  def self.parse(text)
18
25
  attributes = parse_attributes(text)
19
26
  ContentSteeringItem.new(
@@ -22,6 +29,8 @@ module M3u8
22
29
  )
23
30
  end
24
31
 
32
+ # Render as an m3u8 EXT-X-CONTENT-STEERING tag.
33
+ # @return [String]
25
34
  def to_s
26
35
  "#EXT-X-CONTENT-STEERING:#{formatted_attributes}"
27
36
  end
@@ -29,18 +38,8 @@ module M3u8
29
38
  private
30
39
 
31
40
  def formatted_attributes
32
- [server_uri_format,
33
- pathway_id_format].compact.join(',')
34
- end
35
-
36
- def server_uri_format
37
- %(SERVER-URI="#{server_uri}")
38
- end
39
-
40
- def pathway_id_format
41
- return if pathway_id.nil?
42
-
43
- %(PATHWAY-ID="#{pathway_id}")
41
+ [quoted_format('SERVER-URI', server_uri),
42
+ quoted_format('PATHWAY-ID', pathway_id)].compact.join(',')
44
43
  end
45
44
  end
46
45
  end
@@ -3,8 +3,30 @@
3
3
  module M3u8
4
4
  # DateRangeItem represents a #EXT-X-DATERANGE tag
5
5
  class DateRangeItem
6
- include M3u8
7
-
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
8
30
  attr_accessor :id, :class_name, :start_date, :end_date, :duration,
9
31
  :planned_duration, :scte35_cmd, :scte35_out, :scte35_in,
10
32
  :cue, :end_on_next, :client_attributes,
@@ -19,86 +41,114 @@ module M3u8
19
41
  X-CONTENT-MAY-VARY
20
42
  ].freeze
21
43
 
44
+ # @param options [Hash] attribute key-value pairs
22
45
  def initialize(options = {})
23
46
  options.each do |key, value|
24
47
  instance_variable_set("@#{key}", value)
25
48
  end
26
49
  end
27
50
 
28
- 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)
29
55
  attributes = parse_attributes(text)
30
- @id = attributes['ID']
31
- @class_name = attributes['CLASS']
32
- @start_date = attributes['START-DATE']
33
- @end_date = attributes['END-DATE']
34
- @duration = parse_float(attributes['DURATION'])
35
- @planned_duration = parse_float(attributes['PLANNED-DURATION'])
36
- @scte35_cmd = attributes['SCTE35-CMD']
37
- @scte35_out = attributes['SCTE35-OUT']
38
- @scte35_in = attributes['SCTE35-IN']
39
- @cue = attributes['CUE']
40
- @end_on_next = attributes.key?('END-ON-NEXT')
41
- parse_interstitials(attributes)
42
- @client_attributes = parse_client_attributes(attributes)
43
- end
44
-
56
+ options = parse_base_attributes(attributes)
57
+ .merge(parse_interstitials(attributes))
58
+ .merge(client_attributes:
59
+ parse_client_attributes(attributes))
60
+ DateRangeItem.new(options)
61
+ end
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]
45
98
  def to_s
46
99
  "#EXT-X-DATERANGE:#{formatted_attributes}"
47
100
  end
48
101
 
102
+ # Parse SCTE-35 command data.
103
+ # @return [Scte35, nil]
49
104
  def scte35_cmd_info
50
105
  Scte35.parse(scte35_cmd) unless scte35_cmd.nil?
51
106
  end
52
107
 
108
+ # Parse SCTE-35 out data.
109
+ # @return [Scte35, nil]
53
110
  def scte35_out_info
54
111
  Scte35.parse(scte35_out) unless scte35_out.nil?
55
112
  end
56
113
 
114
+ # Parse SCTE-35 in data.
115
+ # @return [Scte35, nil]
57
116
  def scte35_in_info
58
117
  Scte35.parse(scte35_in) unless scte35_in.nil?
59
118
  end
60
119
 
120
+ def self.parse_client_attributes(attributes)
121
+ attributes.select do |key|
122
+ key.start_with?('X-') && !INTERSTITIAL_KEYS.include?(key)
123
+ end
124
+ end
125
+ private_class_method :parse_client_attributes
126
+
61
127
  private
62
128
 
63
129
  def formatted_attributes
64
130
  [%(ID="#{id}"),
65
- class_name_format,
131
+ quoted_format('CLASS', class_name),
66
132
  %(START-DATE="#{start_date}"),
67
- end_date_format,
68
- duration_format,
69
- planned_duration_format,
133
+ quoted_format('END-DATE', end_date),
134
+ unquoted_format('DURATION', duration),
135
+ unquoted_format('PLANNED-DURATION', planned_duration),
70
136
  client_attributes_format,
71
137
  interstitial_formats,
72
- scte35_cmd_format,
73
- scte35_out_format,
74
- scte35_in_format,
75
- cue_format,
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),
76
142
  end_on_next_format].flatten.compact.join(',')
77
143
  end
78
144
 
79
- def class_name_format
80
- quoted_format('CLASS', class_name)
81
- end
82
-
83
- def end_date_format
84
- quoted_format('END-DATE', end_date)
85
- end
86
-
87
- def duration_format
88
- unquoted_format('DURATION', duration)
89
- end
90
-
91
- def planned_duration_format
92
- unquoted_format('PLANNED-DURATION', planned_duration)
93
- end
94
-
95
145
  def client_attributes_format
96
146
  return if client_attributes.nil? || client_attributes.empty?
97
147
 
98
148
  client_attributes.map do |attribute|
99
149
  value = attribute.last
100
- value_format = decimal?(value) ? value : %("#{value}")
101
- "#{attribute.first}=#{value_format}"
150
+ fmt = decimal?(value) ? value : %("#{value}")
151
+ "#{attribute.first}=#{fmt}"
102
152
  end
103
153
  end
104
154
 
@@ -113,76 +163,16 @@ module M3u8
113
163
  end
114
164
  end
115
165
 
116
- def parse_interstitials(attributes)
117
- @asset_uri = attributes['X-ASSET-URI']
118
- @asset_list = attributes['X-ASSET-LIST']
119
- @resume_offset = parse_float(attributes['X-RESUME-OFFSET'])
120
- @playout_limit = parse_float(attributes['X-PLAYOUT-LIMIT'])
121
- @restrict = attributes['X-RESTRICT']
122
- @snap = attributes['X-SNAP']
123
- @timeline_occupies = attributes['X-TIMELINE-OCCUPIES']
124
- @timeline_style = attributes['X-TIMELINE-STYLE']
125
- @content_may_vary = attributes['X-CONTENT-MAY-VARY']
126
- end
127
-
128
166
  def interstitial_formats
129
- [asset_uri_format, asset_list_format,
130
- resume_offset_format, playout_limit_format,
131
- restrict_format, snap_format,
132
- timeline_occupies_format, timeline_style_format,
133
- content_may_vary_format]
134
- end
135
-
136
- def asset_uri_format
137
- quoted_format('X-ASSET-URI', asset_uri)
138
- end
139
-
140
- def asset_list_format
141
- quoted_format('X-ASSET-LIST', asset_list)
142
- end
143
-
144
- def resume_offset_format
145
- unquoted_format('X-RESUME-OFFSET', resume_offset)
146
- end
147
-
148
- def playout_limit_format
149
- unquoted_format('X-PLAYOUT-LIMIT', playout_limit)
150
- end
151
-
152
- def restrict_format
153
- quoted_format('X-RESTRICT', restrict)
154
- end
155
-
156
- def snap_format
157
- quoted_format('X-SNAP', snap)
158
- end
159
-
160
- def timeline_occupies_format
161
- quoted_format('X-TIMELINE-OCCUPIES', timeline_occupies)
162
- end
163
-
164
- def timeline_style_format
165
- quoted_format('X-TIMELINE-STYLE', timeline_style)
166
- end
167
-
168
- def content_may_vary_format
169
- quoted_format('X-CONTENT-MAY-VARY', content_may_vary)
170
- end
171
-
172
- def scte35_cmd_format
173
- unquoted_format('SCTE35-CMD', scte35_cmd)
174
- end
175
-
176
- def scte35_out_format
177
- unquoted_format('SCTE35-OUT', scte35_out)
178
- end
179
-
180
- def scte35_in_format
181
- unquoted_format('SCTE35-IN', scte35_in)
182
- end
183
-
184
- def cue_format
185
- quoted_format('CUE', cue)
167
+ [quoted_format('X-ASSET-URI', asset_uri),
168
+ quoted_format('X-ASSET-LIST', asset_list),
169
+ unquoted_format('X-RESUME-OFFSET', resume_offset),
170
+ unquoted_format('X-PLAYOUT-LIMIT', 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)]
186
176
  end
187
177
 
188
178
  def end_on_next_format
@@ -190,19 +180,5 @@ module M3u8
190
180
 
191
181
  'END-ON-NEXT=YES'
192
182
  end
193
-
194
- def quoted_format(key, value)
195
- %(#{key}="#{value}") unless value.nil?
196
- end
197
-
198
- def unquoted_format(key, value)
199
- "#{key}=#{value}" unless value.nil?
200
- end
201
-
202
- def parse_client_attributes(attributes)
203
- attributes.select do |key|
204
- key.start_with?('X-') && !INTERSTITIAL_KEYS.include?(key)
205
- end
206
- end
207
183
  end
208
184
  end
@@ -7,14 +7,22 @@ module M3u8
7
7
  class DefineItem
8
8
  extend M3u8
9
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
10
14
  attr_accessor :name, :value, :import, :queryparam
11
15
 
16
+ # @param params [Hash] attribute key-value pairs
12
17
  def initialize(params = {})
13
18
  params.each do |key, val|
14
19
  instance_variable_set("@#{key}", val)
15
20
  end
16
21
  end
17
22
 
23
+ # Parse an EXT-X-DEFINE tag.
24
+ # @param text [String] raw tag line
25
+ # @return [DefineItem]
18
26
  def self.parse(text)
19
27
  attributes = parse_attributes(text)
20
28
  DefineItem.new(
@@ -25,6 +33,8 @@ module M3u8
25
33
  )
26
34
  end
27
35
 
36
+ # Render as an m3u8 EXT-X-DEFINE tag.
37
+ # @return [String]
28
38
  def to_s
29
39
  "#EXT-X-DEFINE:#{formatted_attributes}"
30
40
  end
@@ -4,6 +4,8 @@ module M3u8
4
4
  # DiscontinuityItem represents a EXT-X-DISCONTINUITY tag to indicate a
5
5
  # discontinuity between the SegmentItems that proceed and follow it.
6
6
  class DiscontinuityItem
7
+ # Render as an m3u8 EXT-X-DISCONTINUITY tag.
8
+ # @return [String]
7
9
  def to_s
8
10
  "#EXT-X-DISCONTINUITY\n"
9
11
  end