m3u8 1.6.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 +31 -0
  6. data/Gemfile +6 -0
  7. data/LICENSE.txt +1 -1
  8. data/README.md +33 -2
  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 +118 -72
  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 +73 -13
  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: 1fb7826061b077f6e56712067f673393591d628e4e35a895af1910ff7fae859d
4
- data.tar.gz: 8e0298d7fb9806e4fa2d3bc66b522a6f7bf7d0bbad1a57fcefeafeac7255a1f4
3
+ metadata.gz: 395231edf55669bc7358629d116d5f7486a74bf1e7c5f6f9287497b758178511
4
+ data.tar.gz: f1592e2cd9cc9d175e3f06c7b330baffea7271ff1c153895865c35ed96e1c17e
5
5
  SHA512:
6
- metadata.gz: 20c066c9f250d0d7948c3211f958bf553d8385d07f8715274b2738d3c497cf8cf450790489f80ccca88a1e09a67fc43411f055efcc16bf6f9fa07efed43ed407
7
- data.tar.gz: 515ba07430d27872130625dc1d5eac52ebd68a76c20306ff4443b9829f74b3d3a7ceee8d63f118a7e832c6f48a7742e6157eb5bff05e26c198f262566f443c78
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,34 @@
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
+
19
+ **1.7.0**
20
+
21
+ * Added HLS Interstitials first-class `DateRangeItem` accessors:
22
+ `asset_uri`, `asset_list`, `resume_offset`, `playout_limit`,
23
+ `restrict`, `snap`, `timeline_occupies`, `timeline_style`, and
24
+ `content_may_vary`.
25
+ * Promoted supported interstitial `X-` attributes out of
26
+ `client_attributes` into typed fields during parsing and formatting.
27
+ * Refactored `DateRangeItem` attribute formatting helpers to reduce
28
+ duplication while preserving output behavior.
29
+
30
+ ***
31
+
1
32
  **1.6.0**
2
33
 
3
34
  * Added SCTE-35 parsing with `M3u8::Scte35` for `splice_info_section`
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
 
@@ -253,12 +253,43 @@ Insert a timed metadata date range:
253
253
  ```ruby
254
254
  item = M3u8::DateRangeItem.new(
255
255
  id: 'ad-break-1', start_date: '2024-06-01T12:00:00Z',
256
- planned_duration: 30.0,
256
+ planned_duration: 30.0, cue: 'PRE',
257
257
  client_attributes: { 'X-AD-ID' => '"foo"' }
258
258
  )
259
259
  playlist.items << item
260
260
  ```
261
261
 
