m3u8 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
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