m3u8 0.7.0 → 0.7.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile +1 -0
  4. data/Guardfile +1 -0
  5. data/Rakefile +1 -0
  6. data/lib/m3u8.rb +6 -1
  7. data/lib/m3u8/byte_range.rb +1 -0
  8. data/lib/m3u8/date_range_item.rb +113 -0
  9. data/lib/m3u8/discontinuity_item.rb +1 -3
  10. data/lib/m3u8/encryptable.rb +1 -0
  11. data/lib/m3u8/error.rb +1 -0
  12. data/lib/m3u8/key_item.rb +1 -0
  13. data/lib/m3u8/map_item.rb +1 -0
  14. data/lib/m3u8/media_item.rb +1 -0
  15. data/lib/m3u8/playback_start.rb +1 -0
  16. data/lib/m3u8/playlist.rb +1 -0
  17. data/lib/m3u8/playlist_item.rb +12 -18
  18. data/lib/m3u8/reader.rb +17 -9
  19. data/lib/m3u8/segment_item.rb +1 -0
  20. data/lib/m3u8/session_data_item.rb +1 -0
  21. data/lib/m3u8/session_key_item.rb +1 -0
  22. data/lib/m3u8/time_item.rb +1 -0
  23. data/lib/m3u8/version.rb +2 -1
  24. data/lib/m3u8/writer.rb +1 -0
  25. data/spec/fixtures/date_range_scte35.m3u8 +16 -0
  26. data/spec/lib/m3u8/byte_range_spec.rb +1 -0
  27. data/spec/lib/m3u8/date_range_item_spec.rb +124 -0
  28. data/spec/lib/m3u8/discontinuity_item_spec.rb +1 -0
  29. data/spec/lib/m3u8/key_item_spec.rb +1 -0
  30. data/spec/lib/m3u8/map_item_spec.rb +2 -5
  31. data/spec/lib/m3u8/media_item_spec.rb +1 -0
  32. data/spec/lib/m3u8/playback_start_spec.rb +1 -0
  33. data/spec/lib/m3u8/playlist_item_spec.rb +1 -0
  34. data/spec/lib/m3u8/playlist_spec.rb +1 -0
  35. data/spec/lib/m3u8/reader_spec.rb +254 -236
  36. data/spec/lib/m3u8/segment_item_spec.rb +4 -14
  37. data/spec/lib/m3u8/session_data_item_spec.rb +1 -0
  38. data/spec/lib/m3u8/session_key_item_spec.rb +1 -0
  39. data/spec/lib/m3u8/time_item_spec.rb +1 -0
  40. data/spec/lib/m3u8/writer_spec.rb +174 -171
  41. data/spec/lib/m3u8_spec.rb +1 -0
  42. data/spec/spec_helper.rb +1 -0
  43. metadata +8 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cb897f32a24dabcd87ebd26a367425c8eae33113
4
- data.tar.gz: 5726036c12d84a49d24ae14f21cf52e817c3bc1c
3
+ metadata.gz: 75266714b1670b4990419287c300fa920604bf1c
4
+ data.tar.gz: af15a6e305fcbf82517d0b2858aed3fa848a80b2
5
5
  SHA512:
6
- metadata.gz: db0747bc9be34249f3fbf35d40255763e8b96cddcf9d857495361e8186df81fc4285e6740bc54abab804d2617504c77b8ac7befbb2cc2604e7de0a52d2963d65
7
- data.tar.gz: ee311cc358cca4494c6d5c1a4fa8e64e50977076d8001d3a1258ca1a4762354f6886b903293f06a424136044b6112e1f4c501dfe449508238d4e6d5e1e7be0dd
6
+ metadata.gz: 787197f4b3f4807c17125928e3b976c46f66b914b4bf7566c34e5748dd9200d84492c7fa858874d21cbbb9b7dbbfc9b91f04bac8954760a8e18689514459589e
7
+ data.tar.gz: 7d279886bd83eacfcd8e2e71463f6b96075dd5e3ed8d742d3b0c5ee7c0dc97932a7c07943fe5b49bad9e44c6631023f916479c5b0a7936c035d9d37adec79e74
@@ -1,5 +1,11 @@
1
+ 0.7.1 (2/23/2017) - Added support for the EXT-X-DATERANGE tag. Minor refactoring of existing classes.
2
+
3
+ ***
4
+
1
5
  0.7.0 (2/3/2017) - Added support for EXT-X-INDEPENDENT-SEGMENTS and EXT-X-START tags. Minor version bumped due to changes of default values for new playlists.
