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
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # Parses SCTE-35 splice_info_section binary payloads from hex strings
5
+ class Scte35
6
+ class ParseError < StandardError; end
7
+
8
+ attr_reader :table_id, :pts_adjustment, :tier,
9
+ :splice_command_type, :splice_command, :descriptors
10
+
11
+ def initialize(options = {})
12
+ options.each do |key, value|
13
+ instance_variable_set("@#{key}", value)
14
+ end
15
+ end
16
+
17
+ def self.parse(hex_string)
18
+ raw = hex_string.sub(/\A0x/i, '')
19
+ data = [raw].pack('H*')
20
+ reader = Scte35BitReader.new(data)
21
+
22
+ header = parse_header(reader)
23
+ command = parse_command(reader, header)
24
+ descriptors = parse_descriptors(reader, header)
25
+
26
+ args = header.merge(splice_command: command,
27
+ descriptors: descriptors, raw: hex_string)
28
+ new(**args)
29
+ rescue NoMethodError, ArgumentError => e
30
+ raise ParseError, "invalid SCTE-35 data: #{e.message}"
31
+ end
32
+
33
+ def to_s
34
+ @raw
35
+ end
36
+
37
+ def self.parse_header(reader)
38
+ table_id = reader.read_bits(8)
39
+ reader.skip_bits(4) # section_syntax_indicator, private, reserved
40
+ section_length = reader.read_bits(12)
41
+ reader.read_bits(8) # protocol_version
42
+ reader.read_flag # encrypted_packet
43
+ reader.read_bits(6) # encryption_algorithm
44
+ pts_adjustment = reader.read_bits(33)
45
+ reader.read_bits(8) # cw_index
46
+ tier = reader.read_bits(12)
47
+ splice_command_length = reader.read_bits(12)
48
+ splice_command_type = reader.read_bits(8)
49
+
50
+ { table_id: table_id, pts_adjustment: pts_adjustment,
51
+ tier: tier, splice_command_type: splice_command_type,
52
+ splice_command_length: splice_command_length,
53
+ section_length: section_length }
54
+ end
55
+
56
+ def self.parse_command(reader, header)
57
+ cmd_length = command_data_length(reader, header)
58
+
59
+ case header[:splice_command_type]
60
+ when 0x00 then Scte35SpliceNull.new
61
+ when 0x05 then Scte35SpliceInsert.parse_from(reader, cmd_length)
62
+ when 0x06 then Scte35TimeSignal.parse_from(reader, cmd_length)
63
+ else reader.read_bytes(cmd_length)
64
+ end
65
+ end
66
+
67
+ def self.parse_splice_time(reader)
68
+ unless reader.read_flag # time_specified
69
+ reader.skip_bits(7) # reserved
70
+ return nil
71
+ end
72
+
73
+ reader.skip_bits(6) # reserved
74
+ reader.read_bits(33)
75
+ end
76
+
77
+ def self.parse_descriptors(reader, header)
78
+ # For unknown commands with 0xFFF, command consumed all
79
+ # remaining bytes (per spec), so no descriptors to parse
80
+ return [] if unknown_command_with_unspecified_length?(header)
81
+
82
+ desc_loop_length = reader.read_bits(16)
83
+ parse_descriptor_loop(reader, desc_loop_length)
84
+ end
85
+
86
+ def self.unknown_command_with_unspecified_length?(header)
87
+ header[:splice_command_length] == 0xFFF &&
88
+ ![0x00, 0x05, 0x06].include?(header[:splice_command_type])
89
+ end
90
+
91
+ def self.parse_descriptor_loop(reader, remaining)
92
+ descriptors = []
93
+ while remaining.positive?
94
+ tag = reader.read_bits(8)
95
+ length = reader.read_bits(8)
96
+ remaining -= 2 + length
97
+ descriptors << parse_single_descriptor(reader, tag, length)
98
+ end
99
+ descriptors
100
+ end
101
+
102
+ def self.parse_single_descriptor(reader, tag, length)
103
+ identifier = reader.read_bits(32)
104
+ if tag == Scte35SegmentationDescriptor::DESCRIPTOR_TAG &&
105
+ identifier == Scte35SegmentationDescriptor::CUEI_IDENTIFIER
106
+ Scte35SegmentationDescriptor.parse_from(reader, length)
107
+ else
108
+ reader.read_bytes(length - 4)
109
+ end
110
+ end
111
+
112
+ def self.command_data_length(reader, header)
113
+ length = header[:splice_command_length]
114
+ return length unless length == 0xFFF
115
+
116
+ if unknown_command_with_unspecified_length?(header)
117
+ # Unknown command: consume everything up to CRC
118
+ reader.bytes_remaining - 4
119
+ else
120
+ # Known command types parse their own fields; length unused
121
+ 0
122
+ end
123
+ end
124
+
125
+ private_class_method :parse_header, :parse_command,
126
+ :parse_descriptors, :command_data_length,
127
+ :parse_descriptor_loop, :parse_single_descriptor,
128
+ :unknown_command_with_unspecified_length?
129
+ end
130
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # Reads sub-byte bit fields from binary data for SCTE-35 parsing
5
+ class Scte35BitReader
6
+ def initialize(data)
7
+ @data = data.b
8
+ @byte_pos = 0
9
+ @bit_pos = 0
10
+ end
11
+
12
+ def read_bits(count)
13
+ value = 0
14
+ count.times do
15
+ value = (value << 1) | current_bit
16
+ advance_bit
17
+ end
18
+ value
19
+ end
20
+
21
+ def read_flag
22
+ read_bits(1) == 1
23
+ end
24
+
25
+ def read_bytes(count)
26
+ @data[@byte_pos, count].tap { @byte_pos += count }
27
+ end
28
+
29
+ def skip_bits(count)
30
+ count.times { advance_bit }
31
+ end
32
+
33
+ def bytes_remaining
34
+ @data.length - @byte_pos - (@bit_pos.positive? ? 1 : 0)
35
+ end
36
+
37
+ private
38
+
39
+ def current_bit
40
+ (@data.getbyte(@byte_pos) >> (7 - @bit_pos)) & 1
41
+ end
42
+
43
+ def advance_bit
44
+ @bit_pos += 1
45
+ return unless @bit_pos >= 8
46
+
47
+ @bit_pos = 0
48
+ @byte_pos += 1
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # Represents a segmentation_descriptor (tag 0x02) in SCTE-35
5
+ class Scte35SegmentationDescriptor
6
+ CUEI_IDENTIFIER = 0x43554549
7
+ DESCRIPTOR_TAG = 0x02
8
+
9
+ attr_reader :segmentation_event_id, :segmentation_event_cancel_indicator,
10
+ :segmentation_type_id, :segmentation_duration,
11
+ :segmentation_upid_type, :segmentation_upid,
12
+ :segment_num, :segments_expected
13
+
14
+ def initialize(options = {})
15
+ options.each do |key, value|
16
+ instance_variable_set("@#{key}", value)
17
+ end
18
+ end
19
+
20
+ def self.parse_from(reader, _length)
21
+ attrs = { segmentation_event_id: reader.read_bits(32) }
22
+ attrs[:segmentation_event_cancel_indicator] = reader.read_flag
23
+ reader.skip_bits(7) # reserved
24
+ return new(**attrs) if attrs[:segmentation_event_cancel_indicator]
25
+
26
+ parse_segmentation_detail(reader, attrs)
27
+ end
28
+
29
+ def self.parse_segmentation_detail(reader, attrs)
30
+ reader.read_flag # program_segmentation_flag
31
+ duration_flag = reader.read_flag
32
+ reader.skip_bits(6) # delivery_not_restricted + reserved/flags
33
+
34
+ attrs[:segmentation_duration] = reader.read_bits(40) if duration_flag
35
+ parse_upid(reader, attrs)
36
+ attrs[:segmentation_type_id] = reader.read_bits(8)
37
+ attrs[:segment_num] = reader.read_bits(8)
38
+ attrs[:segments_expected] = reader.read_bits(8)
39
+ new(**attrs)
40
+ end
41
+
42
+ def self.parse_upid(reader, attrs)
43
+ attrs[:segmentation_upid_type] = reader.read_bits(8)
44
+ upid_length = reader.read_bits(8)
45
+ return if upid_length.zero?
46
+
47
+ attrs[:segmentation_upid] =
48
+ reader.read_bytes(upid_length)
49
+ .force_encoding('UTF-8')
50
+ end
51
+
52
+ private_class_method :parse_segmentation_detail, :parse_upid
53
+ end
54
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # Represents a splice_insert SCTE-35 command (type 0x05)
5
+ class Scte35SpliceInsert
6
+ attr_reader :splice_event_id, :splice_event_cancel_indicator,
7
+ :out_of_network_indicator, :pts_time,
8
+ :break_duration, :break_auto_return,
9
+ :unique_program_id, :avail_num, :avails_expected
10
+
11
+ def initialize(options = {})
12
+ options.each do |key, value|
13
+ instance_variable_set("@#{key}", value)
14
+ end
15
+ end
16
+
17
+ def self.parse_from(reader, _length)
18
+ attrs = { splice_event_id: reader.read_bits(32) }
19
+ attrs[:splice_event_cancel_indicator] = reader.read_flag
20
+ reader.skip_bits(7) # reserved
21
+ return new(**attrs) if attrs[:splice_event_cancel_indicator]
22
+
23
+ parse_splice_detail(reader, attrs)
24
+ end
25
+
26
+ def self.parse_splice_detail(reader, attrs)
27
+ attrs[:out_of_network_indicator] = reader.read_flag
28
+ program_splice = reader.read_flag
29
+ duration_flag = reader.read_flag
30
+ immediate = reader.read_flag
31
+ reader.skip_bits(4) # reserved
32
+
33
+ if program_splice
34
+ attrs[:pts_time] = Scte35.parse_splice_time(reader) unless immediate
35
+ else
36
+ parse_components(reader, immediate)
37
+ end
38
+ parse_break_duration(reader, attrs) if duration_flag
39
+ attrs[:unique_program_id] = reader.read_bits(16)
40
+ attrs[:avail_num] = reader.read_bits(8)
41
+ attrs[:avails_expected] = reader.read_bits(8)
42
+ new(**attrs)
43
+ end
44
+
45
+ def self.parse_components(reader, immediate)
46
+ component_count = reader.read_bits(8)
47
+ component_count.times do
48
+ reader.read_bits(8) # component_tag
49
+ Scte35.parse_splice_time(reader) unless immediate
50
+ end
51
+ end
52
+
53
+ def self.parse_break_duration(reader, attrs)
54
+ attrs[:break_auto_return] = reader.read_flag
55
+ reader.skip_bits(6) # reserved
56
+ attrs[:break_duration] = reader.read_bits(33)
57
+ end
58
+
59
+ private_class_method :parse_splice_detail, :parse_break_duration,
60
+ :parse_components
61
+ end
62
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # Represents a splice_null SCTE-35 command (type 0x00).
5
+ # Contains no data fields.
6
+ class Scte35SpliceNull # rubocop:disable Lint/EmptyClass
7
+ end
8
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # Represents a time_signal SCTE-35 command (type 0x06)
5
+ class Scte35TimeSignal
6
+ attr_reader :pts_time
7
+
8
+ def initialize(options = {})
9
+ options.each do |key, value|
10
+ instance_variable_set("@#{key}", value)
11
+ end
12
+ end
13
+
14
+ def self.parse_from(reader, _length)
15
+ pts_time = Scte35.parse_splice_time(reader)
16
+ new(pts_time: pts_time)
17
+ end
18
+ end
19
+ end
@@ -1,24 +1,58 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  # SegmentItem represents EXTINF attributes with the URI that follows,
4
5
  # optionally allowing an EXT-X-BYTERANGE tag to be set.
