screencaster-gtk 0.0.4.alpha1 → 0.0.5.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/screencaster +11 -0
- data/lib/screencaster-gtk.rb +213 -125
- data/lib/screencaster-gtk/capture.rb +91 -107
- data/lib/screencaster-gtk/progresstracker.rb +10 -5
- data/lib/screencaster-gtk/savefile.rb +15 -11
- metadata +3 -2
data/bin/screencaster
CHANGED
@@ -1,6 +1,17 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require "screencaster-gtk"
|
4
|
+
#require "rdoc/usage"
|
5
|
+
|
6
|
+
=begin rdoc
|
7
|
+
screencaster [OPTION] ...
|
8
|
+
|
9
|
+
-h, --help:
|
10
|
+
show help
|
11
|
+
|
12
|
+
-s, -p, --start, --pause:
|
13
|
+
Pause a running capture, or restart a paused capture
|
14
|
+
=end
|
4
15
|
|
5
16
|
app = ScreencasterGtk.new
|
6
17
|
app.set_up
|
data/lib/screencaster-gtk.rb
CHANGED
@@ -7,19 +7,27 @@ require "screencaster-gtk/savefile"
|
|
7
7
|
|
8
8
|
##########################
|
9
9
|
|
10
|
-
|
10
|
+
=begin rdoc
|
11
|
+
A program to capture screencasts -- video from monitor and sound from microphone
|
12
|
+
=end
|
11
13
|
class ScreencasterGtk
|
12
|
-
attr_reader :capture_window
|
13
|
-
|
14
|
+
attr_reader :capture_window
|
15
|
+
|
16
|
+
protected
|
17
|
+
attr_reader :window
|
18
|
+
# atr_writer :status_icon
|
14
19
|
|
15
20
|
SCREENCASTER_DIR = File.join(Dir.home, ".screencaster")
|
16
|
-
# Set up logging. Keep 5 log files of
|
21
|
+
# Set up logging. Keep 5 log files of 100K each
|
17
22
|
log_dir = File.join(SCREENCASTER_DIR, 'log')
|
18
23
|
FileUtils.mkpath log_dir
|
19
|
-
|
20
|
-
|
24
|
+
LOGFILE = File.join(log_dir, 'screencaster.log')
|
25
|
+
@@logger = Logger.new(LOGFILE, 5, 100000)
|
26
|
+
@@logger.level = Logger::DEBUG
|
21
27
|
|
22
28
|
PIDFILE = File.join(SCREENCASTER_DIR, "run", "screencaster.pid")
|
29
|
+
|
30
|
+
SOUND_SETTINGS = "/usr/bin/gnome-control-center"
|
23
31
|
|
24
32
|
DEFAULT_SPACE = 10
|
25
33
|
RECORD_IMAGE = Gtk::Image.new(Gtk::Stock::MEDIA_RECORD, Gtk::IconSize::SMALL_TOOLBAR)
|
@@ -28,81 +36,54 @@ class ScreencasterGtk
|
|
28
36
|
CANCEL_IMAGE = Gtk::Image.new(Gtk::Stock::CANCEL, Gtk::IconSize::SMALL_TOOLBAR)
|
29
37
|
QUIT_IMAGE = Gtk::Image.new(Gtk::Stock::QUIT, Gtk::IconSize::SMALL_TOOLBAR)
|
30
38
|
|
39
|
+
public
|
40
|
+
def self.logger
|
41
|
+
@@logger
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.logger=(log_file)
|
45
|
+
@@logger = Logger.new(log_file, 5, 100000)
|
46
|
+
end
|
47
|
+
|
31
48
|
|
32
49
|
def initialize
|
33
50
|
#### Create Main Window
|
34
51
|
|
35
|
-
|
52
|
+
@@logger.info "Started"
|
36
53
|
|
37
54
|
@window = Gtk::Window.new("Screencaster")
|
38
55
|
@window.signal_connect("delete_event") {
|
39
|
-
|
56
|
+
@@logger.debug "delete event occurred"
|
40
57
|
#true
|
41
58
|
self.quit
|
42
59
|
false
|
43
60
|
}
|
44
61
|
|
45
62
|
@window.signal_connect("destroy") {
|
46
|
-
|
63
|
+
@@logger.debug "destroy event occurred"
|
47
64
|
}
|
48
65
|
|
49
66
|
# The following gets minimize and restore events, but not iconify and de-iconify
|
50
67
|
@window.signal_connect("window_state_event") { |w, e|
|
51
|
-
|
68
|
+
@@logger.debug "window_state_event #{e.to_s}"
|
52
69
|
}
|
53
70
|
|
54
71
|
@window.border_width = DEFAULT_SPACE
|
55
72
|
|
56
73
|
control_bar = Gtk::HBox.new(false, ScreencasterGtk::DEFAULT_SPACE)
|
57
74
|
|
58
|
-
@select_button =
|
59
|
-
@
|
60
|
-
|
61
|
-
}
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
control_columns = Gtk::HBox.new(false, ScreencasterGtk::DEFAULT_SPACE) # children have different sizes, spaced by DEFAULT_SPACE
|
72
|
-
|
73
|
-
@record_pause_button = Gtk::Button.new
|
74
|
-
@record_pause_button.image = RECORD_IMAGE
|
75
|
-
@record_pause_button.sensitive = false
|
76
|
-
@record_pause_button.signal_connect("clicked") {
|
77
|
-
self.record_pause
|
78
|
-
}
|
79
|
-
control_bar.pack_start(@record_pause_button, true, false)
|
80
|
-
|
81
|
-
# @pause_button = Gtk::Button.new(Gtk::Stock::MEDIA_PAUSE)
|
82
|
-
# @pause_button.sensitive = false
|
83
|
-
# @pause_button.signal_connect("clicked") {
|
84
|
-
# self.pause
|
85
|
-
# }
|
86
|
-
# control_columns.pack_start(@pause_button, true, false)
|
87
|
-
#
|
88
|
-
@stop_button = Gtk::Button.new
|
89
|
-
@stop_button.image = STOP_IMAGE
|
90
|
-
@stop_button.sensitive = false
|
91
|
-
@stop_button.signal_connect("clicked") {
|
92
|
-
self.stop_recording
|
93
|
-
}
|
94
|
-
control_bar.pack_start(@stop_button, true, false)
|
95
|
-
|
96
|
-
@cancel_button = Gtk::Button.new
|
97
|
-
@cancel_button.image = CANCEL_IMAGE
|
98
|
-
@cancel_button.sensitive = false
|
99
|
-
@cancel_button.signal_connect("clicked") {
|
100
|
-
self.stop_encoding
|
101
|
-
}
|
102
|
-
control_bar.pack_start(@cancel_button, true, false)
|
103
|
-
|
104
|
-
control_row = Gtk::VBox.new(false, ScreencasterGtk::DEFAULT_SPACE) # children have different sizes, spaced by DEFAULT_SPACE
|
105
|
-
control_row.pack_start(control_bar, true, false)
|
75
|
+
@select_button = add_button("Select Window", control_bar) { self.select }
|
76
|
+
@record_pause_button = add_button(RECORD_IMAGE, control_bar) { self.record_pause }
|
77
|
+
@stop_button = add_button(STOP_IMAGE, control_bar) { self.stop_recording }
|
78
|
+
@cancel_button = add_button(CANCEL_IMAGE, control_bar) { self.stop_encoding }
|
79
|
+
if File.executable? SOUND_SETTINGS
|
80
|
+
# There appears to be no stock icon for someting like a volume control.
|
81
|
+
#@sound_settings_button.image = AUDIO_VOLUME_MEDIUM
|
82
|
+
@sound_settings_button = add_button("Sound", control_bar, true) {
|
83
|
+
Thread.new { `#{SOUND_SETTINGS} sound` }
|
84
|
+
}
|
85
|
+
end
|
86
|
+
add_button(QUIT_IMAGE, control_bar, true) { self.quit }
|
106
87
|
|
107
88
|
columns = Gtk::HBox.new(false, ScreencasterGtk::DEFAULT_SPACE)
|
108
89
|
@progress_bar = Gtk::ProgressBar.new
|
@@ -114,20 +95,15 @@ class ScreencasterGtk
|
|
114
95
|
|
115
96
|
the_box = Gtk::VBox.new(false, ScreencasterGtk::DEFAULT_SPACE)
|
116
97
|
the_box.pack_end(progress_row, false, false)
|
117
|
-
the_box.pack_end(
|
98
|
+
the_box.pack_end(control_bar, false, false)
|
118
99
|
|
119
100
|
@window.add(the_box)
|
120
101
|
|
121
102
|
##### Done Creating Main Window
|
122
103
|
|
123
|
-
#### Accelerator Group
|
124
|
-
group = Gtk::AccelGroup.new
|
125
|
-
group.connect(Gdk::Keyval::GDK_N, Gdk::Window::CONTROL_MASK|Gdk::Window::MOD1_MASK, Gtk::ACCEL_VISIBLE) do
|
126
|
-
#puts "You pressed 'Ctrl+Alt+n'"
|
127
|
-
end
|
128
|
-
|
129
104
|
#### Pop up menu on right click
|
130
|
-
|
105
|
+
group = Gtk::AccelGroup.new
|
106
|
+
|
131
107
|
@select = Gtk::ImageMenuItem.new("Select Window")
|
132
108
|
@select.signal_connect('activate'){self.select}
|
133
109
|
|
@@ -198,74 +174,130 @@ class ScreencasterGtk
|
|
198
174
|
#### Done Status Icon
|
199
175
|
|
200
176
|
def quit
|
201
|
-
|
177
|
+
@@logger.debug "Quitting"
|
202
178
|
# self.status_icon.destroy
|
203
|
-
#
|
179
|
+
# @@logger.debug "After status icon destroy."
|
204
180
|
# @window.destroy
|
205
|
-
#
|
181
|
+
# @@logger.debug "After window destroy."
|
206
182
|
# We don't want to destroy here because the object continues to exist
|
207
183
|
# Just hide everything
|
208
184
|
self.hide_all_including_status
|
209
185
|
# self.status_icon.hide doesn't work/exist
|
210
186
|
Gtk.main_quit
|
211
|
-
|
187
|
+
@@logger.debug "After main_quit."
|
212
188
|
end
|
213
189
|
|
190
|
+
public
|
214
191
|
def select
|
215
|
-
|
192
|
+
@@logger.debug "Selecting Window"
|
216
193
|
@capture_window = Capture.new
|
217
194
|
@capture_window.get_window_to_capture
|
218
195
|
@record_pause_button.sensitive = true
|
219
196
|
end
|
220
197
|
|
198
|
+
protected
|
221
199
|
def record_pause
|
222
|
-
|
200
|
+
@@logger.debug "Record/Pause state: #{@capture_window.state}"
|
223
201
|
case @capture_window.state
|
224
202
|
when :recording
|
225
|
-
|
203
|
+
@@logger.debug "Record/Pause -- pause"
|
226
204
|
pause
|
227
205
|
when :paused, :stopped
|
228
|
-
|
206
|
+
@@logger.debug "Record/Pause -- record"
|
229
207
|
record
|
230
208
|
else
|
231
|
-
|
209
|
+
@@logger.error "#{__FILE__} #{__LINE__}: Can't happen (state #{@capture_window.state})."
|
232
210
|
end
|
233
211
|
end
|
234
212
|
|
235
213
|
def record
|
236
|
-
|
214
|
+
@@logger.debug "Recording"
|
237
215
|
recording
|
238
|
-
|
239
|
-
@progress_bar.text = time_elapsed
|
240
|
-
LOGGER.debug "Did elapsed time: #{time_elapsed}"
|
241
|
-
}
|
216
|
+
spawn_record
|
242
217
|
end
|
243
218
|
|
244
219
|
def pause
|
245
|
-
|
220
|
+
@@logger.debug "Pausing"
|
246
221
|
paused
|
247
222
|
@capture_window.pause_recording
|
248
223
|
end
|
249
224
|
|
225
|
+
public
|
250
226
|
def stop_recording
|
251
|
-
|
227
|
+
@@logger.debug "Stopped"
|
252
228
|
not_recording
|
253
229
|
@capture_window.stop_recording
|
254
230
|
|
255
231
|
SaveFile.get_file_to_save { |filename|
|
256
232
|
encoding
|
257
|
-
|
258
|
-
|
259
|
-
@progress_bar.text = time_remaining
|
260
|
-
LOGGER.debug "Did progress #{percent.to_s} time remaining: #{time_remaining}"
|
261
|
-
percent < 1 || stop_encoding
|
262
|
-
}
|
263
|
-
LOGGER.debug "Back from encode"
|
233
|
+
spawn_encode(filename)
|
234
|
+
@@logger.debug "Encode spawned"
|
264
235
|
}
|
265
236
|
end
|
266
237
|
|
238
|
+
=begin rdoc
|
239
|
+
Encode in the background. If the background process fails, be able to pop
|
240
|
+
up a window. Give feedback by calling the optional block with | fraction, message |.
|
241
|
+
=end
|
242
|
+
def spawn_record
|
243
|
+
@background = Thread.new do
|
244
|
+
@capture_window.record do |percent, time_elapsed|
|
245
|
+
@progress_bar.text = time_elapsed
|
246
|
+
@progress_bar.pulse
|
247
|
+
@@logger.debug "Did elapsed time: #{time_elapsed}"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
@background
|
251
|
+
end
|
252
|
+
|
253
|
+
=begin rdoc
|
254
|
+
Encode in the background. If the background process fails, be able to pop
|
255
|
+
up a window. Give feedback by calling the optional block with | fraction, message |.
|
256
|
+
=end
|
257
|
+
def spawn_encode(filename)
|
258
|
+
@background = Thread.new do
|
259
|
+
@capture_window.encode(filename) do |fraction, time_remaining|
|
260
|
+
@progress_bar.pulse if fraction == 0
|
261
|
+
@progress_bar.fraction = fraction
|
262
|
+
@progress_bar.text = time_remaining
|
263
|
+
@@logger.debug "Did progress #{fraction.to_s} time remaining: #{time_remaining}"
|
264
|
+
not_encoding if fraction >= 1
|
265
|
+
end
|
266
|
+
end
|
267
|
+
@background
|
268
|
+
end
|
269
|
+
|
270
|
+
=begin rdoc
|
271
|
+
Check how the background process is doing.
|
272
|
+
If there is none, or it's running fine, return true.
|
273
|
+
If there was a background process but it failed, return false and
|
274
|
+
ensure that the user doesn't get another
|
275
|
+
message about the same condition.
|
276
|
+
=end
|
277
|
+
def check_background
|
278
|
+
# TODO: Partially implemented so far
|
279
|
+
return true if @background.nil?
|
280
|
+
return true if @background.status
|
281
|
+
if ! background_exitstatus
|
282
|
+
@background = nil
|
283
|
+
return false
|
284
|
+
end
|
285
|
+
true
|
286
|
+
end
|
287
|
+
|
288
|
+
def background_exitstatus
|
289
|
+
return true if @background.nil?
|
290
|
+
@@logger.debug "background_exitstatus: #{@background.value.exitstatus}"
|
291
|
+
[0, 255].any? { | x | x == @background.value.exitstatus }
|
292
|
+
end
|
293
|
+
|
294
|
+
def idle
|
295
|
+
error_dialog_tell_about_log unless check_background
|
296
|
+
end
|
297
|
+
|
298
|
+
protected
|
267
299
|
def stop_encoding
|
268
|
-
|
300
|
+
@@logger.debug "Cancelled encoding"
|
269
301
|
not_recording
|
270
302
|
@capture_window.stop_encoding
|
271
303
|
end
|
@@ -279,12 +311,12 @@ class ScreencasterGtk
|
|
279
311
|
when :stopped
|
280
312
|
# Do nothing
|
281
313
|
else
|
282
|
-
|
314
|
+
@@logger.error "#{__FILE__} #{__LINE__}: Can't happen (state #{@capture_window.state})."
|
283
315
|
end
|
284
316
|
end
|
285
317
|
|
286
318
|
def toggle_recording
|
287
|
-
|
319
|
+
@@logger.debug "Toggle recording"
|
288
320
|
return if @capture_window.nil?
|
289
321
|
case @capture_window.state
|
290
322
|
when :recording
|
@@ -296,10 +328,12 @@ class ScreencasterGtk
|
|
296
328
|
when :stopped
|
297
329
|
# Do nothing
|
298
330
|
else
|
299
|
-
|
331
|
+
@@logger.error "#{__FILE__} #{__LINE__}: Can't happen (state #{@capture_window.state})."
|
300
332
|
end
|
301
333
|
end
|
302
334
|
|
335
|
+
##### Methods to set sensitivity of controls
|
336
|
+
|
303
337
|
def recording
|
304
338
|
self.status_icon.stock = Gtk::Stock::MEDIA_PAUSE
|
305
339
|
@record_pause_button.image = PAUSE_IMAGE
|
@@ -378,36 +412,45 @@ class ScreencasterGtk
|
|
378
412
|
self.status_icon.visible = false
|
379
413
|
end
|
380
414
|
|
415
|
+
public
|
416
|
+
|
417
|
+
# Shows the screencaster window and starts processing events from the user.
|
418
|
+
#
|
419
|
+
# The hot key toggles between capture and pause.
|
420
|
+
# The default hot key is Ctrl+Alt+S.
|
381
421
|
def main
|
382
|
-
|
422
|
+
@@logger.info "Starting event loop"
|
383
423
|
self.not_recording
|
384
424
|
self.show_all_including_status
|
425
|
+
GLib::Idle.add { idle }
|
385
426
|
Gtk.main
|
386
|
-
|
427
|
+
@@logger.info "Finished"
|
387
428
|
end
|
388
429
|
|
430
|
+
# Process command line arguments, set up the log file, and set up hot keys.
|
431
|
+
#
|
432
|
+
# * If there's another instance running for the user and the --pause or --start
|
433
|
+
# flags are present, send the USR1 signal to the running instance and exit.
|
434
|
+
# * Don't run the program if there's another instance running for the user.
|
435
|
+
# * If there's no other instance running for the user, and the --pause or --start
|
436
|
+
# flags are not present, start normally.
|
437
|
+
# * The default hot key is Ctrl+Alt+S.
|
389
438
|
def set_up
|
390
|
-
# Don't run the program if there's another instance running for the user.
|
391
|
-
# If there's another instance running for the user and the --pause or --start
|
392
|
-
# flags are present, send the USR1 signal to the running instance
|
393
|
-
# and exit.
|
394
|
-
# If there's no other instance running for the user, and the --pause or --start
|
395
|
-
# flags are not present, start normally.
|
396
439
|
|
397
440
|
output_file = "/home/reid/test-key.log"
|
398
441
|
|
399
|
-
|
442
|
+
@@logger.debug("pid_file is #{PIDFILE}")
|
400
443
|
|
401
444
|
if File.exists? PIDFILE
|
402
445
|
begin
|
403
446
|
f = File.new(PIDFILE)
|
404
|
-
|
447
|
+
@@logger.debug("Opened PIDFILE")
|
405
448
|
existing_pid = f.gets
|
406
449
|
existing_pid = existing_pid.to_i
|
407
450
|
f.close
|
408
|
-
|
451
|
+
@@logger.debug("existing_pid = #{existing_pid.to_s}")
|
409
452
|
rescue StandardError
|
410
|
-
|
453
|
+
@@logger.error("File to read #{PIDFILE}")
|
411
454
|
exit 1
|
412
455
|
end
|
413
456
|
else
|
@@ -424,46 +467,57 @@ class ScreencasterGtk
|
|
424
467
|
case opt
|
425
468
|
when '--help'
|
426
469
|
puts <<-EOF
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
470
|
+
screencaster [OPTION] ...
|
471
|
+
|
472
|
+
-h, --help:
|
473
|
+
show help
|
474
|
+
|
475
|
+
-s, -p, --start, --pause:
|
476
|
+
Pause a running capture, or restart a paused capture
|
434
477
|
EOF
|
435
478
|
exit 0
|
436
479
|
when '--pause', '--start'
|
437
480
|
if existing_pid then
|
438
|
-
|
481
|
+
@@logger.debug("Got a pause for PID #{existing_pid}")
|
439
482
|
begin
|
440
483
|
Process.kill "USR1", existing_pid
|
441
484
|
exit 0
|
442
485
|
rescue SystemCallError
|
443
|
-
|
486
|
+
@@logger.info("Got a pause but PID #{existing_pid} didn't exist")
|
444
487
|
exit 1
|
445
488
|
end
|
446
489
|
else
|
447
|
-
|
490
|
+
@@logger.info("Got a pause but no PID")
|
448
491
|
exit 1
|
449
492
|
end
|
450
493
|
end
|
451
494
|
end
|
452
495
|
|
453
496
|
# TODO: Check for running process and if not, ignore PIDFILE.
|
454
|
-
|
497
|
+
unless existing_pid.nil?
|
498
|
+
error_dialog_tell_about_log("Can't run two instances at once.")
|
499
|
+
exit 1
|
500
|
+
end
|
455
501
|
|
502
|
+
# Add application properties for PulseAudio
|
503
|
+
# See: http://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/Clients/ApplicationProperties/
|
504
|
+
# Unfortunately, this doesn't seem to help
|
505
|
+
# Perhaps because avconv is a separate program and is setting its own values
|
506
|
+
GLib::application_name = "Screencaster"
|
507
|
+
Gtk::Window.default_icon_name = "screencaster"
|
508
|
+
GLib::setenv("PULSE_PROP_media.role", "video")
|
509
|
+
|
456
510
|
@chain = Signal.trap("EXIT") {
|
457
|
-
|
458
|
-
|
459
|
-
|
511
|
+
@@logger.debug "In ScreencasterGtk exit handler @chain: #{@chain}"
|
512
|
+
@@logger.debug("Exiting")
|
513
|
+
@@logger.debug("unlinking") if File.file?(PIDFILE)
|
460
514
|
File.unlink(PIDFILE) if File.file?(PIDFILE)
|
461
515
|
`gconftool-2 --unset /apps/metacity/keybinding_commands/screencaster_pause`
|
462
516
|
`gconftool-2 --unset /apps/metacity/global_keybindings/run_screencaster_pause`
|
463
|
-
|
517
|
+
@@logger.debug "About to call next trap with @chain: #{@chain}"
|
464
518
|
@chain.call unless @chain.nil?
|
465
519
|
}
|
466
|
-
|
520
|
+
@@logger.debug "ScreencasterGtk.whatever @chain: #{@chain}"
|
467
521
|
|
468
522
|
`gconftool-2 --set /apps/metacity/keybinding_commands/screencaster_pause --type string "screencaster --pause"`
|
469
523
|
`gconftool-2 --set /apps/metacity/global_keybindings/run_screencaster_pause --type string "<Control><Alt>S"`
|
@@ -473,18 +527,52 @@ class ScreencasterGtk
|
|
473
527
|
f = File.new(PIDFILE, "w")
|
474
528
|
f.puts(Process.pid.to_s)
|
475
529
|
f.close
|
476
|
-
|
530
|
+
@@logger.debug("Wrote PID #{Process.pid}")
|
477
531
|
rescue StandardError
|
478
|
-
|
532
|
+
@@logger.error("Failed to write #{PIDFILE}")
|
479
533
|
exit 1
|
480
534
|
end
|
481
535
|
|
482
536
|
Signal.trap("USR1") {
|
483
|
-
|
537
|
+
@@logger.debug("Pause/Resume")
|
484
538
|
self.toggle_recording
|
485
539
|
}
|
486
540
|
|
487
|
-
$logger =
|
541
|
+
$logger = @@logger # TODO: Fix logging
|
542
|
+
end
|
543
|
+
|
544
|
+
# Helper functions
|
545
|
+
private
|
546
|
+
def add_button(label, box, sensitive = false, &callback)
|
547
|
+
b = Gtk::Button.new
|
548
|
+
b.image = label if label.is_a? Gtk::Image
|
549
|
+
b.label = label if label.is_a? String
|
550
|
+
b.sensitive = sensitive
|
551
|
+
b.signal_connect("clicked") { callback.call }
|
552
|
+
box.pack_start(b, true, false)
|
553
|
+
b
|
554
|
+
end
|
555
|
+
|
556
|
+
public
|
557
|
+
def error_dialog_tell_about_log(msg = "", file = nil, line = nil)
|
558
|
+
d = Gtk::MessageDialog.new(@window,
|
559
|
+
Gtk::Dialog::DESTROY_WITH_PARENT,
|
560
|
+
Gtk::MessageDialog::WARNING,
|
561
|
+
Gtk::MessageDialog::BUTTONS_CLOSE,
|
562
|
+
"Internal Error")
|
563
|
+
|
564
|
+
d.secondary_text = msg
|
565
|
+
d.secondary_text += " in #{file}" unless file.nil?
|
566
|
+
d.secondary_text += ", line: #{line}" unless line.nil?
|
567
|
+
d.secondary_text.strip!
|
568
|
+
|
569
|
+
@@logger.warn(d.secondary_text)
|
570
|
+
|
571
|
+
d.secondary_text += "\nLook in #{LOGFILE} for further information"
|
572
|
+
d.secondary_text.strip!
|
573
|
+
|
574
|
+
d.run
|
575
|
+
d.destroy
|
488
576
|
end
|
489
577
|
end
|
490
578
|
|
@@ -2,13 +2,26 @@ require 'open3'
|
|
2
2
|
require 'logger'
|
3
3
|
require "screencaster-gtk/progresstracker"
|
4
4
|
|
5
|
+
=begin rdoc
|
6
|
+
Select a window or area of the screen to capture, capture it, and encode it.
|
7
|
+
=end
|
5
8
|
class Capture
|
6
9
|
include ProgressTracker
|
7
10
|
|
8
11
|
attr_writer :left, :top, :right, :bottom
|
9
12
|
attr_reader :state
|
10
|
-
|
11
|
-
|
13
|
+
attr_accessor :pid
|
14
|
+
attr_accessor :tmp_files
|
15
|
+
|
16
|
+
attr_accessor :capture_fps
|
17
|
+
attr_accessor :encode_fps
|
18
|
+
attr_accessor :capture_vcodec
|
19
|
+
attr_accessor :qscale
|
20
|
+
attr_accessor :encode_vcodec
|
21
|
+
attr_accessor :acodec
|
22
|
+
attr_accessor :audio_sample_frequency
|
23
|
+
attr_accessor :audio_input
|
24
|
+
|
12
25
|
|
13
26
|
def initialize
|
14
27
|
@tmp_files = []
|
@@ -17,8 +30,30 @@ class Capture
|
|
17
30
|
$logger.debug "In capture about to chain to trap @exit_chain: #{@exit_chain}"
|
18
31
|
@exit_chain.call unless @exit_chain.nil?
|
19
32
|
}
|
20
|
-
|
33
|
+
#$logger.debug "@exit_chain: #{@exit_chain.to_s}"
|
34
|
+
|
21
35
|
@state = :stopped
|
36
|
+
@coprocess = nil
|
37
|
+
|
38
|
+
@capture_fps = 30
|
39
|
+
@encode_fps = 30 # Don't use this. Just copy through
|
40
|
+
@capture_vcodec = 'huffyuv' # I used to use ffv1. This is for capture
|
41
|
+
@qscale = '4' # Recommended for fast encoding by avconv docs
|
42
|
+
@encode_vcodec = 'libx264'
|
43
|
+
@acodec = 'aac' # Youtube prefers "AAC-LC" but I don't see one called "-LC"
|
44
|
+
@audio_sample_frequency = "48k"
|
45
|
+
@audio_input = "pulse"
|
46
|
+
end
|
47
|
+
|
48
|
+
def wait
|
49
|
+
$logger.debug "wait: Current thread #{Thread.current}"
|
50
|
+
@coprocess.value
|
51
|
+
end
|
52
|
+
|
53
|
+
def status
|
54
|
+
$logger.debug "status: Current thread #{Thread.current}"
|
55
|
+
return false if @coprocess.nil?
|
56
|
+
@coprocess.status
|
22
57
|
end
|
23
58
|
|
24
59
|
def current_tmp_file
|
@@ -79,24 +114,6 @@ class Capture
|
|
79
114
|
info =~ /Height:\s+([[:digit:]]+)/
|
80
115
|
@height = $1.to_i
|
81
116
|
|
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
117
|
@height += @height % 2
|
101
118
|
@width += @width % 2
|
102
119
|
|
@@ -112,60 +129,44 @@ class Capture
|
|
112
129
|
output_file = self.new_tmp_file
|
113
130
|
|
114
131
|
@state = :recording
|
115
|
-
|
116
|
-
audio_options="-f alsa -ac 1 -ab 192k -i pulse -acodec pcm_s16le"
|
132
|
+
audio_options="-f alsa -ac 1 -ab #{@audio_sample_frequency} -i #{@audio_input} -acodec #{@acodec}"
|
117
133
|
|
118
134
|
# And i should probably popen here, save the pid, then fork and start
|
119
135
|
# reading the input, updating the number of frames saved, or the time
|
120
136
|
# recorded.
|
121
137
|
$logger.debug "Capturing...\n"
|
122
|
-
|
123
|
-
# #{audio_options} \
|
124
|
-
# -f x11grab \
|
125
|
-
# -show_region 1 \
|
126
|
-
# -r #{capture_fps} \
|
127
|
-
# -s #{@width}x#{@height} \
|
128
|
-
# -i :0.0+#{@left},#{@top} \
|
129
|
-
# -qscale 0 -vcodec ffv1 \
|
130
|
-
# -y \
|
131
|
-
# #{TMP_FILE}")
|
132
|
-
# Process.detach(@pid)
|
133
|
-
# avconv writes output to stderr, why?
|
134
|
-
# writes with CR and not LF, so it's hard to read
|
135
|
-
# with popen and 2>&1, an extra shell gets created that messed things up.
|
136
|
-
# popen2e helps.
|
137
|
-
vcodec = 'huffyuv' # I used to use ffv1
|
138
|
-
|
138
|
+
|
139
139
|
cmd_line = "avconv \
|
140
140
|
#{audio_options} \
|
141
141
|
-f x11grab \
|
142
142
|
-show_region 1 \
|
143
|
-
-r #{capture_fps} \
|
143
|
+
-r #{@capture_fps} \
|
144
144
|
-s #{@width}x#{@height} \
|
145
145
|
-i :0.0+#{@left},#{@top} \
|
146
|
-
-qscale
|
146
|
+
-qscale #{@qscale} \
|
147
|
+
-vcodec #{@capture_vcodec} \
|
147
148
|
-y \
|
148
149
|
#{output_file}"
|
149
150
|
|
150
151
|
$logger.debug cmd_line
|
151
152
|
|
152
|
-
i, oe, t = Open3.popen2e(cmd_line)
|
153
|
-
@pid = t.pid
|
154
|
-
Process.detach(@pid)
|
155
|
-
|
156
153
|
duration = 0.0
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
154
|
+
i, oe, @coprocess = Open3.popen2e(cmd_line)
|
155
|
+
$logger.debug "record: co-process started #{@coprocess}"
|
156
|
+
@pid = @coprocess.pid
|
157
|
+
|
158
|
+
while line = oe.gets("\r")
|
159
|
+
$logger.debug "****" + line
|
160
|
+
if (line =~ /time=([0-9]*\.[0-9]*)/)
|
161
|
+
duration = $1.to_f
|
162
|
+
$logger.debug "Recording about to yield #{self.total_amount + duration}"
|
163
|
+
yield 0.0, ProgressTracker::format_seconds(self.total_amount + duration) if block_given?
|
165
164
|
end
|
166
|
-
self.total_amount += duration
|
167
|
-
yield 0.0, ProgressTracker::format_seconds(self.total_amount)
|
168
165
|
end
|
166
|
+
self.total_amount += duration
|
167
|
+
yield 1.0, ProgressTracker::format_seconds(self.total_amount) if block_given?
|
168
|
+
$logger.debug "Leaving record"
|
169
|
+
@coprocess.value
|
169
170
|
end
|
170
171
|
|
171
172
|
def stop_recording
|
@@ -201,9 +202,8 @@ class Capture
|
|
201
202
|
$logger.debug "Encoding #{Capture.format_input_files_for_mkvmerge(@tmp_files)}...\n"
|
202
203
|
$logger.debug("Total duration #{self.total_amount.to_s}")
|
203
204
|
|
204
|
-
|
205
|
-
|
206
|
-
self.final_encode(output_file, Capture.tmp_file_name, feedback)
|
205
|
+
merge(Capture.tmp_file_name, @tmp_files, feedback)
|
206
|
+
final_encode(output_file, Capture.tmp_file_name, feedback)
|
207
207
|
end
|
208
208
|
|
209
209
|
# This is ugly.
|
@@ -222,73 +222,57 @@ class Capture
|
|
222
222
|
else
|
223
223
|
cmd_line = "mkvmerge -v -o #{output_file} #{Capture.format_input_files_for_mkvmerge(input_files)}"
|
224
224
|
end
|
225
|
-
$logger.debug "
|
226
|
-
i, oe,
|
227
|
-
$logger.debug "Thread from popen2e: #{
|
228
|
-
@pid =
|
229
|
-
Process.detach(@pid)
|
225
|
+
$logger.debug "merge: command line: #{cmd_line}"
|
226
|
+
i, oe, @coprocess = Open3.popen2e(cmd_line)
|
227
|
+
$logger.debug "merge: Thread from popen2e: #{@coprocess}"
|
228
|
+
@pid = @coprocess.pid
|
230
229
|
$logger.debug "@pid: #{@pid.to_s}"
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
# sleep 2
|
236
|
-
# $logger.debug "Awake!"
|
237
|
-
while l = oe.gets do
|
238
|
-
# TODO: Lots for duplicate code in this line to clean up.
|
239
|
-
if block_given?
|
240
|
-
yield 0.5, ""
|
241
|
-
else
|
242
|
-
feedback.call 0.5, ""
|
243
|
-
end
|
244
|
-
end
|
245
|
-
if block_given?
|
246
|
-
yield 1.0, "Done"
|
230
|
+
while l = oe.gets do
|
231
|
+
# TODO: Lots of duplicate code in this line to clean up.
|
232
|
+
if block_given?
|
233
|
+
yield 0.5, "Merging..."
|
247
234
|
else
|
248
|
-
feedback.call
|
235
|
+
feedback.call 0.5, "Merging..."
|
249
236
|
end
|
250
237
|
end
|
251
|
-
|
238
|
+
if block_given?
|
239
|
+
yield 1.0, "Done"
|
240
|
+
else
|
241
|
+
feedback.call 1.0, "Done"
|
242
|
+
end
|
243
|
+
@coprocess.value
|
252
244
|
end
|
253
245
|
|
254
246
|
def final_encode(output_file, input_file, feedback = proc {} )
|
255
|
-
encode_fps=24
|
256
|
-
video_encoding_options="-vcodec libx264 -pre:v ultrafast"
|
257
|
-
|
258
247
|
# I think I want to popen here, save the pid, then fork and start
|
259
|
-
# updating progress based on what I read,
|
248
|
+
# updating progress based on what I read, while the main body
|
260
249
|
# returns and carries on.
|
261
250
|
|
262
|
-
# The following doesn't seem to be necessary
|
263
|
-
# -s #{@width}x#{@height} \
|
264
251
|
cmd_line = "avconv \
|
265
252
|
-i #{input_file} \
|
266
|
-
#{
|
267
|
-
-r #{encode_fps} \
|
268
|
-
-threads 0 \
|
253
|
+
-vcodec #{@encode_vcodec} \
|
269
254
|
-y \
|
270
255
|
'#{output_file}'"
|
271
256
|
|
272
257
|
$logger.debug cmd_line
|
273
258
|
|
274
|
-
i, oe,
|
275
|
-
@pid =
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
$logger.debug
|
281
|
-
|
282
|
-
$logger.debug '******' + $1
|
283
|
-
self.current_amount = $1.to_f
|
284
|
-
end
|
285
|
-
$logger.debug "******** #{self.current_amount} #{self.fraction_complete}"
|
286
|
-
feedback.call self.fraction_complete, self.time_remaining_s
|
259
|
+
i, oe, @coprocess = Open3.popen2e(cmd_line)
|
260
|
+
@pid = @coprocess.pid
|
261
|
+
|
262
|
+
while (line = oe.gets("\r"))
|
263
|
+
$logger.debug "****" + line
|
264
|
+
if (line =~ /time=([0-9]*\.[0-9]*)/)
|
265
|
+
$logger.debug '******' + $1
|
266
|
+
self.current_amount = $1.to_f
|
287
267
|
end
|
288
|
-
$logger.debug "
|
289
|
-
|
290
|
-
feedback.call self.fraction_complete = 1, self.time_remaining_s
|
268
|
+
$logger.debug "******** #{self.current_amount} #{self.fraction_complete}"
|
269
|
+
feedback.call self.fraction_complete, self.time_remaining_s
|
291
270
|
end
|
271
|
+
$logger.debug "reached end of file"
|
272
|
+
@state = :stopped
|
273
|
+
self.fraction_complete = 1
|
274
|
+
feedback.call self.fraction_complete, self.time_remaining_s
|
275
|
+
@coprocess.value # A little bit of a head game here. Either return this, or maybe have to do t.value.value in caller
|
292
276
|
end
|
293
277
|
|
294
278
|
def stop_encoding
|
@@ -1,7 +1,10 @@
|
|
1
1
|
module ProgressTracker
|
2
|
-
attr_reader :start_time
|
3
2
|
attr_writer :start_time, :total_amount
|
4
3
|
|
4
|
+
def start_time
|
5
|
+
@start_time || @start_time = Time.now
|
6
|
+
end
|
7
|
+
|
5
8
|
def percent_complete
|
6
9
|
self.fraction_complete * 100
|
7
10
|
end
|
@@ -19,9 +22,7 @@ module ProgressTracker
|
|
19
22
|
end
|
20
23
|
|
21
24
|
def current_amount=(amt)
|
22
|
-
|
23
|
-
|
24
|
-
# puts "Setting current_amount #{amt}"
|
25
|
+
self.start_time
|
25
26
|
@current_amount = amt
|
26
27
|
end
|
27
28
|
|
@@ -30,7 +31,11 @@ module ProgressTracker
|
|
30
31
|
end
|
31
32
|
|
32
33
|
def time_remaining
|
33
|
-
|
34
|
+
if self.fraction_complete == 0.0
|
35
|
+
1.0
|
36
|
+
else
|
37
|
+
(Time.new - self.start_time) * (1 - self.fraction_complete) / self.fraction_complete
|
38
|
+
end
|
34
39
|
end
|
35
40
|
|
36
41
|
def time_remaining_s(format = "%dh %02dm %02ds remaining")
|
@@ -1,6 +1,13 @@
|
|
1
1
|
require 'gtk2'
|
2
2
|
|
3
|
+
=begin rdoc
|
4
|
+
Run the standard save file dialog for screencaster
|
5
|
+
=end
|
3
6
|
class SaveFile
|
7
|
+
|
8
|
+
=begin rdoc
|
9
|
+
Create the file chooser dialogue with a default file name.
|
10
|
+
=end
|
4
11
|
def self.set_up_dialog(file_name = "output.mp4")
|
5
12
|
@dialog = Gtk::FileChooserDialog.new(
|
6
13
|
"Save File As ...",
|
@@ -10,22 +17,15 @@ class SaveFile
|
|
10
17
|
[ Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL ],
|
11
18
|
[ Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT ]
|
12
19
|
)
|
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
20
|
@dialog.current_name = GLib.filename_to_utf8(file_name)
|
25
21
|
@dialog.current_folder = GLib.filename_to_utf8(Dir.getwd)
|
26
22
|
@dialog.do_overwrite_confirmation = true
|
27
23
|
end
|
28
24
|
|
25
|
+
=begin rdoc
|
26
|
+
Do the workflow around saving a file, warning the user before allow
|
27
|
+
them to abandon their capture.
|
28
|
+
=end
|
29
29
|
def self.get_file_to_save
|
30
30
|
@dialog || SaveFile.set_up_dialog
|
31
31
|
result = @dialog.run
|
@@ -43,6 +43,10 @@ class SaveFile
|
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
|
+
=begin rdoc
|
47
|
+
Confirm cancellation when the user has captured something but not
|
48
|
+
saved it.
|
49
|
+
=end
|
46
50
|
def self.confirm_cancel(parent)
|
47
51
|
dialog = Gtk::Dialog.new(
|
48
52
|
"Confirm Cancel",
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: screencaster-gtk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5.alpha1
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-12-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: gdk_pixbuf2
|
@@ -141,3 +141,4 @@ signing_key:
|
|
141
141
|
specification_version: 3
|
142
142
|
summary: Screencaster
|
143
143
|
test_files: []
|
144
|
+
has_rdoc:
|