2
6
 
7
+ ***
8
+
3
9
  0.6.9 (4/24/2016) - Merged pull request #17 from [ghn](https://github.com/ghn). Removes return when parsing next line in playlist.
4
10
 
5
11
  ***
data/Gemfile CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  source 'https://rubygems.org'
2
3
 
3
4
  # Specify your gem's dependencies in m3u8.gemspec
data/Guardfile CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  guard :rspec, cmd: 'bundle exec rspec' do
2
3
  watch(%r{^spec/.+_spec\.rb$})
3
4
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'bundler/gem_tasks'
2
3
  require 'rspec/core/rake_task'
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'bigdecimal'
2
3
  require 'stringio'
3
4
  Dir[File.dirname(__FILE__) + '/m3u8/*.rb'].sort.each { |file| require file }
@@ -12,10 +13,14 @@ module M3u8
12
13
  end
13
14
 
14
15
  def parse_attributes(line)
15
- array = line.delete("\n").scan(/([A-z-]+)\s*=\s*("[^"]*"|[^,]*)/)
16
+ array = line.delete("\n").scan(/([A-z0-9-]+)\s*=\s*("[^"]*"|[^,]*)/)
16
17
  Hash[array.map { |key, value| [key, value.delete('"')] }]
17
18
  end
18
19
 
20
+ def parse_float(value)
21
+ value.nil? ? nil : value.to_f
22
+ end
23
+
19
24
  def parse_yes_no(value)
20
25
  value == 'YES' ? true : false
21
26
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # ByteRange represents sub range of a resource
3
4
  class ByteRange
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+ module M3u8
3
+ # DateRangeItem represents a #EXT-X-DATERANGE tag
4
+ class DateRangeItem
5
+ include M3u8
6
+ attr_accessor :id, :class_name, :start_date, :end_date, :duration,
7
+ :planned_duration, :scte35_cmd, :scte35_out, :scte35_in,
8
+ :end_on_next, :client_attributes
9
+
10
+ def initialize(options = {})
11
+ options.each do |key, value|
12
+ instance_variable_set("@#{key}", value)
13
+ end
14
+ end
15
+
16
+ def parse(text)
17
+ 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)
29
+ end
30
+
31
+ def to_s
32
+ "#EXT-X-DATERANGE:#{formatted_attributes}"
33
+ end
34
+
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(',')
49
+ end
50
+
51
+ def class_name_format
52
+ return if class_name.nil?
53
+ %(CLASS="#{class_name}")
54
+ end
55
+
56
+ def end_date_format
57
+ return if end_date.nil?
58
+ %(END-DATE="#{end_date}")
59
+ end
60
+
61
+ def duration_format
62
+ return if duration.nil?
63
+ "DURATION=#{duration}"
64
+ end
65
+
66
+ def planned_duration_format
67
+ return if planned_duration.nil?
68
+ "PLANNED-DURATION=#{planned_duration}"
69
+ end
70
+
71
+ def client_attributes_format
72
+ return if client_attributes.nil?
73
+ client_attributes.map do |attribute|
74
+ value = attribute.last
75
+ value_format = decimal?(value) ? value : %("#{value}")
76
+ "#{attribute.first}=#{value_format}"
77
+ end
78
+ end
79
+
80
+ def decimal?(value)
81
+ return true if value =~ /\A\d+\Z/
82
+ begin
83
+ return true if Float(value)
84
+ rescue
85
+ false
86
+ end
87
+ end
88
+
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}"
102
+ end
103
+
104
+ def end_on_next_format
105
+ return unless end_on_next
106
+ 'END-ON-NEXT=YES'
107
+ end
108
+
109
+ def parse_client_attributes(attributes)
110
+ attributes.select { |key| key.start_with?('X-') }
111
+ end
112
+ end
113
+ end
@@ -1,12 +1,10 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # DiscontinuityItem represents a EXT-X-DISCONTINUITY tag to indicate a
3
4
  # discontinuity between the SegmentItems that proceed and follow it.