5
6
  class SegmentItem
6
7
  include M3u8
8
+ include AttributeFormatter
9
+
10
+ # @return [Float, nil] segment duration in seconds
11
+ # @return [String, nil] segment URI
12
+ # @return [String, nil] human-readable comment after duration
13
+ # @return [TimeItem, Time, nil] program date-time
14
+ # @return [ByteRange, nil] byte range
7
15
  attr_accessor :duration, :segment, :comment, :program_date_time, :byterange
8
16
 
17
+ # @param params [Hash] attribute key-value pairs
9
18
  def initialize(params = {})
10
- intialize_with_byterange(params)
19
+ initialize_with_byterange(params)
20
+ end
21
+
22
+ # Parse an EXTINF tag line.
23
+ # @param text [String] raw tag line
24
+ # @return [SegmentItem]
25
+ def self.parse(text)
26
+ values = text.gsub('#EXTINF:', '')
27
+ .tr("\n", ',').split(',')
28
+ options = { duration: values[0].to_f }
29
+ options[:comment] = values[1] unless values[1].nil?
30
+ SegmentItem.new(options)
11
31
  end
12
32
 
33
+ # Render as an m3u8 EXTINF tag with segment URI.
34
+ # @return [String]
13
35
  def to_s
14
- date = "#{program_date_time}\n" unless program_date_time.nil?
15
- "#EXTINF:#{duration},#{comment}#{byterange_format}\n#{date}#{segment}"
36
+ "#EXTINF:#{decimal_format(duration)},#{comment}#{byterange_format}" \
37
+ "\n#{date_format}#{segment}"
38
+ end
39
+
40
+ def date_format
41
+ return if program_date_time.nil?
42
+
43
+ pdt = if program_date_time.is_a?(TimeItem)
44
+ program_date_time
45
+ else
46
+ TimeItem.new(time: program_date_time)
47
+ end
48
+ "#{pdt}\n"
16
49
  end
