vncrec 1.0.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.
@@ -0,0 +1,202 @@
1
+ module VNCRec
2
+ module RFB
3
+
4
+ module EncZRLE
5
+
6
+ class Stream
7
+
8
+ def initialize(io,bitspp,depth)
9
+ @io = io
10
+ @zstream = Zlib::Inflate.new
11
+ @bpp_orig = (bitspp.to_f/8.0).ceil
12
+ @bpp = case bitspp
13
+ when 32 then
14
+ @depth <= 24 ? 3 : 4
15
+ when 8 then
16
+ 1
17
+ else
18
+ raise "Cannot handle such pixel format"
19
+ end
20
+ @depth = depth
21
+ end
22
+
23
+ def read_zchunk
24
+ zdata_len = (@io.readpartial 4).unpack("L>")[0]
25
+ zdata = ""
26
+ to_read = zdata_len
27
+
28
+ while zdata.size < zdata_len
29
+ zdata += @io.read(to_read)
30
+ to_read = zdata_len - zdata.size
31
+ end
32
+ return zdata
33
+ end
34
+
35
+ def read_rect(w,h)
36
+ fb = Array.new(w*h*@bpp)
37
+ data = ""
38
+ begin
39
+ data = @zstream.inflate(read_zchunk)
40
+ rescue Zlib::DataError
41
+ return
42
+ end
43
+
44
+ stream = StringIO.new data
45
+
46
+ tile_cols = (w.to_f/64).ceil
47
+ tile_rows = (h.to_f/64).ceil
48
+ tile_cols_rem = w % 64
49
+ tile_rows_rem = h % 64
50
+
51
+ tile_rows.times do |tile_row_num|
52
+ tile_cols.times do |tile_col_num|
53
+ th = if ((tile_row_num == tile_rows-1) and (tile_rows_rem > 0))
54
+ tile_rows_rem
55
+ else
56
+ 64
57
+ end
58
+ tw = if ((tile_col_num == tile_cols-1) and (tile_cols_rem > 0))
59
+ tile_cols_rem
60
+ else
61
+ 64
62
+ end
63
+
64
+ subenc = stream.readbyte
65
+ tile = case subenc
66
+ when 0 then #Raw
67
+ tile = Array.new(tw*th)
68
+ th.times do
69
+ tile << (stream.read tw*@bpp_orig).unpack("C*").join
70
+ end
71
+ tile
72
+ when 1 then #Solid
73
+ Array.new(tw*th, stream.read(@bpp_orig))
74
+ when 2..16 then #Packed palette
75
+ handle_ZRLE_packed_palette(stream, subenc, tw,th)
76
+ when 128 then #Plain RLE
77
+ handle_ZRLE_plain_RLE_tile(stream,tw,th)
78
+ when 130..255 then #RLE w/ palette
79
+ handle_ZRLE_palette_RLE_tile(stream,subenc-128, tw,th)
80
+ end
81
+
82
+ th.times do |y|
83
+ boline = (64 * tile_row_num + y) * w
84
+ offx = 64 * tile_col_num
85
+ fb[(boline+offx)...(boline+offx+tw)] = tile[(tw*y)...(tw*(y+1))]
86
+ end
87
+ end #tile_col.times
88
+ end#tile_row.times
89
+ return fb.join
90
+ end
91
+
92
+ def handle_ZRLE_palette_RLE_tile(stream,psize,tw=64,th=64)
93
+ palette = []
94
+ pixels = Array.new(tw*th)
95
+ psize.times do
96
+ palette << stream.read(@bpp)
97
+ end
98
+ len = 0
99
+ begin
100
+ while len < tw*th
101
+ id = stream.read(1).unpack("C")[0]
102
+ #
103
+ #+--------+--------+--------+--------+
104
+ #| id | 255 | .. | <255 |
105
+ #+--------+--------+--------+--------+
106
+ #
107
+ if (id & 0b10000000) == 0
108
+ rl = 1
109
+ else
110
+ id -= 128
111
+ rl = 0
112
+ rem = 0
113
+ while (rem = stream.readbyte) == 255
114
+ rl += 255
115
+ end
116
+ rl += rem + 1
117
+ end
118
+ pixels[len...(len+rl)] = Array.new(rl, palette[id]) #TODO: if rl == 1
119
+ len += rl
120
+ end
121
+ rescue EOFError
122
+ end
123
+ pixels
124
+ end
125
+
126
+ def handle_ZRLE_plain_RLE_tile(stream,tw=64,th=64)
127
+ pixels = Array.new(tw*th)
128
+ len = 0
129
+ begin
130
+ while len < tw*th
131
+ color = stream.read(@bpp)
132
+ #
133
+ #+--------+--------+--------+--------+
134
+ #| color | 255 | .. | <255 |
135
+ #+--------+--------+--------+--------+
136
+ #
137
+ rl = 0
138
+ rem = 0
139
+ while (rem = stream.readbyte) == 255
140
+ rl += 255
141
+ end
142
+ rl += rem + 1
143
+ pixels[len...(len+rl)] = Array.new(rl, color) #TODO: if rl == 1
144
+ len += rl
145
+ end
146
+ rescue EOFError
147
+ end
148
+ pixels
149
+ end
150
+
151
+ def handle_ZRLE_packed_palette(stream, psize, tw=64, th=64)
152
+ pixels = Array.new(tw*th, 0)
153
+ bitspp = case psize
154
+ when 2 then 1
155
+ when 3..4 then 2
156
+ when 5..16 then 4
157
+ else
158
+ return pixels
159
+ end
160
+ palette = []
161
+ psize.times do
162
+ palette << stream.read(@bpp).unpack("C*")
163
+ end
164
+ count = case psize
165
+ when 2 then th*((tw+7)/8)
166
+ when 3..4 then th*((tw+3)/4)
167
+ when 5..16 then th*((tw+1)/2)
168
+ end
169
+ off_bits = 0
170
+ bits_per_row = bitspp * tw
171
+ padding_bits = bits_per_row % 8
172
+ encoded_len_bits = 64 * (bits_per_row + padding_bits)
173
+ encoded = stream.read(count).unpack("C*")
174
+ pixnum = 0
175
+ while off_bits < (encoded_len_bits - padding_bits - bitspp)
176
+ b1 = encoded[off_bits/8]
177
+ b2 = encoded[off_bits/8 + 1] || 0
178
+ b1 <<= 8
179
+ pixels[pixnum] = palette[h_bitmask(b2 + b1, bitspp, off_bits % 16)].pack("C*")
180
+ off_bits += if (off_bits % bits_per_row) > (bits_per_row - padding_bits) and (padding_bits > 0)
181
+ bitspp + padding_bits
182
+ else
183
+ bitspp
184
+ end
185
+ pixnum += 1
186
+ end
187
+ pixels
188
+ end
189
+ end
190
+
191
+
192
+ end
193
+
194
+ end
195
+ end
196
+
197
+ def h_bitmask(input,count,offset=0)
198
+ #return first n bits of ushort as integer
199
+ #TODO: make generalization of input type
200
+ input <<= offset
201
+ (input & (0xFFFF - 2**(16-count) + 1)) >> (16 - count)
202
+ end
@@ -0,0 +1,200 @@
1
+ require 'socket'
2
+
3
+ require 'vncrec/rfb/encraw.rb'
4
+ require 'vncrec/rfb/enczrle.rb'
5
+ require 'vncrec/rfb/enchex.rb'
6
+
7
+ module VNCRec
8
+ module RFB
9
+ class Proxy
10
+ attr_accessor :name, :w, :h, :io, :data
11
+
12
+ # @param io [IO, #read, #sysread, #syswrite, #read_nonblock] string stream from VNC server.
13
+ # @param rfbv [String] version of RFB protocol, 3.8 is the only supported by now
14
+ # @param enc [Integer] encoding of video data used to transfer. One of the following:
15
+ # * {ENC_RAW}
16
+ # * {ENC_HEXTILE}
17
+ # * {ENC_ZRLE}
18
+ # @param pf [Hash] pixel format:
19
+ # * {VNCRec::PIX_FMT_BGR8} - 8 bits per pixel
20
+ # * {VNCRec::PIX_FMT_BGR32} - 32 bits per pixel
21
+ # @param w width of the screen area
22
+ # @param h height of the screen area
23
+ def initialize(io, rfbv, enc, pf)
24
+ @io = io
25
+ @version = rfbv
26
+ @enc = enc
27
+ @pf = pf
28
+ end
29
+
30
+ def prepare_framebuffer(w, h, bpp)
31
+ @w = w
32
+ @h = h
33
+ @bpp = bpp
34
+ @bypp = (bpp / 8.0).to_i
35
+ @wb = @w * @bypp
36
+ @data = "\x00" * @wb * @h
37
+ end
38
+
39
+ # Perform handshake
40
+ # @return w,h,server_name or nil
41
+ def handshake
42
+ # version
43
+ version = @io.readpartial 12
44
+ @io.syswrite(@version + "\n")
45
+
46
+ # security
47
+ num_of_st = @io.readbyte
48
+ if num_of_st == 0 # failed
49
+ reason_len = @io.readpartial(4).unpack('L>')[0]
50
+ reason = @io.readpartial(reason_len)
51
+ fail reason
52
+ else
53
+ num_of_st.times do
54
+ @io.readbyte
55
+ end
56
+ end
57
+
58
+ reply = [1].pack('C') # security type:none
59
+ @io.syswrite reply
60
+
61
+ stype = (@io.readpartial 4).unpack('H' * 8)
62
+ # client init
63
+ @io.syswrite reply
64
+
65
+ # server init
66
+ w = @io.readpartial(2).unpack('S>')[0]
67
+ h = @io.readpartial(2).unpack('S>')[0]
68
+ pf = @io.readpartial 16
69
+ nlen = @io.readpartial(4).unpack('L>')[0]
70
+ @name = @io.readpartial nlen
71
+ return [w, h, @name]
72
+ rescue
73
+ return nil
74
+ end
75
+
76
+ # Set a way that server should use to represent pixel data
77
+ # @param [Hash] pixel format:
78
+ # * {VNCRec::PIX_FMT_BGR8}
79
+ # * {VNCRec::PIX_FMT_BGRA}
80
+ def set_pixel_format(format)
81
+ msg = [0, 0, 0, 0].pack('CC3')
82
+ begin
83
+ @io.syswrite msg
84
+
85
+ msg = [
86
+ format[:bpp],
87
+ format[:depth],
88
+ format[:bend],
89
+ format[:tcol],
90
+ format[:rmax],
91
+ format[:gmax],
92
+ format[:bmax],
93
+ format[:rshif],
94
+ format[:gshif],
95
+ format[:bshif],
96
+ 0, 0, 0
97
+ ].pack('CCCCS>S>S>CCCC3')
98
+ return @io.syswrite msg
99
+
100
+ rescue
101
+ return nil
102
+ end
103
+ end
104
+
105
+ # Set way of encoding video frames.
106
+ # @param encodings [Array<Integer>] encoding of video data used to transfer.
107
+ # * {ENC_RAW}
108
+ # * {ENC_HEXTILE}
109
+ # * {ENC_ZRLE}
110
+ def set_encodings(encodings)
111
+ num = encodings.size
112
+ msg = [2, 0, num].pack('CCS>')
113
+ begin
114
+ @io.syswrite msg
115
+ encodings.each do |e|
116
+ @io.syswrite([e].pack('l>'))
117
+ end
118
+ rescue
119
+ return nil
120
+ end
121
+ end
122
+
123
+ # Request framebuffer update.
124
+ # @param [Integer] inc incremental, request just difference
125
+ # between previous and current framebuffer state.
126
+ # @param x [Integer]
127
+ # @param y [Integer]
128
+ # @param w [Integer]
129
+ # @param h [Integer]
130
+ def fb_update_request(inc, x, y, w, h)
131
+ @inc = inc > 0
132
+ msg = [3, inc, x, y, w, h].pack('CCS>S>S>S>')
133
+ return @io.write msg
134
+ rescue
135
+ return nil
136
+ end
137
+
138
+ # Handle VNC server response. Call it right after +fb_update_request+.
139
+ # @return [Array] type, (either framebuffer, "bell", +handle_server_cuttext+ or +handle_colormap_update+ results)
140
+ def handle_response
141
+ t = (io.readpartial 1).ord
142
+ case t
143
+ when 0 then
144
+ handle_fb_update
145
+ return [t, @data]
146
+ when 1 then
147
+ return [t, handle_colormap_update]
148
+ when 2 then
149
+ return [t, 'bell']
150
+ when 3 then
151
+ return [t, handle_server_cuttext]
152
+ else
153
+ return [-1, nil]
154
+ end
155
+ end
156
+
157
+ # Receives data and applies diffs(if incremental) to the @data
158
+ def handle_fb_update
159
+ fail 'run #prepare_framebuffer first' unless @data
160
+ enc = nil
161
+ @encs ||= { 0 => VNCRec::RFB::EncRaw,
162
+ 5 => VNCRec::RFB::EncHextile,
163
+ 16 => VNCRec::RFB::EncZRLE
164
+ }
165
+ _, numofrect = @io.read(3).unpack('CS>')
166
+ i = 0
167
+ while i < numofrect
168
+ hdr = @io.read 12
169
+ x, y, w, h, enc = hdr.unpack('S>S>S>S>l>')
170
+ mod = @encs.fetch(enc) { fail "Unsupported encoding #{enc}" }
171
+ mod.read_rect @io, x, y, w, h, @bpp, @data, @wb, @h
172
+ i += 1
173
+ end
174
+ end
175
+
176
+ # @return [Array] palette
177
+ def handle_colormap_update
178
+ _, first_color, noc = (@io.read 5).unpack('CS>S>')
179
+ palette = []
180
+ noc.times do
181
+ palette << (@io.read 6).unpack('S>S>S>')
182
+ end
183
+ return palette
184
+ rescue
185
+ return nil
186
+ end
187
+
188
+ # @return [String] server cut text
189
+ def handle_server_cuttext
190
+ begin
191
+ _, _, _, len = (@io.read 7).unpack('C3L>')
192
+ text = @io.read len
193
+ rescue
194
+ return nil
195
+ end
196
+ text
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,3 @@
1
+ module VNCRec
2
+ VERSION = "1.0.1"
3
+ end
@@ -0,0 +1,209 @@
1
+ require "vncrec/constants.rb"
2
+ require "timeout"
3
+ require "thread"
4
+
5
+ module VNCRec
6
+ # Writers are wrappers for video files.
7
+ module Writers
8
+ # Raw video writer. Very similar to File
9
+ class RawVideo
10
+ def initialize(filename)
11
+ @filename = filename
12
+ @file = File.open(filename, 'w')
13
+ end
14
+
15
+ def write(data)
16
+ @file.write data
17
+ @file.flush
18
+ end
19
+
20
+ def close
21
+ @file.close
22
+ end
23
+
24
+ def closed?
25
+ @file.closed?
26
+ end
27
+
28
+ def size
29
+ @file.size
30
+ end
31
+ end
32
+
33
+ # FFmpeg writer. Pipes video to FFmpeg instance exactly
34
+ # *fps* times per second. Audio addition is also
35
+ # supported (`:ffmpeg_ia`- and `:ffmpeg_out_opts strings`)
36
+ class FFmpeg
37
+ # @param filename [String] a name for video file.
38
+ # Should contain extension i.e. _.mp4_ of _.flv_.
39
+ # @note Choose _-acodec_ option in +:ffmpeg_out_opts+ accordingly.
40
+ # @param opts [Hash] options:
41
+ # * fps
42
+ # * pix_fmt (see +:colormode+)
43
+ # * geometry
44
+ # * ffmpeg_iv_opts
45
+ # * ffmpeg_ia_opts
46
+ # * ffmpeg_out_opts
47
+ # See {VNCRec::Recorder#initialize} for descriptions
48
+ def initialize(filename, opts = {})
49
+ @filename = filename
50
+ @fps = opts[:fps] || 12
51
+ pf = opts.fetch(:pix_fmt) { fail 'Undefined pixel format' }
52
+ @pix_fmt = get_pix_fmt pf
53
+ @size = opts.fetch(:geometry) { fail 'Undefined frame size' }
54
+ @frame_length = frame_length
55
+ @ffmpeg_iv_opts = opts[:ffmpeg_iv_opts]
56
+ @ffmpeg_ia_opts = opts[:ffmpeg_ia_opts]
57
+ @ffmpeg_out_opts = opts[:ffmpeg_out_opts]
58
+ @cmd = "ffmpeg -y -s #{@size} -r #{@fps} -f rawvideo -pix_fmt #{@pix_fmt[:string]} \
59
+ #{@ffmpeg_iv_opts} \
60
+ -i pipe:0 \
61
+ #{@ffmpeg_ia_opts} \
62
+ #{@ffmpeg_out_opts} #{@filename} &>/dev/null"
63
+ @data_avail = false
64
+ spawn
65
+ end
66
+
67
+ def write(data)
68
+ begin
69
+ written = @pipe_to_writer.syswrite(data)
70
+ rescue Errno::EPIPE
71
+ raise 'No writer running'
72
+ end
73
+ fail 'Not enough data is piped to writer' if written % @frame_length != 0
74
+ @pipe_to_writer.flush
75
+ @data_avail = true
76
+ end
77
+
78
+ def close
79
+ Process.kill('KILL', @pid)
80
+ Timeout.timeout(5) do
81
+ Process.waitpid(@pid)
82
+ end
83
+ rescue Timeout::Error
84
+ raise 'Writer hanged'
85
+ rescue Errno::ESRCH, Errno::ECHILD
86
+ raise 'No writer running'
87
+ end
88
+
89
+ def closed?
90
+ Timeout.timeout(0.05) do
91
+ Process.waitpid(@pid)
92
+ return true
93
+ end
94
+ rescue Timeout::Error
95
+ return false
96
+ rescue Errno::ECHILD
97
+ return true
98
+ end
99
+
100
+ # @return [Integer] filesize. If no file created yet
101
+ # 0 is returned.
102
+ def size
103
+ s = File.size(@filename)
104
+ return s
105
+ rescue Errno::ENOENT
106
+ return 0
107
+ end
108
+
109
+ private
110
+
111
+ def spawn
112
+ @pipe, @pipe_to_writer = IO.pipe
113
+ @pid = fork do
114
+ Signal.trap('INT') {}
115
+ @pipe_to_writer.close
116
+ @lock = Mutex.new
117
+ @written = 0
118
+ @output_ready = false
119
+ STDIN.reopen(@pipe)
120
+ routine
121
+ end
122
+ @pipe.close
123
+ end
124
+
125
+ def routine
126
+ @output = IO.popen(@cmd)
127
+ @output_ready = true
128
+ IO.select([STDIN])
129
+ @th = Thread.new(thread_func)
130
+ loop do
131
+ data = STDIN.read(@frame_length)
132
+ fail 'wrong length' if data.length != @frame_length
133
+ if @lock.try_lock
134
+ @framebuffer = data
135
+ @lock.unlock
136
+ else
137
+ @framebuffer2 = data
138
+ @cached = true
139
+ end
140
+ end
141
+ end
142
+
143
+ def flush
144
+ return unless @output.closed? || @data_avail
145
+ @lock.synchronize do
146
+ if @cached
147
+ @framebuffer = @framebuffer2
148
+ @cached = false
149
+ end
150
+ @written += @output.syswrite @framebuffer
151
+ @output.flush
152
+ end
153
+ end
154
+
155
+ def thread_func
156
+ adjust_sleep_time { flush }
157
+ i = 0
158
+ loop do
159
+ i += 1
160
+ if (i % 100) == 0
161
+ adjust_sleep_time { flush }
162
+ else
163
+ flush
164
+ end
165
+ sleep @sl
166
+ end
167
+ end
168
+
169
+ def frame_length
170
+ bpp = @pix_fmt[:bpp] / 8
171
+ dim = @size.split('x').map(&:to_i).reduce(&:*)
172
+ bpp * dim
173
+ end
174
+
175
+ def adjust_sleep_time(&_block)
176
+ t1 = Time.now
177
+ yield
178
+ t2 = Time.now
179
+ @sl = 1.0 / @fps - (t2 - t1)
180
+ end
181
+
182
+ def get_pix_fmt(fmt)
183
+ sym = fmt.to_s.upcase.prepend('PIX_FMT_').to_sym
184
+ fail "Unknown pixel format #{fmt}" unless VNCRec.const_defined? sym
185
+ VNCRec.const_get(sym)
186
+ end
187
+ end
188
+
189
+ def self.get_writer(filename, opts = {})
190
+ begin
191
+ File.write(filename, '')
192
+ rescue Errno::EACCES
193
+ raise 'Cannot create output file'
194
+ end
195
+ @path, @filename = File.split filename
196
+ @extname = File.extname filename
197
+ return RawVideo.new(@path + '/' + @filename) if @extname == '.raw'
198
+ if @extname.empty?
199
+ if @path != '/dev'
200
+ return RawVideo.new(@path + '/' + @filename + '.raw')
201
+ else
202
+ return FFmpeg.new(@path + '/' + @filename, opts)
203
+ end
204
+ else
205
+ return FFmpeg.new(@path + '/' + @filename, opts)
206
+ end
207
+ end
208
+ end
209
+ end