4
5
  class DiscontinuityItem
5
6
  attr_accessor :tag
6
7
 
7
- def initialize
8
- end
9
-
10
8
  def to_s
11
9
  "#EXT-X-DISCONTINUITY\n"
12
10
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # Encapsulates logic common to encryption key tags
3
4
  module Encryptable
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  class PlaylistTypeError < StandardError
3
4
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # KeyItem represents a set of EXT-X-KEY attributes
3
4
  class KeyItem
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # MapItem represents a EXT-X-MAP tag which specifies how to obtain the Media
3
4
  # Initialization Section
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # MediaItem represents a set of EXT-X-MEDIA attributes
3
4
  class MediaItem
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # PlaybackStart represents a #EXT-X-START tag and attributes
3
4
  class PlaybackStart
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # Playlist represents an m3u8 playlist, it can be a master playlist or a set
3
4
  # of media segments
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # PlaylistItem represents a set of EXT-X-STREAM-INF or
3
4
  # EXT-X-I-FRAME-STREAM-INF attributes
@@ -36,17 +37,11 @@ module M3u8
36
37
  def codecs
37
38
  return @codecs unless @codecs.nil?
38
39
 
39
- video_codec = video_codec profile, level
40
+ video = video_codec(profile, level)
41
+ return audio_codec if video.nil?
42
+ return video if audio_codec.nil?
40
43
 
41
- if video_codec.nil?
42
- return audio_codec
43
- else
44
- if audio_codec.nil?
45
- return video_codec
46
- else
47
- return "#{video_codec},#{audio_codec}"
48
- end
49
- end
44
+ "#{video},#{audio_codec}"
50
45
  end
51
46
 
52
47
  def to_s
@@ -94,7 +89,7 @@ module M3u8
94
89
  end
95
90
 
96
91
  def validate
97
- fail MissingCodecError, MISSING_CODEC_MESSAGE if codecs.nil?
92
+ raise MissingCodecError, MISSING_CODEC_MESSAGE if codecs.nil?
98
93
  end
99
94
 
100
95
  def m3u8_format
@@ -177,18 +172,17 @@ module M3u8
177
172
 
178
173
  def audio_codec
179
174
  return if @audio_codec.nil?
180
- return 'mp4a.40.2' if @audio_codec.downcase == 'aac-lc'
181
- return 'mp4a.40.5' if @audio_codec.downcase == 'he-aac'
182
- return 'mp4a.40.34' if @audio_codec.downcase == 'mp3'
175
+ return 'mp4a.40.2' if @audio_codec.casecmp('aac-lc').zero?
176
+ return 'mp4a.40.5' if @audio_codec.casecmp('he-aac').zero?
177
+ return 'mp4a.40.34' if @audio_codec.casecmp('mp3').zero?
183
178
  end
184
179
 
185
180
  def video_codec(profile, level)
186
181
  return if profile.nil? || level.nil?
187
182
 
188
- profile.downcase!
189
- return baseline_codec_string(level) if profile == 'baseline'
190
- return main_codec_string(level) if profile == 'main'
191
- return high_codec_string(level) if profile == 'high'
183
+ return baseline_codec_string(level) if profile.casecmp('baseline').zero?
184
+ return main_codec_string(level) if profile.casecmp('main').zero?
185
+ return high_codec_string(level) if profile.casecmp('high').zero?
192
186
  end