17
50
 
18
51
  private
19
52
 
20
53
  def byterange_format
21
54
  return if byterange.nil?
55
+
22
56
  "\n#EXT-X-BYTERANGE:#{byterange}"
23
57
  end
24
58
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # ServerControlItem represents an EXT-X-SERVER-CONTROL tag which
5
+ # provides directives for Low-Latency HLS delivery.
6
+ class ServerControlItem
7
+ extend M3u8
8
+ include AttributeFormatter
9
+
10
+ # @return [Float, nil] skip threshold in seconds
11
+ # @return [Boolean, nil] whether dateranges can be skipped
12
+ # @return [Float, nil] hold-back duration in seconds
13
+ # @return [Float, nil] part hold-back duration in seconds
14
+ # @return [Boolean, nil] whether blocking reload is supported
15
+ attr_accessor :can_skip_until, :can_skip_dateranges, :hold_back,
16
+ :part_hold_back, :can_block_reload
17
+
18
+ # @param params [Hash] attribute key-value pairs
19
+ def initialize(params = {})
20
+ params.each do |key, value|
21
+ instance_variable_set("@#{key}", value)
22
+ end
23
+ end
24
+
25
+ # Parse an EXT-X-SERVER-CONTROL tag.
26
+ # @param text [String] raw tag line
27
+ # @return [ServerControlItem]
28
+ def self.parse(text)
29
+ attributes = parse_attributes(text)
30
+ ServerControlItem.new(
31
+ can_skip_until: parse_float(attributes['CAN-SKIP-UNTIL']),
32
+ can_skip_dateranges:
33
+ parse_yes_no(attributes['CAN-SKIP-DATERANGES']),
34
+ hold_back: parse_float(attributes['HOLD-BACK']),
35
+ part_hold_back: parse_float(attributes['PART-HOLD-BACK']),
36
+ can_block_reload:
37
+ parse_yes_no(attributes['CAN-BLOCK-RELOAD'])
38
+ )
39
+ end
40
+
41
+ # Render as an m3u8 EXT-X-SERVER-CONTROL tag.
42
+ # @return [String]
43
+ def to_s
44
+ "#EXT-X-SERVER-CONTROL:#{formatted_attributes}"
45
+ end
46
+
47
+ private
48
+
49
+ def formatted_attributes
50
+ [unquoted_format('CAN-SKIP-UNTIL', can_skip_until),
51
+ can_skip_dateranges_format,
52
+ unquoted_format('HOLD-BACK', hold_back),
53
+ unquoted_format('PART-HOLD-BACK', part_hold_back),
54
+ can_block_reload_format].compact.join(',')
55
+ end
56
+
57
+ def can_skip_dateranges_format
58
+ return unless can_skip_dateranges
59
+
60
+ 'CAN-SKIP-DATERANGES=YES'
61
+ end
62
+
63
+ def can_block_reload_format
64
+ return unless can_block_reload
65
+
66
+ 'CAN-BLOCK-RELOAD=YES'
67
+ end
68
+ end
69
+ end
@@ -1,16 +1,27 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  # SessionDataItem represents a set of EXT-X-SESSION-DATA attributes
4
5
  class SessionDataItem
