em-rtmp 0.0.3
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.
- data/Gemfile +3 -0
- data/README.md +26 -0
- data/Rakefile +96 -0
- data/lib/em-rtmp.rb +18 -0
- data/lib/em-rtmp/buffer.rb +33 -0
- data/lib/em-rtmp/connect_request.rb +84 -0
- data/lib/em-rtmp/connection.rb +189 -0
- data/lib/em-rtmp/connection_delegate.rb +60 -0
- data/lib/em-rtmp/handshake.rb +94 -0
- data/lib/em-rtmp/header.rb +193 -0
- data/lib/em-rtmp/heartbeat.rb +36 -0
- data/lib/em-rtmp/io_helpers.rb +192 -0
- data/lib/em-rtmp/logger.rb +48 -0
- data/lib/em-rtmp/message.rb +96 -0
- data/lib/em-rtmp/pending_request.rb +48 -0
- data/lib/em-rtmp/request.rb +101 -0
- data/lib/em-rtmp/response.rb +108 -0
- data/lib/em-rtmp/response_router.rb +130 -0
- data/lib/em-rtmp/rtmp.rb +30 -0
- data/lib/em-rtmp/uuid.rb +13 -0
- data/lib/em-rtmp/version.rb +5 -0
- metadata +146 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
require "em-rtmp/buffer"
|
2
|
+
require "em-rtmp/io_helpers"
|
3
|
+
|
4
|
+
module EventMachine
|
5
|
+
module RTMP
|
6
|
+
class ConnectionDelegate
|
7
|
+
include IOHelpers
|
8
|
+
|
9
|
+
attr_accessor :state
|
10
|
+
|
11
|
+
# Initialize the connection delegate by storing a reference to
|
12
|
+
# the connection
|
13
|
+
#
|
14
|
+
# Returns nothing.
|
15
|
+
def initialize(connection)
|
16
|
+
@connection = connection
|
17
|
+
@state = :none
|
18
|
+
end
|
19
|
+
|
20
|
+
# Connection Delegates send read operations directly to the connection
|
21
|
+
|
22
|
+
# Reads from the connection buffer
|
23
|
+
#
|
24
|
+
# length - Bytes to read
|
25
|
+
#
|
26
|
+
# Returns the result of the read
|
27
|
+
def read(length)
|
28
|
+
@connection.read length
|
29
|
+
end
|
30
|
+
|
31
|
+
# Connection Delegates send write operations directly to the connection
|
32
|
+
|
33
|
+
# Writes to the connection socket
|
34
|
+
#
|
35
|
+
# data - Data to write
|
36
|
+
#
|
37
|
+
# Returns the result of the write
|
38
|
+
def write(data)
|
39
|
+
@connection.write data
|
40
|
+
end
|
41
|
+
|
42
|
+
# Obtain the number of bytes waiting in the buffer
|
43
|
+
#
|
44
|
+
# Returns an Integer
|
45
|
+
def bytes_waiting
|
46
|
+
@connection.bytes_waiting
|
47
|
+
end
|
48
|
+
|
49
|
+
# Used to track changes in state
|
50
|
+
#
|
51
|
+
# Returns nothing
|
52
|
+
def change_state(state)
|
53
|
+
return if @state == state
|
54
|
+
Logger.info "state changed from #{@state} to #{state}", caller: caller
|
55
|
+
@state = state
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require "em-rtmp/connection_delegate"
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module RTMP
|
5
|
+
class Handshake < ConnectionDelegate
|
6
|
+
|
7
|
+
# The RTMP handshake involes each party sending 3 packets of data.
|
8
|
+
#
|
9
|
+
# Client Server
|
10
|
+
# --------------------------------------------
|
11
|
+
# 0x03 ->
|
12
|
+
# 1536 random bytes (a) ->
|
13
|
+
# <- 0x03
|
14
|
+
# <- 1536 random bytes (b)
|
15
|
+
# b ->
|
16
|
+
# <- a
|
17
|
+
#
|
18
|
+
# The handshake is completed by verifying that the response received
|
19
|
+
# matches the challenge string, with the notable exception that the
|
20
|
+
# first 4 bytes may be used for timestamping and can be different, and
|
21
|
+
# the second 4 bytes must all be zero.
|
22
|
+
|
23
|
+
HANDSHAKE_VERSION = 0x03
|
24
|
+
HANDSHAKE_LENGTH = 1536
|
25
|
+
|
26
|
+
# Handles a change to the buffer state
|
27
|
+
#
|
28
|
+
# Returns a symbol indicating our state
|
29
|
+
def buffer_changed
|
30
|
+
Logger.debug "#{state} #{bytes_waiting}"
|
31
|
+
case state
|
32
|
+
when :challenge_issued
|
33
|
+
if bytes_waiting >= (1 + HANDSHAKE_LENGTH)
|
34
|
+
handle_server_challenge
|
35
|
+
end
|
36
|
+
when :challenge_received
|
37
|
+
if bytes_waiting >= HANDSHAKE_LENGTH
|
38
|
+
handle_server_response
|
39
|
+
end
|
40
|
+
else
|
41
|
+
raise HandshakeError, "Reached unexpected state"
|
42
|
+
end
|
43
|
+
|
44
|
+
state
|
45
|
+
end
|
46
|
+
|
47
|
+
# Send the version byte followed by our challenge
|
48
|
+
#
|
49
|
+
# Returns a state update
|
50
|
+
def issue_challenge
|
51
|
+
Logger.debug "issuing client challenge"
|
52
|
+
|
53
|
+
@client_challenge = "\x00\x00\x00\x00\x00\x00\x00\x00" + (8...HANDSHAKE_LENGTH).map{rand(255)}.pack('C*')
|
54
|
+
|
55
|
+
write_uint8 HANDSHAKE_VERSION
|
56
|
+
write @client_challenge
|
57
|
+
|
58
|
+
change_state :challenge_issued
|
59
|
+
end
|
60
|
+
|
61
|
+
# Receives the server version byte and reissues it to the stream
|
62
|
+
#
|
63
|
+
# Returns a state update
|
64
|
+
def handle_server_challenge
|
65
|
+
Logger.debug "handling server challenge"
|
66
|
+
|
67
|
+
server_version = read_uint8
|
68
|
+
unless server_version == HANDSHAKE_VERSION
|
69
|
+
raise HandshakeError, "Expected version byte to be 0x03"
|
70
|
+
end
|
71
|
+
|
72
|
+
server_challenge = read(HANDSHAKE_LENGTH)
|
73
|
+
write server_challenge
|
74
|
+
change_state :challenge_received
|
75
|
+
end
|
76
|
+
|
77
|
+
# Reads the server response to our challenge to authenticate peer
|
78
|
+
#
|
79
|
+
# Returns a state update
|
80
|
+
def handle_server_response
|
81
|
+
Logger.debug "handling client response"
|
82
|
+
|
83
|
+
server_response = read(HANDSHAKE_LENGTH)
|
84
|
+
unless server_response == @client_challenge
|
85
|
+
raise HandshakeError, "Expected server to return client challenge"
|
86
|
+
end
|
87
|
+
|
88
|
+
Logger.debug "handshake complete"
|
89
|
+
change_state :handshake_complete
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module RTMP
|
3
|
+
class Header
|
4
|
+
|
5
|
+
# The packet header is read as follows:
|
6
|
+
#
|
7
|
+
# The first byte (8 bits) is read into two values:
|
8
|
+
# Header type (2 bits)
|
9
|
+
# Stream ID (6 bits)
|
10
|
+
#
|
11
|
+
# The header type determines a header length, from which the rest of the data may be read:
|
12
|
+
# TYPE BYTES DESCRIPTION
|
13
|
+
# 0b11 1 Just the header
|
14
|
+
# 0b10 4 Above plus timestamp (uint24 big endian)
|
15
|
+
# 0b01 8 Above plus body length (uint24 big endian) and message id (uint8 big endian)
|
16
|
+
# 0b00 12 Above plus Message stream id (uint32 little endian)
|
17
|
+
|
18
|
+
attr_accessor :header_length, :body_length, :channel_id,
|
19
|
+
:message_type_id, :message_stream_id, :timestamp
|
20
|
+
|
21
|
+
HEADER_DEFAULTS = {
|
22
|
+
header_length: 12,
|
23
|
+
timestamp: 0,
|
24
|
+
channel_id: 3,
|
25
|
+
message_type_id: 0,
|
26
|
+
message_stream_id: 0
|
27
|
+
}
|
28
|
+
|
29
|
+
# RTMP has a variable length header
|
30
|
+
# The keys below are binary values which correspond to expected byte lengths
|
31
|
+
# (0b00 in header length field would result in an expected 12 byte header)
|
32
|
+
HEADER_LENGTHS = {
|
33
|
+
0b00 => 12,
|
34
|
+
0b01 => 8,
|
35
|
+
0b10 => 4,
|
36
|
+
0b11 => 1
|
37
|
+
}
|
38
|
+
|
39
|
+
# RTMP uses a single byte to represent the message type
|
40
|
+
# These are the known values.
|
41
|
+
MESSAGE_TYPES = {
|
42
|
+
0x00 => :none,
|
43
|
+
0x01 => :chunk_size,
|
44
|
+
0x02 => :abort,
|
45
|
+
0x03 => :ack,
|
46
|
+
0x04 => :ping,
|
47
|
+
0x05 => :ack_size,
|
48
|
+
0x06 => :bandwidth,
|
49
|
+
0x08 => :audio,
|
50
|
+
0x09 => :video,
|
51
|
+
0x0f => :flex, # aka AMF3 data
|
52
|
+
0x10 => :amf3_shared_object, # documented as kMsgContainer=16
|
53
|
+
0x11 => :amf3,
|
54
|
+
0x12 => :invoke, # aka AMF0 data
|
55
|
+
0x13 => :amf0_shared_object, # documented as kMsgContainer=19
|
56
|
+
0x14 => :amf0,
|
57
|
+
0x16 => :flv # documented as aggregate
|
58
|
+
}
|
59
|
+
|
60
|
+
# Initialize and set instance variables
|
61
|
+
#
|
62
|
+
# attrs - Hash of instance variables
|
63
|
+
#
|
64
|
+
# Returns nothing
|
65
|
+
def initialize(attrs={})
|
66
|
+
attrs.each {|k,v| send("#{k}=", v)}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Inherit values from another header
|
70
|
+
#
|
71
|
+
# h - other Header object
|
72
|
+
#
|
73
|
+
# Returns self
|
74
|
+
def +(header)
|
75
|
+
keys = %w[header_length body_length channel_id message_type_id message_stream_id timestamp]
|
76
|
+
other_values = Hash[keys.map {|k| [k, header.instance_variable_get("@#{k}")]}]
|
77
|
+
other_values.each do |k, v|
|
78
|
+
unless v.nil?
|
79
|
+
send("#{k}=", v) unless v.nil?
|
80
|
+
end
|
81
|
+
end
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
# Retrieve the message type for our header
|
86
|
+
#
|
87
|
+
# Returns a symbol or nil
|
88
|
+
def message_type
|
89
|
+
MESSAGE_TYPES[message_type_id] || "unknown_type_#{message_type_id}".to_sym
|
90
|
+
end
|
91
|
+
|
92
|
+
# Set message type as symbol
|
93
|
+
#
|
94
|
+
# type - message type symbol
|
95
|
+
#
|
96
|
+
# Returns the id set
|
97
|
+
def message_type=(type)
|
98
|
+
self.message_type_id = MESSAGE_TYPES.invert[type]
|
99
|
+
end
|
100
|
+
|
101
|
+
# Encode the instantiated header to a buffer
|
102
|
+
#
|
103
|
+
# io - IO destination to write to
|
104
|
+
#
|
105
|
+
# Returns the buffer
|
106
|
+
def encode
|
107
|
+
|
108
|
+
# Set defaults if necessary
|
109
|
+
HEADER_DEFAULTS.each do |k, v|
|
110
|
+
send("#{k}=", v) if instance_variable_get("@#{k}").nil?
|
111
|
+
end
|
112
|
+
|
113
|
+
h_type = HEADER_LENGTHS.invert[header_length]
|
114
|
+
|
115
|
+
buffer = Buffer.new
|
116
|
+
buffer.write_bitfield [h_type, 2], [channel_id, 6]
|
117
|
+
|
118
|
+
if header_length >= 4
|
119
|
+
buffer.write_uint24_be timestamp
|
120
|
+
end
|
121
|
+
|
122
|
+
if header_length >= 8
|
123
|
+
buffer.write_uint24_be body_length
|
124
|
+
buffer.write_uint8 message_type_id
|
125
|
+
end
|
126
|
+
|
127
|
+
if header_length == 12
|
128
|
+
buffer.write_uint32_le message_stream_id
|
129
|
+
end
|
130
|
+
|
131
|
+
buffer.string
|
132
|
+
end
|
133
|
+
|
134
|
+
# Read the header from a connection
|
135
|
+
#
|
136
|
+
# connection - connection to use
|
137
|
+
#
|
138
|
+
# Returns a new header instance
|
139
|
+
def self.read_from_connection(stream)
|
140
|
+
header = new
|
141
|
+
|
142
|
+
begin
|
143
|
+
h_type, header.channel_id = stream.read_bitfield(2, 6)
|
144
|
+
rescue => e
|
145
|
+
raise HeaderError, "Unable to read header type byte from buffer: #{e}"
|
146
|
+
end
|
147
|
+
|
148
|
+
unless header.header_length = HEADER_LENGTHS[h_type]
|
149
|
+
raise HeaderError, "invalid header type #{h_type}"
|
150
|
+
end
|
151
|
+
|
152
|
+
# Stream ID may occupy up to two more bytes depending on the
|
153
|
+
# value of the channel_id we have read:
|
154
|
+
# 0 - value is second byte + 64
|
155
|
+
# 1 - value is third byte * 256 + second byte + 64
|
156
|
+
# 2 - low level protocol message (ignore)
|
157
|
+
|
158
|
+
if header.channel_id == 0x00
|
159
|
+
header.channel_id = stream.read_uint8 + 64
|
160
|
+
elsif header.channel_id == 0x01
|
161
|
+
header.channel_id = stream.read_uint8 + 64 + (stream.read_uint8 * 256)
|
162
|
+
end
|
163
|
+
|
164
|
+
# The timestamp is a 3-byte uint24. If this matches 0xffffff we will use another 4 bytes
|
165
|
+
# after the header as the real timestamp.
|
166
|
+
if header.header_length >= 4
|
167
|
+
header.timestamp = stream.read_uint24_be
|
168
|
+
end
|
169
|
+
|
170
|
+
# The next 3 bytes are the length of the object body, followed by a single byte
|
171
|
+
# representing the content type
|
172
|
+
if header.header_length >= 8
|
173
|
+
header.body_length = stream.read_uint24_be
|
174
|
+
header.message_type_id = stream.read_uint8
|
175
|
+
end
|
176
|
+
|
177
|
+
# The next 4 bytes are the stream ID
|
178
|
+
if header.header_length >= 12
|
179
|
+
header.message_stream_id = stream.read_uint32_le
|
180
|
+
end
|
181
|
+
|
182
|
+
# If the timestamp was 0xffffff, the next 4 bytes are the real timestamp
|
183
|
+
if header.timestamp == 0xffffff
|
184
|
+
header.timestamp = stream.read_uint32_be
|
185
|
+
end
|
186
|
+
|
187
|
+
header
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module RTMP
|
3
|
+
class Heartbeat < ConnectionDelegate
|
4
|
+
|
5
|
+
def initialize(connection)
|
6
|
+
super connection
|
7
|
+
end
|
8
|
+
|
9
|
+
def buffer_changed
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
def start
|
14
|
+
@timer ||= EventMachine::PeriodicTimer.new(15) do
|
15
|
+
pulse
|
16
|
+
end
|
17
|
+
|
18
|
+
@block = Proc.new do
|
19
|
+
Logger.debug "Heartbeat Pulsing"
|
20
|
+
end
|
21
|
+
|
22
|
+
@block.call
|
23
|
+
end
|
24
|
+
|
25
|
+
def pulse
|
26
|
+
@block.call
|
27
|
+
end
|
28
|
+
|
29
|
+
def cancel
|
30
|
+
@timer.cancel
|
31
|
+
@timer = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module RTMP
|
3
|
+
module IOHelpers
|
4
|
+
|
5
|
+
# Read a unsigned 8-bit integer
|
6
|
+
#
|
7
|
+
# Returns the result of the read
|
8
|
+
def read_uint8
|
9
|
+
read(1).unpack('C')[0]
|
10
|
+
end
|
11
|
+
|
12
|
+
# Write an unsigned 8-bit integer
|
13
|
+
#
|
14
|
+
# value - Value to write
|
15
|
+
#
|
16
|
+
# Returns the result of the stream operation
|
17
|
+
def write_uint8(value)
|
18
|
+
write [value].pack('C')
|
19
|
+
end
|
20
|
+
|
21
|
+
# Read a unsigned 16-bit integer
|
22
|
+
#
|
23
|
+
# Returns the result of the read
|
24
|
+
def read_uint16_be
|
25
|
+
read(2).unpack('n')[0]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Write an unsigned 16-bit integer
|
29
|
+
#
|
30
|
+
# value - Value to write
|
31
|
+
#
|
32
|
+
# Returns the result of the stream operation
|
33
|
+
def write_uint16_be(value)
|
34
|
+
write [value].pack('n')
|
35
|
+
end
|
36
|
+
|
37
|
+
# Read a unsigned 24-bit integer
|
38
|
+
#
|
39
|
+
# Returns the result of the read
|
40
|
+
def read_uint24_be
|
41
|
+
("\x00" + read(3)).unpack('N')[0]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Write an unsigned 24-bit integer
|
45
|
+
#
|
46
|
+
# value - Value to write
|
47
|
+
#
|
48
|
+
# Returns the result of the stream operation
|
49
|
+
def write_uint24_be(value)
|
50
|
+
write [value].pack('N')[1,3]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Read a unsigned 32-bit integer (big endian)
|
54
|
+
#
|
55
|
+
# Returns the result of the read
|
56
|
+
def read_uint32_be
|
57
|
+
read(4).unpack('N')[0]
|
58
|
+
end
|
59
|
+
|
60
|
+
# Read a unsigned 32-bit integer (little endian)
|
61
|
+
#
|
62
|
+
# Returns the result of the read
|
63
|
+
def read_uint32_le
|
64
|
+
read(4).unpack('V')[0]
|
65
|
+
end
|
66
|
+
|
67
|
+
# Write an unsigned 32-bit integer (big endian)
|
68
|
+
#
|
69
|
+
# value - Value to write
|
70
|
+
#
|
71
|
+
# Returns the result of the stream operation
|
72
|
+
def write_uint32_be(value)
|
73
|
+
write [value].pack('N')
|
74
|
+
end
|
75
|
+
|
76
|
+
# Write an unsigned 32-bit integer (big endian)
|
77
|
+
#
|
78
|
+
# value - Value to write
|
79
|
+
#
|
80
|
+
# Returns the result of the stream operation
|
81
|
+
def write_uint32_le(value)
|
82
|
+
write [value].pack('V')
|
83
|
+
end
|
84
|
+
|
85
|
+
# Read a double (big endian)
|
86
|
+
#
|
87
|
+
# Returns the result of the read
|
88
|
+
def read_double_be
|
89
|
+
read(8).unpack('G')[0]
|
90
|
+
end
|
91
|
+
|
92
|
+
# Write a double (big endian)
|
93
|
+
#
|
94
|
+
# value - Value to write
|
95
|
+
#
|
96
|
+
# Returns the result of the stream operation
|
97
|
+
def write_double_be(value)
|
98
|
+
write [value].pack('G')
|
99
|
+
end
|
100
|
+
|
101
|
+
# Read an int29
|
102
|
+
#
|
103
|
+
# Returns the result of the stream operation
|
104
|
+
def read_int29
|
105
|
+
count = 1
|
106
|
+
result = 0
|
107
|
+
byte = read_uint8
|
108
|
+
|
109
|
+
while (byte & 0x80 != 0) && count < 4 do
|
110
|
+
result <<= 7
|
111
|
+
result |= (byte & 0x7f)
|
112
|
+
byte = read_uint8
|
113
|
+
count += 1
|
114
|
+
end
|
115
|
+
|
116
|
+
if count < 4
|
117
|
+
result <<= 7
|
118
|
+
result |= byte
|
119
|
+
else
|
120
|
+
result <<= 8
|
121
|
+
result |= byte
|
122
|
+
end
|
123
|
+
|
124
|
+
result
|
125
|
+
end
|
126
|
+
|
127
|
+
# Write an int29
|
128
|
+
#
|
129
|
+
# value - Value to write
|
130
|
+
#
|
131
|
+
# Returns the result of the stream operation
|
132
|
+
def write_int29(value)
|
133
|
+
value = value & 0x1fffffff
|
134
|
+
if(value < 0x80)
|
135
|
+
result = [value].pack('c')
|
136
|
+
write result
|
137
|
+
elsif(value < 0x4000)
|
138
|
+
result = [value >> 7 & 0x7f | 0x80].pack('c') + [value & 0x7f].pack('c')
|
139
|
+
write result
|
140
|
+
elsif(value < 0x200000)
|
141
|
+
result = [value >> 14 & 0x7f | 0x80].pack('c') + [value >> 7 & 0x7f | 0x80].pack('c') + [value & 0x7f].pack('c')
|
142
|
+
write result
|
143
|
+
else
|
144
|
+
result = [value >> 22 & 0x7f | 0x80].pack('c') + [value >> 15 & 0x7f | 0x80].pack('c') + [value >> 8 & 0x7f | 0x80].pack('c') + [value & 0xff].pack('c')
|
145
|
+
write result
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Read a bit field and return the mapped results
|
150
|
+
#
|
151
|
+
# widths - Array of integers representing the size of the fields
|
152
|
+
#
|
153
|
+
# Returns the value for each field read
|
154
|
+
def read_bitfield(*widths)
|
155
|
+
byte = read_uint8
|
156
|
+
shifts_and_masks(widths).map{ |shift, mask|
|
157
|
+
(byte >> shift) & mask
|
158
|
+
}
|
159
|
+
end
|
160
|
+
|
161
|
+
# Write a bit field to the stream
|
162
|
+
#
|
163
|
+
# values_and_widths - An array of arrays, each containing two values:
|
164
|
+
# [0] - value to be written
|
165
|
+
# [1] - width of value
|
166
|
+
#
|
167
|
+
# Returns the value for each field read
|
168
|
+
def write_bitfield(*values_and_widths)
|
169
|
+
sm = shifts_and_masks(values_and_widths.map{ |_,w| w })
|
170
|
+
write_uint8 values_and_widths.zip(sm).inject(0){ |byte, ((value, width), (shift, mask))|
|
171
|
+
byte | ((value & mask) << shift)
|
172
|
+
}
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
# Obtain the shift and bitmasks for a set of widths
|
178
|
+
#
|
179
|
+
# Returns an array of arrays, each top-level array containing
|
180
|
+
# two values:
|
181
|
+
# [0] - shift
|
182
|
+
# [1] - mask
|
183
|
+
def shifts_and_masks(bit_widths)
|
184
|
+
(0 ... bit_widths.length).map{ |i| [
|
185
|
+
bit_widths[i+1 .. -1].inject(0){ |a,e| a + e },
|
186
|
+
0b1111_1111 >> (8 - bit_widths[i])
|
187
|
+
]}
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|