193
187
 
194
188
  def baseline_codec_string(level)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # Reader provides parsing of m3u8 playlists
3
4
  class Reader
@@ -27,17 +28,20 @@ module M3u8
27
28
  end
28
29
 
29
30
  def media_segment_tags
30
- { '#EXTINF' => ->(line) { parse_segment(line) },
31
+ {
32
+ '#EXTINF' => ->(line) { parse_segment(line) },
31
33
  '#EXT-X-DISCONTINUITY' => ->(line) { parse_discontinuity(line) },
32
34
  '#EXT-X-BYTERANGE' => ->(line) { parse_byterange(line) },
33
35
  '#EXT-X-KEY' => ->(line) { parse_key(line) },
34
36
  '#EXT-X-MAP' => ->(line) { parse_map(line) },
35
- '#EXT-X-PROGRAM-DATE-TIME' => ->(line) { parse_time(line) }
37
+ '#EXT-X-PROGRAM-DATE-TIME' => ->(line) { parse_time(line) },
38
+ '#EXT-X-DATERANGE' => ->(line) { parse_date_range(line) }
36
39
  }
37
40
  end
38
41
 
39
42
  def media_playlist_tags
40
- { '#EXT-X-MEDIA-SEQUENCE' => ->(line) { parse_sequence(line) },
43
+ {
44
+ '#EXT-X-MEDIA-SEQUENCE' => ->(line) { parse_sequence(line) },
41
45
  '#EXT-X-ALLOW-CACHE' => ->(line) { parse_cache(line) },
42
46
  '#EXT-X-TARGETDURATION' => ->(line) { parse_target(line) },
43
47
  '#EXT-X-I-FRAMES-ONLY' => proc { playlist.iframes_only = true },
@@ -46,7 +50,8 @@ module M3u8
46
50
  end
47
51
 
48
52
  def master_playlist_tags
49
- { '#EXT-X-MEDIA' => ->(line) { parse_media(line) },
53
+ {
54
+ '#EXT-X-MEDIA' => ->(line) { parse_media(line) },
50
55
  '#EXT-X-SESSION-DATA' => ->(line) { parse_session_data(line) },
51
56
  '#EXT-X-SESSION-KEY' => ->(line) { parse_session_key(line) },
52
57
  '#EXT-X-STREAM-INF' => ->(line) { parse_stream(line) },
@@ -55,17 +60,14 @@ module M3u8
55
60
  end
56
61
 
57
62
  def universal_tags
58
- { '#EXT-X-START' => ->(line) { parse_start(line) },
63
+ {
64
+ '#EXT-X-START' => ->(line) { parse_start(line) },
59
65
  '#EXT-X-INDEPENDENT-SEGMENTS' => proc do
60
66
  playlist.independent_segments = true
61
67
  end
62
68
  }
63
69
  end
64
70
 
65
- def parse_independent_segments(line)
66
- parse_independent_segments
67
- end
68
-
69
71
  def parse_line(line)
70
72
  return if match_tag(line)
71
73
  parse_next_line(line) if !item.nil? && open
@@ -179,6 +181,12 @@ module M3u8
179
181
  end
180
182
  end
181
183
 
184
+ def parse_date_range(line)
185
+ item = M3u8::DateRangeItem.new
186
+ item.parse(line)
187
+ playlist.items << item
188
+ end
189
+
182
190
  def parse_next_line(line)
183
191
  value = line.delete("\n").delete("\r")
184
192
  if master
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # SegmentItem represents EXTINF attributes with the URI that follows,
3
4
  # optionally allowing an EXT-X-BYTERANGE tag to be set.
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # SessionDataItem represents a set of EXT-X-SESSION-DATA attributes
3
4
  class SessionDataItem
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # KeyItem represents a set of EXT-X-SESSION-KEY attributes
3
4
  class SessionKeyItem
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # TimeItem represents EXT-X-PROGRAM-DATE-TIME
3
4
  class TimeItem
@@ -1,4 +1,5 @@
1
+ # frozen_string_literal: true
1
2
  # M3u8 provides parsing, generation, and validation of m3u8 playlists
2
3
  module M3u8
3
- VERSION = '0.7.0'
4
+ VERSION = '0.7.1'
4
5
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module M3u8
2
3
  # Writer provides generation of text output of playlists in m3u8 format
3
4
  class Writer
@@ -0,0 +1,16 @@
1
+ #EXTM3U
2
+ #EXT-X-TARGETDURATION:60
3
+ #EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:
4
+ 15:00Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF0
5
+ 00014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50
6
+ 000008700000000
7
+ #EXTINF:20.0,
8
+ http://media.example.com/first.ts
9
+ #EXTINF:20.0,
10
+ http://media.example.com/second.ts
11
+ #EXTINF:20.0,
12
+ http://media.example.com/third.ts
13
+ #EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN=
14
+ 0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A00
15
+ 08029896F50000008700000000
16
+ #EXT-X-ENDLIST
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'spec_helper'
2
3
 
3
4
  describe M3u8::ByteRange do
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ describe M3u8::DateRangeItem do
5
+ describe '#new' do
6
+ it 'should assign attributes via option hash' do
7
+ options = { id: 'test_id', class_name: 'test_class',
8
+ start_date: '2014-03-05T11:15:00Z',
9
+ end_date: '2014-03-05T11:16:00Z', duration: 60.1,
10
+ planned_duration: 59.993,
11
+ scte35_out: '0xFC002F0000000000FF0',
12
+ scte35_in: '0xFC002F0000000000FF1',
13
+ scte35_cmd: '0xFC002F0000000000FF2', end_on_next: true,
14
+ client_attributes: { 'X-CUSTOM' => 45.3 } }
15
+ item = described_class.new(options)
16
+
17
+ expect(item.id).to eq('test_id')
18
+ expect(item.class_name).to eq('test_class')
19
+ expect(item.start_date).to eq('2014-03-05T11:15:00Z')
20
+ expect(item.end_date).to eq('2014-03-05T11:16:00Z')
21
+ expect(item.duration).to eq(60.1)
22
+ expect(item.planned_duration).to eq(59.993)
23
+ expect(item.scte35_out).to eq('0xFC002F0000000000FF0')
24
+ expect(item.scte35_in).to eq('0xFC002F0000000000FF1')
25
+ expect(item.scte35_cmd).to eq('0xFC002F0000000000FF2')
26
+ expect(item.end_on_next).to be true
27
+ expect(item.client_attributes.empty?).to be false
28
+ expect(item.client_attributes['X-CUSTOM']).to eq(45.3)
29
+ end
30
+ end
31
+
32
+ describe '#parse' do
33
+ it 'should parse m3u8 tag into instance' do
34
+ item = described_class.new
35
+ line = '#EXT-X-DATERANGE:ID="splice-6FFFFFF0",CLASS="test_class"' \
36
+ 'START-DATE="2014-03-05T11:15:00Z",' \
37
+ 'END-DATE="2014-03-05T11:16:00Z",DURATION=60.1,' \
38
+ 'PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF0,' \
39
+ 'SCTE35-IN=0xFC002F0000000000FF1,' \
40
+ 'SCTE35-CMD=0xFC002F0000000000FF2,' \
41
+ 'END-ON-NEXT=YES'
42
+ item.parse(line)
43
+
44
+ expect(item.id).to eq('splice-6FFFFFF0')
45
+ expect(item.class_name).to eq('test_class')
46
+ expect(item.start_date).to eq('2014-03-05T11:15:00Z')
47
+ expect(item.end_date).to eq('2014-03-05T11:16:00Z')
48
+ expect(item.duration).to eq(60.1)
49
+ expect(item.planned_duration).to eq(59.993)
50
+ expect(item.scte35_out).to eq('0xFC002F0000000000FF0')
51
+ expect(item.scte35_in).to eq('0xFC002F0000000000FF1')
52
+ expect(item.scte35_cmd).to eq('0xFC002F0000000000FF2')
53
+ expect(item.end_on_next).to be true
54
+ expect(item.client_attributes.empty?).to be true
55
+ end
56
+
57
+ it 'should ignore optional attributes' do
58
+ item = described_class.new
59
+ line = '#EXT-X-DATERANGE:ID="splice-6FFFFFF0",' \
60
+ 'START-DATE="2014-03-05T11:15:00Z"'
61
+ item.parse(line)
62
+
63
+ expect(item.id).to eq('splice-6FFFFFF0')
64
+ expect(item.class_name).to be_nil
65
+ expect(item.start_date).to eq('2014-03-05T11:15:00Z')
66
+ expect(item.end_date).to be_nil
67
+ expect(item.duration).to be_nil
68
+ expect(item.planned_duration).to be_nil
69
+ expect(item.scte35_out).to be_nil
70
+ expect(item.scte35_in).to be_nil
71
+ expect(item.scte35_cmd).to be_nil
72
+ expect(item.end_on_next).to be false
73
+ expect(item.client_attributes.empty?).to be true
74
+ end
75
+
76
+ it 'should parse client-defined attributes' do
77
+ item = described_class.new
78
+ line = '#EXT-X-DATERANGE:ID="splice-6FFFFFF0",' \
79
+ 'START-DATE="2014-03-05T11:15:00Z",' \
80
+ 'X-CUSTOM-VALUE="test_value",'
81
+ item.parse(line)
82
+
83
+ expect(item.client_attributes['X-CUSTOM-VALUE']).to eq('test_value')
84
+ end
85
+ end
86
+
87
+ describe '#to_s' do
88
+ it 'should render m3u8 tag' do
89
+ options = { id: 'test_id', class_name: 'test_class',
90
+ start_date: '2014-03-05T11:15:00Z',
91
+ end_date: '2014-03-05T11:16:00Z', duration: 60.1,
92
+ planned_duration: 59.993,
93
+ scte35_out: '0xFC002F0000000000FF0',
94
+ scte35_in: '0xFC002F0000000000FF1',
95
+ scte35_cmd: '0xFC002F0000000000FF2', end_on_next: true,
96
+ client_attributes: { 'X-CUSTOM' => 45.3,
97
+ 'X-CUSTOM-TEXT' => 'test_value' } }
98
+ item = described_class.new(options)
99
+
100
+ expected = '#EXT-X-DATERANGE:ID="test_id",CLASS="test_class",' \
101
+ 'START-DATE="2014-03-05T11:15:00Z",' \
102
+ 'END-DATE="2014-03-05T11:16:00Z",DURATION=60.1,' \
103
+ 'PLANNED-DURATION=59.993,' \
104
+ 'X-CUSTOM=45.3,' \
105
+ 'X-CUSTOM-TEXT="test_value",' \
106
+ 'SCTE35-CMD=0xFC002F0000000000FF2,' \
107
+ 'SCTE35-OUT=0xFC002F0000000000FF0,' \
108
+ 'SCTE35-IN=0xFC002F0000000000FF1,' \
109
+ 'END-ON-NEXT=YES'
110
+
111
+ expect(item.to_s).to eq(expected)
112
+ end
113
+
114
+ it 'should ignore optional attributes' do
115
+ options = { id: 'test_id', start_date: '2014-03-05T11:15:00Z' }
116
+ item = described_class.new(options)
117
+
118
+ expected = '#EXT-X-DATERANGE:ID="test_id",' \
119
+ 'START-DATE="2014-03-05T11:15:00Z"'
120
+
121
+ expect(item.to_s).to eq(expected)
122
+ end
123
+ end
124
+ end