5
6
  extend M3u8
7
+ include AttributeFormatter
8
+
9
+ # @return [String, nil] DATA-ID value
10
+ # @return [String, nil] VALUE attribute
11
+ # @return [String, nil] URI attribute
12
+ # @return [String, nil] LANGUAGE attribute
6
13
  attr_accessor :data_id, :value, :uri, :language
7
14
 
15
+ # @param params [Hash] attribute key-value pairs
8
16
  def initialize(params = {})
9
17
  params.each do |key, value|
10
18
  instance_variable_set("@#{key}", value)
11
19
  end
12
20
  end
13
21
 
22
+ # Parse an EXT-X-SESSION-DATA tag.
23
+ # @param text [String] raw tag line
24
+ # @return [SessionDataItem]
14
25
  def self.parse(text)
15
26
  attributes = parse_attributes text
16
27
  options = { data_id: attributes['DATA-ID'], value: attributes['VALUE'],
@@ -18,36 +29,14 @@ module M3u8
18
29
  M3u8::SessionDataItem.new options
19
30
  end
20
31
 
32
+ # Render as an m3u8 EXT-X-SESSION-DATA tag.
33
+ # @return [String]
21
34
  def to_s
22
- attributes = [data_id_format,
23
- value_format,
24
- uri_format,
25
- language_format].compact.join(',')
35
+ attributes = [quoted_format('DATA-ID', data_id),
36
+ quoted_format('VALUE', value),
37
+ quoted_format('URI', uri),
38
+ quoted_format('LANGUAGE', language)].compact.join(',')
26
39
  "#EXT-X-SESSION-DATA:#{attributes}"
27
40
  end
28
-
29
- private
30
-
31
- def data_id_format
32
- %(DATA-ID="#{data_id}")
33
- end
34
-
35
- def value_format
36
- return if value.nil?
37
-
38
- %(VALUE="#{value}")
39
- end
40
-
41
- def uri_format
42
- return if uri.nil?
43
-
44
- %(URI="#{uri}")
45
- end
46
-
47
- def language_format
48
- return if language.nil?
49
-
50
- %(LANGUAGE="#{language}")
51
- end
52
41
  end
53
42
  end
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
- # KeyItem represents a set of EXT-X-SESSION-KEY attributes
4
+ # SessionKeyItem represents EXT-X-SESSION-KEY attributes
4
5
  class SessionKeyItem
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-SESSION-KEY tag.
18
+ # @param text [String] raw tag line
19
+ # @return [SessionKeyItem]
15
20
  def self.parse(text)
16
21
  attributes = parse_attributes(text)
17
22
  SessionKeyItem.new(attributes)
18
23
  end
19
24
 
25
+ # Render as an m3u8 EXT-X-SESSION-KEY tag.
26
+ # @return [String]
20
27
  def to_s
21
28
  "#EXT-X-SESSION-KEY:#{attributes_to_s}"
22
29
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # SkipItem represents an EXT-X-SKIP tag used in Playlist Delta
5
+ # Updates for Low-Latency HLS.
6
+ class SkipItem
7
+ extend M3u8
8
+ include AttributeFormatter
9
+
10
+ # @return [Integer, nil] number of skipped segments
11
+ # @return [String, nil] recently removed dateranges
12
+ attr_accessor :skipped_segments, :recently_removed_dateranges
13
+
14
+ # @param params [Hash] attribute key-value pairs
15
+ def initialize(params = {})
16
+ params.each do |key, value|
17
+ instance_variable_set("@#{key}", value)
18
+ end
19
+ end
20
+
21
+ # Parse an EXT-X-SKIP tag.
22
+ # @param text [String] raw tag line
23
+ # @return [SkipItem]
24
+ def self.parse(text)
25
+ attributes = parse_attributes(text)
26
+ SkipItem.new(
27
+ skipped_segments:
28
+ attributes['SKIPPED-SEGMENTS'].to_i,
29
+ recently_removed_dateranges:
30
+ attributes['RECENTLY-REMOVED-DATERANGES']
31
+ )
32
+ end
33
+
34
+ # Render as an m3u8 EXT-X-SKIP tag.
35
+ # @return [String]
36
+ def to_s
37
+ "#EXT-X-SKIP:#{formatted_attributes}"
38
+ end
39
+
40
+ private
41
+
42
+ def formatted_attributes
43
+ [unquoted_format('SKIPPED-SEGMENTS', skipped_segments),
44
+ quoted_format('RECENTLY-REMOVED-DATERANGES',
45
+ recently_removed_dateranges)].compact.join(',')
46
+ end
47
+ end
48
+ end
@@ -1,22 +1,31 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  # TimeItem represents EXT-X-PROGRAM-DATE-TIME
4
5
  class TimeItem
5
6
  extend M3u8
7
+
8
+ # @return [Time, String, nil] program date-time value
6
9
  attr_accessor :time
7
10
 
11
+ # @param params [Hash] :time value
8
12
  def initialize(params = {})
9
13
  params.each do |key, value|
10
14
  instance_variable_set("@#{key}", value)
11
15
  end
12
16
  end
13
17
 
18
+ # Parse an EXT-X-PROGRAM-DATE-TIME tag.
19
+ # @param text [String] raw tag line
20
+ # @return [TimeItem]
14
21
  def self.parse(text)
15
22
  time = text.gsub('#EXT-X-PROGRAM-DATE-TIME:', '')
16
23
  options = { time: Time.parse(time) }
17
24
  TimeItem.new(options)
18
25
  end
19
26
 
27
+ # Render as an m3u8 EXT-X-PROGRAM-DATE-TIME tag.
28
+ # @return [String]
20
29
  def to_s
21
30
  %(#EXT-X-PROGRAM-DATE-TIME:#{time_format})
22
31
  end
@@ -25,6 +34,7 @@ module M3u8
25
34
 
26
35
  def time_format
27
36
  return time if time.is_a?(String)
37
+
28
38
  time.iso8601
29
39
  end
30
40
  end
data/lib/m3u8/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  # M3u8 provides parsing, generation, and validation of m3u8 playlists
4
4
  module M3u8
5
- VERSION = '0.8.2'
5
+ VERSION = '1.8.1'
6
6
  end