262
+ #### HLS Interstitials
263
+
264
+ `DateRangeItem` supports [HLS Interstitials](https://developer.apple.com/documentation/http-live-streaming/providing-an-hls-interstitial) attributes as first-class accessors for ad insertion, pre/post-rolls, and timeline integration:
265
+
266
+ ```ruby
267
+ item = M3u8::DateRangeItem.new(
268
+ id: 'ad-break-1',
269
+ class_name: 'com.apple.hls.interstitial',
270
+ start_date: '2024-06-01T12:00:00Z',
271
+ asset_uri: 'http://example.com/ad.m3u8',
272
+ resume_offset: 0.0,
273
+ playout_limit: 30.0,
274
+ restrict: 'SKIP,JUMP',
275
+ snap: 'OUT',
276
+ content_may_vary: 'YES'
277
+ )
278
+ playlist.items << item
279
+ ```
280
+
281
+ | HLS Attribute | Accessor | Type |
282
+ |----------------------|---------------------|--------|
283
+ | X-ASSET-URI | `asset_uri` | String |
284
+ | X-ASSET-LIST | `asset_list` | String |
285
+ | X-RESUME-OFFSET | `resume_offset` | Float |
286
+ | X-PLAYOUT-LIMIT | `playout_limit` | Float |
287
+ | X-RESTRICT | `restrict` | String |
288
+ | X-SNAP | `snap` | String |
289
+ | X-TIMELINE-OCCUPIES | `timeline_occupies` | String |
290
+ | X-TIMELINE-STYLE | `timeline_style` | String |
291
+ | X-CONTENT-MAY-VARY | `content_may_vary` | String |
292
+
262
293
  Signal an encoding discontinuity:
263
294
 
264
295
  ```ruby
@@ -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,87 +3,143 @@
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
- :end_on_next, :client_attributes
11
-
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
12
45
  def initialize(options = {})
13
46
  options.each do |key, value|
14
47
  instance_variable_set("@#{key}", value)
15
48
  end
16
49
  end
17
50
 
18
- 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)
19
55
  attributes = parse_attributes(text)
20
- @id = attributes['ID']
21
- @class_name = attributes['CLASS']
22
- @start_date = attributes['START-DATE']
23
- @end_date = attributes['END-DATE']
24
- @duration = parse_float(attributes['DURATION'])
25
- @planned_duration = parse_float(attributes['PLANNED-DURATION'])
26
- @scte35_cmd = attributes['SCTE35-CMD']
27
- @scte35_out = attributes['SCTE35-OUT']
28
- @scte35_in = attributes['SCTE35-IN']
29
- @end_on_next = attributes.key?('END-ON-NEXT')
30
- @client_attributes = parse_client_attributes(attributes)
31
- end
32
-
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]
33
98
  def to_s
34
99
  "#EXT-X-DATERANGE:#{formatted_attributes}"
35
100
  end
36
101
 
102
+ # Parse SCTE-35 command data.
103
+ # @return [Scte35, nil]
37
104
  def scte35_cmd_info
38
105
  Scte35.parse(scte35_cmd) unless scte35_cmd.nil?
39
106
  end
40
107
 
108
+ # Parse SCTE-35 out data.
109
+ # @return [Scte35, nil]
41
110
  def scte35_out_info
42
111
  Scte35.parse(scte35_out) unless scte35_out.nil?
43
112
  end
44
113
 
114
+ # Parse SCTE-35 in data.
115
+ # @return [Scte35, nil]
45
116
  def scte35_in_info
46
117
  Scte35.parse(scte35_in) unless scte35_in.nil?
47
118
  end
48
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
+
49
127
  private
50
128
 
51
129
  def formatted_attributes
52
130
  [%(ID="#{id}"),
53
- class_name_format,
131
+ quoted_format('CLASS', class_name),
54
132
  %(START-DATE="#{start_date}"),
55
- end_date_format,
56
- duration_format,
57
- planned_duration_format,
133
+ quoted_format('END-DATE', end_date),
134
+ unquoted_format('DURATION', duration),
135
+ unquoted_format('PLANNED-DURATION', planned_duration),
58
136
  client_attributes_format,
59
- scte35_cmd_format,
60
- scte35_out_format,
61
- scte35_in_format,
62
- end_on_next_format].compact.join(',')
63
- end
64
-
65
- def class_name_format
66
- return if class_name.nil?
67
-
68
- %(CLASS="#{class_name}")
69
- end
70
-
71
- def end_date_format
72
- return if end_date.nil?
73
-
74
- %(END-DATE="#{end_date}")
75
- end
76
-
77
- def duration_format
78
- return if duration.nil?
79
-
80
- "DURATION=#{duration}"
81
- end
82
-
83
- def planned_duration_format
84
- return if planned_duration.nil?
85
-
86
- "PLANNED-DURATION=#{planned_duration}"
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(',')
87
143
  end
88
144
 
89
145
  def client_attributes_format
@@ -91,8 +147,8 @@ module M3u8
91
147
 
92
148
  client_attributes.map do |attribute|
93
149
  value = attribute.last
94
- value_format = decimal?(value) ? value : %("#{value}")
95
- "#{attribute.first}=#{value_format}"
150
+ fmt = decimal?(value) ? value : %("#{value}")
151
+ "#{attribute.first}=#{fmt}"
96
152
  end
97
153
  end
98
154
 
@@ -107,22 +163,16 @@ module M3u8
107
163
  end
108
164
  end
109
165
 
110
- def scte35_cmd_format
111
- return if scte35_cmd.nil?
112
-
113
- "SCTE35-CMD=#{scte35_cmd}"
114
- end
115
-
116
- def scte35_out_format
117
- return if scte35_out.nil?
118
-
119
- "SCTE35-OUT=#{scte35_out}"
120
- end
121
-
122
- def scte35_in_format
123
- return if scte35_in.nil?
124
-
125
- "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', 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)]
126
176
  end
127
177
 
128
178
  def end_on_next_format
@@ -130,9 +180,5 @@ module M3u8
130
180
 
131
181
  'END-ON-NEXT=YES'
132
182
  end
133
-
134
- def parse_client_attributes(attributes)
135
- attributes.select { |key| key.start_with?('X-') }
136
- end
137
183
  end
138
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