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