rtcp 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 79f2f783d38dba6bdba307f5755c6132787b34589abae10722db511c4cd1c0c5
4
+ data.tar.gz: 3cd788f1b867909219ca14aeb97cdc791acce681fbcc1e806cb68436d82eaf5a
5
+ SHA512:
6
+ metadata.gz: ce3a1522f2ff96cfc9b87ccc17317cd884609b09f87a55b59f28b1d816374f44fd666c0e212d85baf1af8fde844043061b6c51daba48869895518d6ba60aa70e
7
+ data.tar.gz: b932fec91523c05f1f5e157e0eb548545877c49fe0543bc8784160ba640916ec1e248989c769951d244f687885eab44a442edc607c6eea14a9618c36bd0382ef
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/README.rdoc ADDED
@@ -0,0 +1,79 @@
1
+ = rtcp {<img src="https://codeclimate.com/github/rusch/rtcp.png" />}[https://codeclimate.com/github/rusch/rtcp] {<img src="https://travis-ci.org/rusch/rtcp.png?branch=master" alt="Build Status" />}[https://travis-ci.org/rusch/rtcp]
2
+
3
+
4
+ * https://github.com/rusch/rtcp
5
+ * http://www.ietf.org/rfc/rfc3550.txt
6
+ * http://www.ietf.org/rfc/rfc4585.txt
7
+
8
+ == Description
9
+
10
+ The RTP Control Protocol (RTCP) gathers statistical data about RTP sessions,
11
+ for example transmitted octet and packet counts, lost packet counts,
12
+ round-trip delay time and interarrival jitter.
13
+
14
+ This library parses RTCP data into ruby objects.
15
+
16
+ == Features
17
+
18
+ * Parse RTCP Packets into Ruby Objects
19
+ * Supports the following RTCP Packet types:
20
+ * 200: Sender Report
21
+ * 201: Receiver Report
22
+ * 202: Source Description
23
+ * 203: Goodbye
24
+ * 204: Application-Defined (Does not decode application-dependent data)
25
+ * 206: Payload-Specific FB message (Does not decode FCI block)
26
+ * 207: Extended Report
27
+ * 209: Receiver Summary Information
28
+
29
+ == Examples
30
+
31
+ === Parse an RTCP Packet
32
+
33
+ require 'rtcp'
34
+
35
+ rr = RTCP.decode(rtcp_rr_data) # => [RTCP::RR Object]
36
+ rr.ssrc # => 3945864703
37
+ rr.length # => 8
38
+ rr.version # => 2
39
+ rr.report_blocks # => []
40
+
41
+ === Parse a sequence of RTCP Packets
42
+
43
+ require 'rtcp'
44
+
45
+ msgs = RTCP.decode(rtcp_data) # => [Array of RTCP::* Objects]
46
+
47
+ msgs[0].class # => RTCP::RR
48
+ msgs[0].ssrc # => 3945864703
49
+
50
+ msgs[1].class # => RTCP::SDES
51
+ msgs[1].chunks # => [Array of Hashes]
52
+ msgs[1].chunks[0][:ssrc] # => 3945864703
53
+ msgs[1].chunks[0][:type] # => :cname
54
+ msgs[1].chunks[0][:data] # => "00-09-df-1b-ec-0a"
55
+
56
+ msgs[2].class # => RTCP::RSI
57
+ msgs[2].ssrc # => 3945864703
58
+ msge[2].ntp_timestamp # => [Time Object]
59
+
60
+ msgs[3].class # => RTCP::APP
61
+ msgs[3].name # => "PLII"
62
+ msgs[3].app_data # => [Binary Data -- The application data]
63
+
64
+ msgs[4].class # => RTCP
65
+ msgs[4].to_s # => [Binary Data -- The whole RTCP packet]
66
+
67
+ == Requirements
68
+
69
+ * Rubies (tested, at least):
70
+ * 1.9.3
71
+ * JRuby 1.7.3 (1.9 mode)
72
+
73
+ == Install
74
+
75
+ $ gem install
76
+
77
+ == Copyright
78
+
79
+ Copyright (c) 2013 Christian Rusch
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new
6
+
7
+ task test: :spec
8
+ task default: :test
data/lib/rtcp.rb ADDED
@@ -0,0 +1,99 @@
1
+ require_relative 'rtcp/version'
2
+ require_relative 'rtcp/decode_error'
3
+ require_relative 'rtcp/sr'
4
+ require_relative 'rtcp/rr'
5
+ require_relative 'rtcp/sdes'
6
+ require_relative 'rtcp/bye'
7
+ require_relative 'rtcp/xr'
8
+ require_relative 'rtcp/app'
9
+ require_relative 'rtcp/rsi'
10
+ require_relative 'rtcp/psfb'
11
+
12
+ class RTCP
13
+
14
+ attr_reader :length, :type_id
15
+
16
+ @@packet_classes = {}
17
+ self.constants.each do |sym|
18
+ const = self.const_get(sym)
19
+ if const.is_a?(Class) && const <= self
20
+ @@packet_classes[const::PT_ID] = const
21
+ end
22
+ end
23
+
24
+ # Decodes the supplied RTCP packet and returns it
25
+ def self.decode(data)
26
+ raise(RTCP::DecodeError, "Truncated Packet") if (data.length < 4)
27
+
28
+ packet_type, length = data.unpack('xCn')
29
+ length = 4 * (length + 1)
30
+ raise(RTCP::DecodeError, "Truncated Packet") if (data.length < length)
31
+
32
+ self.packet_class(packet_type).new.decode(data.slice(0..(length - 1)))
33
+ end
34
+
35
+ # Decodes all RTCP packets in the supplied string returns them in an array
36
+ def self.decode_all(data)
37
+ packets = []
38
+ while data && data.length > 0
39
+ packet = self.decode(data)
40
+ packets.push(packet)
41
+ data = data.slice(packet.length..-1)
42
+ end
43
+ packets
44
+ end
45
+
46
+ def decode(packet_data)
47
+ @type_id, length = packet_data.unpack('xCn')
48
+ @length = 4 * (length + 1)
49
+
50
+ @packet_data = packet_data
51
+ self
52
+ end
53
+
54
+ # Returns the packet as RTCP data string
55
+ def to_s
56
+ @packet_data
57
+ end
58
+
59
+ protected
60
+
61
+ # Ensures that the current RTCP Packet object is able to decode the RTCP
62
+ # packet with the given Packet Type ID.
63
+ #
64
+ # Raises an RTCP::DecodeError exception when this is not the case.
65
+ def ensure_packet_type(packet_type)
66
+ if packet_type != self.class::PT_ID
67
+ raise(RTCP::DecodeError, "Wrong Packet Type. packet_type=#{packet_type}")
68
+ end
69
+ end
70
+
71
+ # Extracts and returns the payload data from the given packet_data using the
72
+ # supplied packet length and header_length values.
73
+ #
74
+ # It also sets the @packet_data instance variable, which is currently used
75
+ # by the to_s method for returning the packet data.
76
+ #
77
+ # Raises an RTCP::DecodeError exception when the packet_data is shorter
78
+ # than packet_length.
79
+ def payload_data(packet_data, packet_length, header_length)
80
+ if packet_data.length > packet_length
81
+ @packet_data = packet_data[0..packet_length]
82
+ elsif packet_data.length == packet_length
83
+ @packet_data = packet_data
84
+ else
85
+ raise RTCP::DecodeError, "Truncated Packet"
86
+ end
87
+
88
+ @packet_data[header_length..-1]
89
+ end
90
+
91
+ private
92
+
93
+ # Returns the Class to use for handling RTCP packets of the given packet
94
+ # type.
95
+ def self.packet_class(packet_type)
96
+ @@packet_classes[packet_type] || self
97
+ end
98
+
99
+ end
data/lib/rtcp/app.rb ADDED
@@ -0,0 +1,36 @@
1
+ # APP: Application-Defined RTCP Packet
2
+ # Documentation: RFC 3550, 6.7
3
+ #
4
+ # 0 1 2 3
5
+ # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
6
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
7
+ # |V=2|P| subtype | PT=APP=204 | length |
8
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
9
+ # | SSRC/CSRC |
10
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
11
+ # | name (ASCII) |
12
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
13
+ # | application-dependent data ...
14
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
15
+ # 0 1 2 3
16
+
17
+ class RTCP::APP < RTCP
18
+
19
+ PT_ID = 204
20
+
21
+ attr_reader :version, :subtype, :ssrc, :name, :app_data
22
+
23
+ def decode(packet_data)
24
+ vpst, packet_type, length, @ssrc, @name = packet_data.unpack('CCnNa4')
25
+ ensure_packet_type(packet_type)
26
+
27
+ @length = 4 * (length + 1)
28
+ @version = vpst >> 6
29
+ @subtype = vpst & 31
30
+
31
+ @app_data = payload_data(packet_data, @length, 12)
32
+
33
+ self
34
+ end
35
+
36
+ end
data/lib/rtcp/bye.rb ADDED
@@ -0,0 +1,48 @@
1
+ # BYE: Goodbye RTCP Packet
2
+ # Documentation: RFC 3550, 6.6
3
+ #
4
+ # 0 1 2 3
5
+ # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
6
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
7
+ # |V=2|P| SC | PT=BYE=203 | length |
8
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
9
+ # | SSRC/CSRC |
10
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
11
+ # : ... :
12
+ # +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
13
+ # (opt) | length | reason for leaving ...
14
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
15
+
16
+ class RTCP::BYE < RTCP
17
+
18
+ PT_ID = 203
19
+
20
+ attr_reader :version, :ssrcs, :reason, :padding
21
+
22
+ def decode(packet_data)
23
+ vpsc, packet_type, length = packet_data.unpack('CCn')
24
+ ensure_packet_type(packet_type)
25
+
26
+ @length = 4 * (length + 1)
27
+ @version = vpsc >> 6
28
+ count = vpsc & 15
29
+
30
+ bye_data = payload_data(packet_data, @length, 4)
31
+
32
+ @ssrcs = bye_data.unpack("N#{count}")
33
+
34
+ if (4 * count) < bye_data.length
35
+ rlen, data = bye_data.unpack("x#{4 * count}Ca*")
36
+ @reason = data[0..(rlen - 1)]
37
+
38
+ # If the string fills the packet to the next 32-bit boundary,
39
+ # the string is not null terminated. If not, the BYE packet
40
+ # MUST be padded with null octets to the next 32- bit boundary.
41
+ # $TODO: Remove padding?
42
+
43
+ # $TODO: Check for/extract packet padding
44
+ end
45
+ self
46
+ end
47
+
48
+ end
@@ -0,0 +1,4 @@
1
+ class RTCP
2
+ class DecodeError < StandardError
3
+ end
4
+ end
data/lib/rtcp/psfb.rb ADDED
@@ -0,0 +1,55 @@
1
+ # PSFB: Payload-specific FB message
2
+ # Documentation: RFC 4585, 6.1.
3
+ #
4
+ # 0 1 2 3
5
+ # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
6
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
7
+ # |V=2|P| FMT | PT | length |
8
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
9
+ # | SSRC of packet sender |
10
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
11
+ # | SSRC of media source |
12
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
13
+ # : Feedback Control Information (FCI) :
14
+ # : :
15
+
16
+ class RTCP::PSFB < RTCP
17
+
18
+ FORMATS = {
19
+ 1 => :pli, # Picture Loss Indication (PLI)
20
+ 2 => :sli, # Slice Loss Indication (SLI)
21
+ 3 => :rpsi, # Reference Picture Selection Indication (RPSI)
22
+ 15 => :afb, # Application layer FB (AFB) message
23
+ }
24
+
25
+ PT_ID = 206
26
+
27
+ attr_reader :version, :format, :sender_ssrc, :source_ssrc, :fci,
28
+ :first_mb, :number, :picture_id
29
+
30
+ def decode(packet_data)
31
+ vpfmt, packet_type, length, @sender_ssrc, @source_ssrc =
32
+ packet_data.unpack('CCnN2')
33
+ ensure_packet_type(packet_type)
34
+
35
+ @length = 4 * (length + 1)
36
+ @version = vpfmt >> 6
37
+ format = vpfmt & 31
38
+ @format = FORMATS[format] || format
39
+
40
+ @fci_data = payload_data(packet_data, @length, 12)
41
+
42
+ case @format
43
+ when :sli
44
+ pl = @fci_data.unpack('L')
45
+ @first_mb = pl >> 19
46
+ @number = (pl >> 6) & 8191
47
+ @picture_id = pl & 63
48
+ # when :pli # No parameters
49
+ # when :rpsi
50
+ # when :afb
51
+ end
52
+ self
53
+ end
54
+
55
+ end
data/lib/rtcp/rr.rb ADDED
@@ -0,0 +1,45 @@
1
+ # RR: Receiver Report RTCP Packet
2
+ # Documentation: RFC 3550, 6.4.2
3
+ #
4
+ # 0 1 2 3
5
+ # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
6
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
7
+ # header |V=2|P| RC | PT=RR=201 | length |
8
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
9
+ # | SSRC of packet sender |
10
+ # +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
11
+ # report | SSRC_1 (SSRC of first source) |
12
+ # block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
13
+ # 1 | fraction lost | cumulative number of packets lost |
14
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
15
+ # | extended highest sequence number received |
16
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
17
+ # | interarrival jitter |
18
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
19
+ # | last SR (LSR) |
20
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
21
+ # | delay since last SR (DLSR) |
22
+ # +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
23
+ # report | SSRC_2 (SSRC of second source) |
24
+ # block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
25
+ # 2 : ... :
26
+ # +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
27
+ # | profile-specific extensions |
28
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
29
+
30
+ class RTCP::RR < RTCP::SR
31
+
32
+ PT_ID = 201
33
+
34
+ attr_reader :version, :ssrc, :report_blocks, :padding
35
+
36
+ def decode(packet_data)
37
+ vprc, packet_type, length, @ssrc = packet_data.unpack('CCnN')
38
+ ensure_packet_type(packet_type)
39
+ @length = 4 * (length + 1)
40
+ @version, @padding, rc = decode_vprc(vprc, @length - 8)
41
+ @report_blocks = decode_reports(payload_data(packet_data, @length, 8), rc)
42
+ self
43
+ end
44
+
45
+ end
data/lib/rtcp/rsi.rb ADDED
@@ -0,0 +1,82 @@
1
+ # RSI: Receiver Summary Information Packet
2
+ # Documentation: RFC 5760, 7.1.1.
3
+ #
4
+ # 0 1 2 3
5
+ # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
6
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
7
+ # |V=2|P|reserved | PT=RSI=209 | length |
8
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
9
+ # | SSRC |
10
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
11
+ # | Summarized SSRC |
12
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
13
+ # | NTP Timestamp (most significant word) |
14
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
15
+ # | NTP Timestamp (least significant word) |
16
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
17
+ # : Sub-report blocks :
18
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
19
+ # 0 1 2 3
20
+ #
21
+ # Sub-Report-Block Type
22
+ #
23
+ # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
24
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
25
+ # | SRBT | Length | |
26
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ SRBT-specific data +
27
+ # | |
28
+ # : :
29
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
30
+ #
31
+ # Generic Sub-Report Block Fields
32
+ #
33
+ # 0 1 2 3
34
+ # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
35
+ # +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
36
+ # | SRBT | Length | NDB | MF |
37
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
38
+ # | Minimum Distribution Value |
39
+ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
40
+ # | Maximum Distribution Value |
41
+ # +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
42
+ # | Distribution Buckets |
43
+ # | ... |
44
+ # | ... |
45
+ # +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
46
+
47
+ class RTCP::RSI < RTCP
48
+
49
+ PT_ID = 209
50
+
51
+ attr_reader :version, :ssrc, :summarized_ssrc, :ntp_timestamp, :report_blocks
52
+
53
+ def decode(packet_data)
54
+ vp, packet_type, length, @ssrc, @summarized_ssrc, ntp_h, ntp_l =
55
+ packet_data.unpack('CCnN4')
56
+ ensure_packet_type(packet_type)
57
+
58
+ @length = 4 * (length + 1)
59
+ @version = vp >> 6
60
+ @ntp_timestamp = Time.at(ntp_h - 2208988800 + (ntp_l.to_f / 0x100000000))
61
+ @report_blocks = decode_reports(payload_data(packet_data, @length, 20))
62
+ self
63
+ end
64
+
65
+ private
66
+
67
+ def decode_reports(data)
68
+ blocks = []
69
+ while data && data.length >= 2
70
+ type, len = report_block_data.unpack('CC')
71
+ if data.length < len
72
+ raise DecodeError, "Truncated Packet"
73
+ end
74
+ blocks.push({
75
+ type: type,
76
+ data: data.slice!(0..(len-1))
77
+ })
78
+ end
79
+ blocks
80
+ end
81
+
82
+ end