screencaster-gtk 0.0.3.alpha1

Sign up to get free protection for your applications and to get access to all the features.
data/bin/screencaster ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "screencaster-gtk"
4
+
5
+ app = ScreencasterGtk.new
6
+ app.set_up
7
+ app.main
@@ -0,0 +1,462 @@
1
+ require 'gtk2'
2
+ require 'logger'
3
+ require 'fileutils'
4
+ require 'getoptlong'
5
+ require "screencaster-gtk/capture"
6
+ require "screencaster-gtk/savefile"
7
+
8
+ ##########################
9
+
10
+
11
+ class ScreencasterGtk
12
+ attr_reader :capture_window, :window
13
+ attr_writer :status_icon
14
+
15
+ SCREENCASTER_DIR = File.join(Dir.home, ".screencaster")
16
+ # Set up logging. Keep 5 log files of a 100K each
17
+ log_dir = File.join(SCREENCASTER_DIR, 'log')
18
+ FileUtils.mkpath log_dir
19
+ LOGGER = Logger.new(File.join(log_dir, 'screencaster.log'), 5, 100000)
20
+ LOGGER.level = Logger::DEBUG
21
+
22
+ PIDFILE = File.join(SCREENCASTER_DIR, "run", "screencaster.pid")
23
+
24
+ DEFAULT_SPACE = 10
25
+
26
+ def initialize
27
+ #### Create Main Window
28
+
29
+ LOGGER.info "Started"
30
+
31
+ @window = Gtk::Window.new
32
+ @window.signal_connect("delete_event") {
33
+ LOGGER.debug "delete event occurred"
34
+ #true
35
+ self.quit
36
+ false
37
+ }
38
+
39
+ @window.signal_connect("destroy") {
40
+ LOGGER.debug "destroy event occurred"
41
+ }
42
+
43
+ # The following gets minimize and restore events, but not iconify and de-iconify
44
+ @window.signal_connect("window_state_event") { |w, e|
45
+ puts ("window_state_event #{e.to_s}")
46
+ }
47
+
48
+ @window.border_width = DEFAULT_SPACE
49
+
50
+ bottom_columns = Gtk::HBox.new(false, ScreencasterGtk::DEFAULT_SPACE) # children have different sizes, spaced by DEFAULT_SPACE
51
+
52
+ @select_button = Gtk::Button.new("Select Window to Record")
53
+ @select_button.signal_connect("clicked") {
54
+ self.select
55
+ }
56
+ bottom_columns.pack_start(@select_button, true, false)
57
+
58
+ button = Gtk::Button.new("Quit")
59
+ button.signal_connect("clicked") {
60
+ self.quit
61
+ }
62
+ bottom_columns.pack_end(button, true, false)
63
+
64
+ bottom_row = Gtk::VBox.new(false, ScreencasterGtk::DEFAULT_SPACE) # children have different sizes, spaced by DEFAULT_SPACE
65
+ bottom_row.pack_end(bottom_columns, false)
66
+
67
+ control_columns = Gtk::HBox.new(false, ScreencasterGtk::DEFAULT_SPACE) # children have different sizes, spaced by DEFAULT_SPACE
68
+
69
+ @record_button = Gtk::Button.new("Record")
70
+ @record_button.sensitive = false
71
+ @record_button.signal_connect("clicked") {
72
+ self.record
73
+ }
74
+ control_columns.pack_start(@record_button, true, false)
75
+
76
+ @pause_button = Gtk::Button.new("Pause")
77
+ @pause_button.sensitive = false
78
+ @pause_button.signal_connect("clicked") {
79
+ self.pause
80
+ }
81
+ control_columns.pack_start(@pause_button, true, false)
82
+
83
+ @stop_button = Gtk::Button.new("Stop")
84
+ @stop_button.sensitive = false
85
+ @stop_button.signal_connect("clicked") {
86
+ self.stop_recording
87
+ }
88
+ control_columns.pack_start(@stop_button, true, false)
89
+
90
+ control_row = Gtk::VBox.new(false, ScreencasterGtk::DEFAULT_SPACE) # children have different sizes, spaced by DEFAULT_SPACE
91
+ control_row.pack_start(control_columns, true, false)
92
+
93
+ columns = Gtk::HBox.new(false, ScreencasterGtk::DEFAULT_SPACE)
94
+ @progress_bar = Gtk::ProgressBar.new
95
+ columns.pack_start(@progress_bar, true, false)
96
+
97
+ @cancel_button = Gtk::Button.new("Cancel")
98
+ @cancel_button.sensitive = false
99
+ @cancel_button.signal_connect("clicked") {
100
+ self.stop_encoding
101
+ }
102
+ columns.pack_start(@cancel_button, true, false)
103
+
104
+ progress_row = Gtk::VBox.new(false, ScreencasterGtk::DEFAULT_SPACE) # children have different sizes, spaced by DEFAULT_SPACE
105
+ progress_row.pack_start(columns, true, false)
106
+
107
+ the_box = Gtk::VBox.new(false, ScreencasterGtk::DEFAULT_SPACE)
108
+ the_box.pack_end(bottom_row, false, false)
109
+ the_box.pack_end(progress_row, false, false)
110
+ the_box.pack_end(control_row, false, false)
111
+
112
+ @window.add(the_box)
113
+
114
+ ##### Done Creating Main Window
115
+
116
+ #### Accelerator Group
117
+ group = Gtk::AccelGroup.new
118
+ group.connect(Gdk::Keyval::GDK_N, Gdk::Window::CONTROL_MASK|Gdk::Window::MOD1_MASK, Gtk::ACCEL_VISIBLE) do
119
+ #puts "You pressed 'Ctrl+Alt+n'"
120
+ end
121
+
122
+ #### Pop up menu on right click
123
+
124
+ @select = Gtk::ImageMenuItem.new("Select Window")
125
+ @select.signal_connect('activate'){self.select}
126
+
127
+ @record = Gtk::ImageMenuItem.new(Gtk::Stock::MEDIA_RECORD)
128
+ @record.signal_connect('activate'){self.record}
129
+ @record.add_accelerator('activate',
130
+ group, Gdk::Keyval::GDK_R,
131
+ Gdk::Window::CONTROL_MASK | Gdk::Window::MOD1_MASK,
132
+ Gtk::ACCEL_VISIBLE)
133
+ @pause = Gtk::ImageMenuItem.new(Gtk::Stock::MEDIA_PAUSE)
134
+ @pause.signal_connect('activate'){self.pause}
135
+ @pause.add_accelerator('activate',
136
+ group, Gdk::Keyval::GDK_P,
137
+ Gdk::Window::CONTROL_MASK | Gdk::Window::MOD1_MASK,
138
+ Gtk::ACCEL_VISIBLE)
139
+ @stop = Gtk::ImageMenuItem.new(Gtk::Stock::MEDIA_STOP)
140
+ @stop.signal_connect('activate'){self.stop}
141
+ @stop.add_accelerator('activate',
142
+ group, Gdk::Keyval::GDK_S,
143
+ Gdk::Window::CONTROL_MASK | Gdk::Window::MOD1_MASK,
144
+ Gtk::ACCEL_VISIBLE)
145
+ @show_hide = Gtk::MenuItem.new("Hide")
146
+ @show_hide.signal_connect('activate'){self.show_hide_all}
147
+
148
+ quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT)
149
+ quit.signal_connect('activate'){self.quit}
150
+
151
+ @menu = Gtk::Menu.new
152
+ @menu.append(@select)
153
+
154
+ @menu.append(Gtk::SeparatorMenuItem.new)
155
+ @menu.append(@record)
156
+ @menu.append(@pause)
157
+ @menu.append(@stop)
158
+
159
+ @menu.append(Gtk::SeparatorMenuItem.new)
160
+ @menu.append(@show_hide)
161
+
162
+ @menu.append(Gtk::SeparatorMenuItem.new)
163
+ @menu.append(quit)
164
+
165
+ @menu.show_all
166
+
167
+ #### Done Menus
168
+
169
+ #### Attach accelerators to window
170
+ #root = Gdk::Window.default_root_window
171
+ @window.add_accel_group(group)
172
+ end
173
+
174
+ #### Status Icon
175
+ def status_icon
176
+ return @status_icon unless @status_icon.nil?
177
+
178
+ @status_icon = Gtk::StatusIcon.new
179
+ # Some space appears in the tray immediately, so hide it.
180
+ # The icon doesn't actually appear until you start Gtk.main
181
+ @status_icon.visible = false
182
+ @status_icon.stock = Gtk::Stock::MEDIA_RECORD
183
+ @status_icon.tooltip = 'Screencaster'
184
+
185
+ ##Show menu on right click
186
+ @status_icon.signal_connect('popup-menu'){|tray, button, time| @menu.popup(nil, nil, button, time)}
187
+
188
+ @status_icon
189
+ end
190
+
191
+ #### Done Status Icon
192
+
193
+ def quit
194
+ LOGGER.debug "Quitting"
195
+ # self.status_icon.destroy
196
+ # LOGGER.debug "After status icon destroy."
197
+ # @window.destroy
198
+ # LOGGER.debug "After window destroy."
199
+ # We don't want to destroy here because the object continues to exist
200
+ # Just hide everything
201
+ self.hide_all_including_status
202
+ # self.status_icon.hide doesn't work/exist
203
+ Gtk.main_quit
204
+ LOGGER.debug "After main_quit."
205
+ end
206
+
207
+ def select
208
+ LOGGER.debug "Selecting Window"
209
+ @capture_window = Capture.new
210
+ @capture_window.get_window_to_capture
211
+ @record_button.sensitive = true
212
+ end
213
+
214
+ def record
215
+ LOGGER.debug "Recording"
216
+ recording
217
+ @capture_window.record
218
+ end
219
+
220
+ def pause
221
+ LOGGER.debug "Pausing"
222
+ paused
223
+ @capture_window.pause_recording
224
+ end
225
+
226
+ def stop_recording
227
+ LOGGER.debug "Stopped"
228
+ not_recording
229
+ @capture_window.stop_recording
230
+
231
+ SaveFile.get_file_to_save { |filename|
232
+ encoding
233
+ @capture_window.encode(filename) { |percent, time_remaining|
234
+ @progress_bar.fraction = percent
235
+ @progress_bar.text = time_remaining
236
+ LOGGER.debug "Did progress #{percent.to_s} time remaining: #{time_remaining}"
237
+ percent < 1 || stop_encoding
238
+ }
239
+ LOGGER.debug "Back from encode"
240
+ }
241
+ end
242
+
243
+ def stop_encoding
244
+ LOGGER.debug "Cancelled encoding"
245
+ not_recording
246
+ @capture_window.stop_encoding
247
+ end
248
+
249
+ def stop
250
+ case @capture_window.state
251
+ when :recording || :paused
252
+ stop_recording
253
+ when :encoding
254
+ stop_encoding
255
+ when :stopped
256
+ # Do nothing
257
+ else
258
+ LOGGER.error "#{__FILE__} #{__LINE__}: Can't happen."
259
+ end
260
+ end
261
+
262
+ def toggle_recording
263
+ LOGGER.debug "Toggle recording"
264
+ return if @capture_window.nil?
265
+ case @capture_window.state
266
+ when :recording
267
+ pause
268
+ when :paused
269
+ record
270
+ when :encoding
271
+ stop_encoding
272
+ when :stopped
273
+ # Do nothing
274
+ else
275
+ LOGGER.error "#{__FILE__} #{__LINE__}: Can't happen."
276
+ end
277
+ end
278
+
279
+ def recording
280
+ self.status_icon.stock = Gtk::Stock::MEDIA_STOP
281
+ @select.sensitive = @select_button.sensitive = false
282
+ @pause.sensitive = @pause_button.sensitive = true
283
+ @stop.sensitive = @stop_button.sensitive = true
284
+ @record.sensitive = @record_button.sensitive = false
285
+ @cancel_button.sensitive = false
286
+ end
287
+
288
+ def not_recording
289
+ self.status_icon.stock = Gtk::Stock::MEDIA_RECORD
290
+ @select.sensitive = @select_button.sensitive = true
291
+ @pause.sensitive = @pause_button.sensitive = false
292
+ @stop.sensitive = @stop_button.sensitive = false
293
+ @record.sensitive = @record_button.sensitive = ! @capture_window.nil?
294
+ @cancel_button.sensitive = false
295
+ end
296
+
297
+ def paused
298
+ self.status_icon.stock = Gtk::Stock::MEDIA_RECORD
299
+ @select.sensitive = @select_button.sensitive = false
300
+ @pause.sensitive = @pause_button.sensitive = false
301
+ @stop.sensitive = @stop_button.sensitive = true
302
+ @record.sensitive = @record_button.sensitive = ! @capture_window.nil?
303
+ @cancel_button.sensitive = true
304
+ end
305
+
306
+ def encoding
307
+ self.status_icon.stock = Gtk::Stock::MEDIA_STOP
308
+ @pause.sensitive = @pause_button.sensitive = false
309
+ @stop.sensitive = @stop_button.sensitive = false
310
+ @record.sensitive = @record_button.sensitive = false
311
+ @cancel_button.sensitive = true
312
+ end
313
+
314
+ def not_encoding
315
+ not_recording
316
+ end
317
+
318
+ def show_all
319
+ @show_hide.label = "Hide"
320
+ @show_hide_state = :show
321
+ @window.show_all
322
+ end
323
+
324
+ def hide_all
325
+ @show_hide.label = "Show"
326
+ @show_hide_state = :hide
327
+ @window.hide_all
328
+ end
329
+
330
+ def show_hide_all
331
+ case @show_hide_state
332
+ when :show
333
+ self.hide_all
334
+ when :hide
335
+ self.show_all
336
+ else
337
+ logger.error("#{__FILE__} (#{__LINE__} @show_hide_state: #{@show_hide_state.to_s}")
338
+ end
339
+ end
340
+
341
+ def show_all_including_status
342
+ self.show_all
343
+ self.status_icon.visible = true
344
+ end
345
+
346
+ def hide_all_including_status
347
+ self.hide_all
348
+ self.status_icon.visible = false
349
+ end
350
+
351
+ def main
352
+ LOGGER.info "Starting"
353
+ self.not_recording
354
+ self.show_all_including_status
355
+ Gtk.main
356
+ LOGGER.info "Finished"
357
+ end
358
+
359
+ def set_up
360
+ # Don't run the program if there's another instance running for the user.
361
+ # If there's another instance running for the user and the --pause or --start
362
+ # flags are present, send the USR1 signal to the running instance
363
+ # and exit.
364
+ # If there's no other instance running for the user, and the --pause or --start
365
+ # flags are not present, start normally.
366
+
367
+ output_file = "/home/reid/test-key.log"
368
+
369
+ ScreencasterGtk::LOGGER.debug("pid_file is #{PIDFILE}")
370
+
371
+ if File.exists? PIDFILE
372
+ begin
373
+ f = File.new(PIDFILE)
374
+ ScreencasterGtk::LOGGER.debug("Opened PIDFILE")
375
+ existing_pid = f.gets
376
+ existing_pid = existing_pid.to_i
377
+ f.close
378
+ ScreencasterGtk::LOGGER.debug("existing_pid = #{existing_pid.to_s}")
379
+ rescue StandardError
380
+ LOGGER.error("File to read #{PIDFILE}")
381
+ exit 1
382
+ end
383
+ else
384
+ existing_pid = nil
385
+ end
386
+
387
+ opts = GetoptLong.new(
388
+ [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
389
+ [ '--pause', GetoptLong::NO_ARGUMENT ],
390
+ [ '--start', GetoptLong::NO_ARGUMENT ]
391
+ )
392
+
393
+ opts.each do |opt, arg|
394
+ case opt
395
+ when '--help'
396
+ puts <<-EOF
397
+ screencaster [OPTION] ...
398
+
399
+ -h, --help:
400
+ show help
401
+
402
+ -s, -p, --start, --pause:
403
+ Pause a running capture, or restart a paused capture
404
+ EOF
405
+ exit 0
406
+ when '--pause' || '--start'
407
+ if existing_pid then
408
+ ScreencasterGtk::LOGGER.debug("Got a pause for PID #{existing_pid}")
409
+ begin
410
+ Process.kill "USR1", existing_pid
411
+ exit 0
412
+ rescue SystemCallError
413
+ LOGGER.info("Got a pause but PID #{existing_pid} didn't exist")
414
+ exit 1
415
+ end
416
+ else
417
+ ScreencasterGtk::LOGGER.info("Got a pause but no PID")
418
+ exit 1
419
+ end
420
+ end
421
+ end
422
+
423
+ # TODO: Check for running process and if not, ignore PIDFILE.
424
+ (ScreencasterGtk::LOGGER.debug("Can't run two instances at once."); exit 1) if ! existing_pid.nil?
425
+
426
+ @chain = Signal.trap("EXIT") {
427
+ LOGGER.debug "In ScreencasterGtk exit handler @chain: #{@chain}"
428
+ ScreencasterGtk::LOGGER.debug("Exiting")
429
+ ScreencasterGtk::LOGGER.debug("unlinking") if File.file?(PIDFILE)
430
+ File.unlink(PIDFILE) if File.file?(PIDFILE)
431
+ `gconftool-2 --unset /apps/metacity/keybinding_commands/screencaster_pause`
432
+ `gconftool-2 --unset /apps/metacity/global_keybindings/run_screencaster_pause`
433
+ LOGGER.debug "About to call next trap with @chain: #{@chain}"
434
+ @chain.call unless @chain.nil?
435
+ }
436
+ LOGGER.debug "ScreencasterGtk.whatever @chain: #{@chain}"
437
+
438
+ `gconftool-2 --set /apps/metacity/keybinding_commands/screencaster_pause --type string "screencaster --pause"`
439
+ `gconftool-2 --set /apps/metacity/global_keybindings/run_screencaster_pause --type string "<Control><Alt>S"`
440
+
441
+ begin
442
+ FileUtils.mkpath(File.dirname(PIDFILE))
443
+ f = File.new(PIDFILE, "w")
444
+ f.puts(Process.pid.to_s)
445
+ f.close
446
+ LOGGER.debug("Wrote PID #{Process.pid}")
447
+ rescue StandardError
448
+ LOGGER.error("Failed to write #{PIDFILE}")
449
+ exit 1
450
+ end
451
+
452
+ Signal.trap("USR1") {
453
+ ScreencasterGtk::LOGGER.debug("Pause/Resume")
454
+ self.toggle_recording
455
+ }
456
+
457
+ $logger = ScreencasterGtk::LOGGER # TODO: Fix logging
458
+ end
459
+ end
460
+
461
+ #puts "Loaded #{__FILE__}"
462
+
@@ -0,0 +1,292 @@
1
+ require 'open3'
2
+ require 'logger'
3
+ require "screencaster-gtk/progresstracker"
4
+
5
+ class Capture
6
+ include ProgressTracker
7
+
8
+ attr_writer :left, :top, :right, :bottom
9
+ attr_reader :state
10
+ attr :pid
11
+ attr :tmp_files
12
+
13
+ def initialize
14
+ @tmp_files = []
15
+ @exit_chain = Signal.trap("EXIT") {
16
+ self.cleanup
17
+ $logger.debug "In capture about to chain to trap @exit_chain: #{@exit_chain}"
18
+ @exit_chain.call unless @exit_chain.nil?
19
+ }
20
+ $logger.debug "@exit_chain: #{@exit_chain.to_s}"
21
+ @state = :stopped
22
+ end
23
+
24
+ def current_tmp_file
25
+ @tmp_files.last
26
+ end
27
+
28
+ def new_tmp_file
29
+ @tmp_files << Capture.tmp_file_name(@tmp_files.size)
30
+ self.current_tmp_file
31
+ end
32
+
33
+ def self.tmp_file_name(index = nil)
34
+ "/tmp/screencaster_#{$$}" + (index.nil? ? "": "_#{"%04d" % index}") + ".mkv"
35
+ end
36
+
37
+ def self.format_input_files_for_mkvmerge(files)
38
+ files.drop(1).inject("\"#{files.first}\"") {|a, b| "#{a} + \"#{b}\"" }
39
+ end
40
+
41
+ def width
42
+ @right - @left
43
+ end
44
+
45
+ def height=(h)
46
+ @bottom = @top + h
47
+ h
48
+ end
49
+
50
+ def height
51
+ @bottom - @top
52
+ end
53
+
54
+ def width=(w)
55
+ @right = @left + w
56
+ w
57
+ end
58
+
59
+ # I have to refactor this to make it more testable.
60
+ # As a general approach, I want to factor out the parts that have human input
61
+ # so that whatever I have to do for that is as small as possible.
62
+
63
+ def get_window_to_capture
64
+ print "Click in the window you want to capture.\n"
65
+ info = `xwininfo`
66
+
67
+ info =~ /Window id: (0x[[:xdigit:]]+)/
68
+ window_id = $1
69
+
70
+ info =~ /geometry\s+([[:digit:]])+x([[:digit:]]+)\+([[:digit:]]+)-([[:digit:]]+)/
71
+
72
+ info =~ /Absolute upper-left X:\s+([[:digit:]]+)/
73
+ @left = $1.to_i
74
+ info =~ /Absolute upper-left Y:\s+([[:digit:]]+)/
75
+ @top = $1.to_i
76
+
77
+ info =~ /Width:\s+([[:digit:]]+)/
78
+ @width = $1.to_i
79
+ info =~ /Height:\s+([[:digit:]]+)/
80
+ @height = $1.to_i
81
+
82
+ $logger.debug "Before xprop: Capturing #{@left.to_s},#{@top.to_s} to #{(@left+@width).to_s},#{(@top+@height).to_s}. Dimensions #{@width.to_s},#{@height.to_s}.\n"
83
+
84
+ # Use xprop on the window to figure out decorations? Maybe...
85
+ # $logger.debug "Window ID: #{window_id}"
86
+ # info = `xprop -id #{window_id}`
87
+ # info =~ /_NET_FRAME_EXTENTS\(CARDINAL\) = ([[:digit:]]+), ([[:digit:]]+), ([[:digit:]]+), ([[:digit:]]+)/
88
+ # border_left = $1.to_i
89
+ # border_right = $2.to_i
90
+ # border_top = $3.to_i
91
+ # border_bottom = $4.to_i
92
+ #
93
+ # $logger.debug "Borders: #{border_left.to_s},#{border_top.to_s},#{border_right.to_s},#{border_bottom.to_s}.\n"
94
+ #
95
+ # top += border_top
96
+ # left += border_left
97
+ # height -= border_top + border_bottom
98
+ # width -= border_left + border_right
99
+
100
+ @height += @height % 2
101
+ @width += @width % 2
102
+
103
+ $logger.debug "Capturing #{@left},#{@top} to #{@left+@width},#{@top+@height}. Dimensions #{@width},#{@height}.\n"
104
+ end
105
+
106
+ def record
107
+ if @state != :paused
108
+ @tmp_files = []
109
+ self.total_amount = 0.0
110
+ end
111
+ record_one_file(self.new_tmp_file)
112
+ end
113
+
114
+ def record_one_file(output_file)
115
+ @state = :recording
116
+ capture_fps=24
117
+ audio_options="-f alsa -ac 1 -ab 192k -i pulse -acodec pcm_s16le"
118
+
119
+ # And i should probably popen here, save the pid, then fork and start
120
+ # reading the input, updating the number of frames saved, or the time
121
+ # recorded.
122
+ $logger.debug "Capturing...\n"
123
+ # @pid = Process.spawn("avconv \
124
+ # #{audio_options} \
125
+ # -f x11grab \
126
+ # -show_region 1 \
127
+ # -r #{capture_fps} \
128
+ # -s #{@width}x#{@height} \
129
+ # -i :0.0+#{@left},#{@top} \
130
+ # -qscale 0 -vcodec ffv1 \
131
+ # -y \
132
+ # #{TMP_FILE}")
133
+ # Process.detach(@pid)
134
+ # avconv writes output to stderr, why?
135
+ # writes with CR and not LF, so it's hard to read
136
+ # with popen and 2>&1, an extra shell gets created that messed things up.
137
+ # popen2e helps.
138
+ vcodec = 'huffyuv' # I used to use ffv1
139
+
140
+ cmd_line = "avconv \
141
+ #{audio_options} \
142
+ -f x11grab \
143
+ -show_region 1 \
144
+ -r #{capture_fps} \
145
+ -s #{@width}x#{@height} \
146
+ -i :0.0+#{@left},#{@top} \
147
+ -qscale 0 -vcodec #{vcodec} \
148
+ -y \
149
+ #{output_file}"
150
+
151
+ $logger.debug cmd_line
152
+
153
+ i, oe, t = Open3.popen2e(cmd_line)
154
+ @pid = t.pid
155
+ Process.detach(@pid)
156
+
157
+ duration = 0.0
158
+ Thread.new do
159
+ while line = oe.gets("\r")
160
+ $logger.debug "****" + line
161
+ if (line =~ /time=([0-9]*\.[0-9]*)/)
162
+ duration = $1.to_f
163
+ end
164
+ end
165
+ self.total_amount += duration
166
+ end
167
+ end
168
+
169
+ def stop_recording
170
+ begin
171
+ Process.kill("INT", @pid)
172
+ rescue SystemCallError
173
+ $logger.error("No recording to stop.") unless @state == :paused
174
+ end
175
+ @state = :stopped
176
+ end
177
+
178
+ def pause_recording
179
+ begin
180
+ Process.kill("INT", @pid)
181
+ rescue SystemCallError
182
+ $logger.error("No recording to pause.")
183
+ end
184
+ @state = :paused
185
+ end
186
+
187
+ # Refactoring this to make it more testable and so it works:
188
+ # Encoding now has two steps: Merge the files (if more than one)
189
+ # and then encode
190
+ # Encode takes an optional block that updates a progress bar or other type
191
+ # of status
192
+ # I believe I have to split it out so that variables are in scope when I
193
+ # need them to be, but mainly I need to make this testable, and now is the time.
194
+
195
+ def encode(output_file = "output.mp4", &feedback)
196
+ state = :encoding
197
+ output_file =~ /.mp4$/ || output_file += ".mp4"
198
+
199
+ $logger.debug "Encoding #{Capture.format_input_files_for_mkvmerge(@tmp_files)}...\n"
200
+ $logger.debug("Total duration #{self.total_amount.to_s}")
201
+
202
+ t = self.merge(Capture.tmp_file_name, @tmp_files)
203
+ t.value
204
+ self.final_encode(output_file, Capture.tmp_file_name, feedback)
205
+ end
206
+
207
+ # This is ugly.
208
+ # When you open co-processes, they do get stuck together.
209
+ # It seems the if I don't read what's coming out of the co-process, it waits.
210
+ # But if I read it, then it goes right to the end until it returns.
211
+
212
+ def merge(output_file, input_files, feedback = proc {} )
213
+ $logger.debug("Merging #{input_files.size.to_s} files: #{Capture.format_input_files_for_mkvmerge(input_files)}")
214
+
215
+ if input_files.size == 1
216
+ cmd_line = "cp #{input_files[0]} #{output_file}"
217
+ #cmd_line = "sleep 5"
218
+ else
219
+ cmd_line = "mkvmerge -v -o #{output_file} #{Capture.format_input_files_for_mkvmerge(input_files)}"
220
+ end
221
+ $logger.debug "Merge command line: #{cmd_line}"
222
+ i, oe, t = Open3.popen2e(cmd_line)
223
+ @pid = t.pid
224
+ Process.detach(@pid)
225
+ $logger.debug "@pid: #{@pid.to_s}"
226
+
227
+ t = Thread.new do
228
+ # $logger.debug "Sleeping..."
229
+ # sleep 2
230
+ # $logger.debug "Awake!"
231
+ while oe.gets do
232
+ # $logger.debug "Line"
233
+ end
234
+ feedback.call 1.0, "Done"
235
+ end
236
+ return t
237
+ end
238
+
239
+ def final_encode(output_file, input_file, feedback = proc {} )
240
+ encode_fps=24
241
+ video_encoding_options="-vcodec libx264 -pre:v ultrafast"
242
+
243
+ # I think I want to popen here, save the pid, then fork and start
244
+ # updating progress based on what I read, which the main body
245
+ # returns and carries on.
246
+
247
+ # The following doesn't seem to be necessary
248
+ # -s #{@width}x#{@height} \
249
+ cmd_line = "avconv \
250
+ -i #{input_file} \
251
+ #{video_encoding_options} \
252
+ -r #{encode_fps} \
253
+ -threads 0 \
254
+ -y \
255
+ '#{output_file}'"
256
+
257
+ $logger.debug cmd_line
258
+
259
+ i, oe, t = Open3.popen2e(cmd_line)
260
+ @pid = t.pid
261
+ Process.detach(@pid)
262
+
263
+ Thread.new do
264
+ while (line = oe.gets("\r"))
265
+ $logger.debug "****" + line
266
+ if (line =~ /time=([0-9]*\.[0-9]*)/)
267
+ $logger.debug '******' + $1
268
+ self.current_amount = $1.to_f
269
+ end
270
+ $logger.debug "******** #{self.current_amount} #{self.fraction_complete}"
271
+ feedback.call self.fraction_complete, self.time_remaining_s
272
+ end
273
+ $logger.debug "reached end of file"
274
+ @state = :stopped
275
+ feedback.call self.fraction_complete = 1, self.time_remaining_s
276
+ end
277
+ end
278
+
279
+ def stop_encoding
280
+ begin
281
+ Process.kill("INT", @pid)
282
+ rescue SystemCallError
283
+ $logger.error("No encoding to stop.")
284
+ end
285
+ end
286
+
287
+ def cleanup
288
+ @tmp_files.each { |f| File.delete(f) if File.exists?(f) }
289
+ File.delete(Capture.tmp_file_name) if File.exists?(Capture.tmp_file_name)
290
+ end
291
+ end
292
+
@@ -0,0 +1,45 @@
1
+ module ProgressTracker
2
+ attr_reader :start_time
3
+ attr_writer :start_time, :total_amount
4
+
5
+ def percent_complete
6
+ self.fraction_complete * 100
7
+ end
8
+
9
+ def fraction_complete
10
+ [ self.current_amount.to_f / self.total_amount.to_f, 1.0 ].min
11
+ end
12
+
13
+ def fraction_complete=(fraction)
14
+ @current_amount = fraction * self.total_amount
15
+ end
16
+
17
+ def current_amount
18
+ @current_amount || 0.0
19
+ end
20
+
21
+ def current_amount=(amt)
22
+ @start_time || @start_time = Time.new
23
+
24
+ # puts "Setting current_amount #{amt}"
25
+ @current_amount = amt
26
+ end
27
+
28
+ def total_amount
29
+ @total_amount || 1.0
30
+ end
31
+
32
+ def time_remaining
33
+ (Time.new - @start_time) * (1 - self.fraction_complete) / self.fraction_complete
34
+ end
35
+
36
+ def time_remaining_s(format = "%dh %02dm %02ds remaining")
37
+ t = self.time_remaining
38
+ h = (t / 3600).to_i
39
+ m = ((t - h * 3600) / 60).to_i
40
+ s = (t % 60).to_i
41
+ sprintf(format, h, m, s)
42
+ end
43
+
44
+ end
45
+
@@ -0,0 +1,75 @@
1
+ require 'gtk2'
2
+
3
+ class SaveFile
4
+ def self.set_up_dialog(file_name = "output.mp4")
5
+ @dialog = Gtk::FileChooserDialog.new(
6
+ "Save File As ...",
7
+ $window,
8
+ Gtk::FileChooser::ACTION_SAVE,
9
+ nil,
10
+ [ Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL ],
11
+ [ Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT ]
12
+ )
13
+ # dialog.signal_connect('response') do |w, r|
14
+ # odg = case r
15
+ # when Gtk::Dialog::RESPONSE_ACCEPT
16
+ # filename = dialog.filename
17
+ # "'ACCEPT' (#{r}) button pressed -- filename is {{ #{filename} }}"
18
+ # when Gtk::Dialog::RESPONSE_CANCEL; "'CANCEL' (#{r}) button pressed"
19
+ # else; "Undefined response ID; perhaps Close-x? (#{r})"
20
+ # end
21
+ # puts odg
22
+ # dialog.destroy
23
+ # end
24
+ @dialog.current_name = GLib.filename_to_utf8(file_name)
25
+ @dialog.current_folder = GLib.filename_to_utf8(Dir.getwd)
26
+ @dialog.do_overwrite_confirmation = true
27
+ end
28
+
29
+ def self.get_file_to_save
30
+ @dialog || SaveFile.set_up_dialog
31
+ result = @dialog.run
32
+ file_name = @dialog.filename
33
+ case result
34
+ when Gtk::Dialog::RESPONSE_ACCEPT
35
+ @dialog.hide
36
+ yield file_name
37
+ when Gtk::Dialog::RESPONSE_CANCEL
38
+ self.confirm_cancel(@dialog)
39
+ @dialog.hide
40
+ else
41
+ LOGGER.error("Can't happen #{__FILE__} line: #{__LINE__}")
42
+ @dialog.hide
43
+ end
44
+ end
45
+
46
+ def self.confirm_cancel(parent)
47
+ dialog = Gtk::Dialog.new(
48
+ "Confirm Cancel",
49
+ parent,
50
+ Gtk::Dialog::MODAL,
51
+ [ "Discard Recording", Gtk::Dialog::RESPONSE_CANCEL ],
52
+ [ "Save Recording As...", Gtk::Dialog::RESPONSE_OK ]
53
+ )
54
+ dialog.has_separator = false
55
+ label = Gtk::Label.new("Your recording has not been saved.")
56
+ image = Gtk::Image.new(Gtk::Stock::DIALOG_WARNING, Gtk::IconSize::DIALOG)
57
+
58
+ hbox = Gtk::HBox.new(false, 5)
59
+ hbox.border_width = 10
60
+ hbox.pack_start_defaults(image);
61
+ hbox.pack_start_defaults(label);
62
+
63
+ dialog.vbox.add(hbox)
64
+ dialog.show_all
65
+ result = dialog.run
66
+ dialog.destroy
67
+
68
+ case result
69
+ when Gtk::Dialog::RESPONSE_OK
70
+ self.get_file_to_save
71
+ else
72
+ end
73
+ end
74
+ end
75
+
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: screencaster-gtk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3.alpha1
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - Larry Reid
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-11-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: gdk_pixbuf2
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
22
+ - - ! '>='
23
+ - !ruby/object:Gem::Version
24
+ version: 2.0.2
25
+ type: :runtime
26
+ prerelease: false
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ - - ! '>='
34
+ - !ruby/object:Gem::Version
35
+ version: 2.0.2
36
+ - !ruby/object:Gem::Dependency
37
+ name: cairo
38
+ requirement: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '1.12'
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: 1.12.6
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.12'
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: 1.12.6
58
+ - !ruby/object:Gem::Dependency
59
+ name: glib2
60
+ requirement: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: '2.0'
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: 2.0.2
69
+ type: :runtime
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: '2.0'
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: 2.0.2
80
+ - !ruby/object:Gem::Dependency
81
+ name: gtk2
82
+ requirement: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ~>
86
+ - !ruby/object:Gem::Version
87
+ version: '2.0'
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: 2.0.2
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ~>
97
+ - !ruby/object:Gem::Version
98
+ version: '2.0'
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: 2.0.2
102
+ description: A gem for capturing screencasts
103
+ email: larry.reid@jadesystems.ca
104
+ executables:
105
+ - screencaster
106
+ extensions: []
107
+ extra_rdoc_files: []
108
+ files:
109
+ - lib/screencaster-gtk.rb
110
+ - lib/screencaster-gtk/savefile.rb
111
+ - lib/screencaster-gtk/capture.rb
112
+ - lib/screencaster-gtk/progresstracker.rb
113
+ - bin/screencaster
114
+ homepage: http://github.org/lcreid/screencaster
115
+ licenses:
116
+ - GPL2
117
+ post_install_message:
118
+ rdoc_options: []
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ! '>='
125
+ - !ruby/object:Gem::Version
126
+ version: 1.9.2
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ! '>'
131
+ - !ruby/object:Gem::Version
132
+ version: 1.3.1
133
+ requirements:
134
+ - avconv
135
+ - wmctl
136
+ - libavcodec-extra-53
137
+ - mkvtoolnix
138
+ rubyforge_project:
139
+ rubygems_version: 1.8.24
140
+ signing_key:
141
+ specification_version: 3
142
+ summary: Screencaster
143
+ test_files: []