vncrec 1.0.1

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