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.
@@ -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
@@ -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, :window
13
- attr_writer :status_icon
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 a 100K each
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
- LOGGER = Logger.new(File.join(log_dir, 'screencaster.log'), 5, 100000)
20
- LOGGER.level = Logger::DEBUG
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
- LOGGER.info "Started"
52
+ @@logger.info "Started"
36
53
 
37
54
  @window = Gtk::Window.new("Screencaster")
38
55
  @window.signal_connect("delete_event") {
39
- LOGGER.debug "delete event occurred"
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
- LOGGER.debug "destroy event occurred"
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
- LOGGER.debug "window_state_event #{e.to_s}"
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 = Gtk::Button.new("Select Window")
59
- @select_button.signal_connect("clicked") {
60
- self.select
61
- }
62
- control_bar.pack_start(@select_button, true, false)
63
-
64
- button = Gtk::Button.new
65
- button.image = QUIT_IMAGE
66
- button.signal_connect("clicked") {
67
- self.quit
68
- }
69
- control_bar.pack_end(button, true, false)
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(control_row, false, false)
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
- LOGGER.debug "Quitting"
177
+ @@logger.debug "Quitting"
202
178
  # self.status_icon.destroy
203
- # LOGGER.debug "After status icon destroy."
179
+ # @@logger.debug "After status icon destroy."
204
180
  # @window.destroy
205
- # LOGGER.debug "After window destroy."
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
- LOGGER.debug "After main_quit."
187
+ @@logger.debug "After main_quit."
212
188
  end
213
189
 
190
+ public
214
191
  def select
215
- LOGGER.debug "Selecting Window"
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
- LOGGER.debug "Record/Pause state: #{@capture_window.state}"
200
+ @@logger.debug "Record/Pause state: #{@capture_window.state}"
223
201
  case @capture_window.state
224
202
  when :recording
225
- LOGGER.debug "Record/Pause -- pause"
203
+ @@logger.debug "Record/Pause -- pause"
226
204
  pause
227
205
  when :paused, :stopped
228
- LOGGER.debug "Record/Pause -- record"
206
+ @@logger.debug "Record/Pause -- record"
229
207
  record
230
208
  else
231
- LOGGER.error "#{__FILE__} #{__LINE__}: Can't happen (state #{@capture_window.state})."
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
- LOGGER.debug "Recording"
214
+ @@logger.debug "Recording"
237
215
  recording
238
- @capture_window.record { |percent, time_elapsed|
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
- LOGGER.debug "Pausing"
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
- LOGGER.debug "Stopped"
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
- @capture_window.encode(filename) { |percent, time_remaining|
258
- @progress_bar.fraction = percent
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
- LOGGER.debug "Cancelled encoding"
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
- LOGGER.error "#{__FILE__} #{__LINE__}: Can't happen (state #{@capture_window.state})."
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
- LOGGER.debug "Toggle recording"
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
- LOGGER.error "#{__FILE__} #{__LINE__}: Can't happen (state #{@capture_window.state})."
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
- LOGGER.info "Starting"
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
- LOGGER.info "Finished"
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
- ScreencasterGtk::LOGGER.debug("pid_file is #{PIDFILE}")
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
- ScreencasterGtk::LOGGER.debug("Opened PIDFILE")
447
+ @@logger.debug("Opened PIDFILE")
405
448
  existing_pid = f.gets
406
449
  existing_pid = existing_pid.to_i
407
450
  f.close
408
- ScreencasterGtk::LOGGER.debug("existing_pid = #{existing_pid.to_s}")
451
+ @@logger.debug("existing_pid = #{existing_pid.to_s}")
409
452
  rescue StandardError
410
- LOGGER.error("File to read #{PIDFILE}")
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
- screencaster [OPTION] ...
428
-
429
- -h, --help:
430
- show help
431
-
432
- -s, -p, --start, --pause:
433
- Pause a running capture, or restart a paused capture
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
- ScreencasterGtk::LOGGER.debug("Got a pause for PID #{existing_pid}")
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
- LOGGER.info("Got a pause but PID #{existing_pid} didn't exist")
486
+ @@logger.info("Got a pause but PID #{existing_pid} didn't exist")
444
487
  exit 1
445
488
  end
446
489
  else
447
- ScreencasterGtk::LOGGER.info("Got a pause but no PID")
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
- (ScreencasterGtk::LOGGER.debug("Can't run two instances at once."); exit 1) if ! existing_pid.nil?
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
- LOGGER.debug "In ScreencasterGtk exit handler @chain: #{@chain}"
458
- ScreencasterGtk::LOGGER.debug("Exiting")
459
- ScreencasterGtk::LOGGER.debug("unlinking") if File.file?(PIDFILE)
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
- LOGGER.debug "About to call next trap with @chain: #{@chain}"
517
+ @@logger.debug "About to call next trap with @chain: #{@chain}"
464
518
  @chain.call unless @chain.nil?
465
519
  }
466
- LOGGER.debug "ScreencasterGtk.whatever @chain: #{@chain}"
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
- LOGGER.debug("Wrote PID #{Process.pid}")
530
+ @@logger.debug("Wrote PID #{Process.pid}")
477
531
  rescue StandardError
478
- LOGGER.error("Failed to write #{PIDFILE}")
532
+ @@logger.error("Failed to write #{PIDFILE}")
479
533
  exit 1
480
534
  end
481
535
 
482
536
  Signal.trap("USR1") {
483
- ScreencasterGtk::LOGGER.debug("Pause/Resume")
537
+ @@logger.debug("Pause/Resume")
484
538
  self.toggle_recording
485
539
  }
486
540
 
487
- $logger = ScreencasterGtk::LOGGER # TODO: Fix logging
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
- attr :pid
11
- attr :tmp_files
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
- $logger.debug "@exit_chain: #{@exit_chain.to_s}"
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
- capture_fps=24
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
- # @pid = Process.spawn("avconv \
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 0 -vcodec #{vcodec} \
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
- Thread.new do
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)
164
- end
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
- t = self.merge(Capture.tmp_file_name, @tmp_files)
205
- t.value
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 "Merge command line: #{cmd_line}"
226
- i, oe, t = Open3.popen2e(cmd_line)
227
- $logger.debug "Thread from popen2e: #{t}"
228
- @pid = t.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
- t = Thread.new do
233
- $logger.debug "Thread from Thread.new: #{t}"
234
- # $logger.debug "Sleeping..."
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 1.0, "Done"
235
+ feedback.call 0.5, "Merging..."
249
236
  end
250
237
  end
251
- return t
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, which the main body
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
- #{video_encoding_options} \
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, t = Open3.popen2e(cmd_line)
275
- @pid = t.pid
276
- Process.detach(@pid)
277
-
278
- Thread.new do
279
- while (line = oe.gets("\r"))
280
- $logger.debug "****" + line
281
- if (line =~ /time=([0-9]*\.[0-9]*)/)
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 "reached end of file"
289
- @state = :stopped
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
- @start_time || @start_time = Time.new
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
- (Time.new - @start_time) * (1 - self.fraction_complete) / self.fraction_complete
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.alpha1
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-11-29 00:00:00.000000000 Z
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: