screencaster-gtk 0.0.4.alpha1 → 0.0.5.alpha1
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.
- 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:
|