screencaster-gtk 0.0.5.alpha1 → 0.0.6.alpha1

Sign up to get free protection for your applications and to get access to all the features.
@@ -174,17 +174,15 @@ class ScreencasterGtk
174
174
  #### Done Status Icon
175
175
 
176
176
  def quit
177
- @@logger.debug "Quitting"
178
- # self.status_icon.destroy
179
- # @@logger.debug "After status icon destroy."
180
- # @window.destroy
181
- # @@logger.debug "After window destroy."
182
- # We don't want to destroy here because the object continues to exist
183
- # Just hide everything
184
- self.hide_all_including_status
185
- # self.status_icon.hide doesn't work/exist
186
- Gtk.main_quit
187
- @@logger.debug "After main_quit."
177
+ if 0 < @capture_window.raw_files.size && SaveFile.are_you_sure?(@window)
178
+ @@logger.debug "Quitting"
179
+ # We don't want to destroy here because the object continues to exist
180
+ # Just hide everything
181
+ self.hide_all_including_status
182
+ # self.status_icon.hide doesn't work/exist
183
+ Gtk.main_quit
184
+ @@logger.debug "After main_quit."
185
+ end
188
186
  end
189
187
 
190
188
  public
@@ -11,7 +11,7 @@ class Capture
11
11
  attr_writer :left, :top, :right, :bottom
12
12
  attr_reader :state
13
13
  attr_accessor :pid
14
- attr_accessor :tmp_files
14
+ attr_accessor :raw_files
15
15
 
16
16
  attr_accessor :capture_fps
17
17
  attr_accessor :encode_fps
@@ -24,13 +24,14 @@ class Capture
24
24
 
25
25
 
26
26
  def initialize
27
- @tmp_files = []
28
27
  @exit_chain = Signal.trap("EXIT") {
29
28
  self.cleanup
30
29
  $logger.debug "In capture about to chain to trap @exit_chain: #{@exit_chain}"
31
30
  @exit_chain.call unless @exit_chain.nil?
32
31
  }
33
32
  #$logger.debug "@exit_chain: #{@exit_chain.to_s}"
33
+
34
+ self.reset_without_cleanup
34
35
 
35
36
  @state = :stopped
36
37
  @coprocess = nil
@@ -57,11 +58,11 @@ class Capture
57
58
  end
58
59
 
59
60
  def current_tmp_file
60
- @tmp_files.last
61
+ @raw_files.last
61
62
  end
62
63
 
63
64
  def new_tmp_file
64
- @tmp_files << Capture.tmp_file_name(@tmp_files.size)
65
+ @raw_files << Capture.tmp_file_name(@raw_files.size)
65
66
  self.current_tmp_file
66
67
  end
67
68
 
@@ -72,6 +73,10 @@ class Capture
72
73
  def self.format_input_files_for_mkvmerge(files)
73
74
  files.drop(1).inject("\"#{files.first}\"") {|a, b| "#{a} + \"#{b}\"" }
74
75
  end
76
+
77
+ def video_segments_size
78
+ @raw_files.size
79
+ end
75
80
 
76
81
  def width
77
82
  @right - @left
@@ -121,32 +126,16 @@ class Capture
121
126
  end
122
127
 
123
128
  def record
124
- if @state != :paused
125
- @tmp_files = []
126
- self.total_amount = 0.0
127
- end
128
-
129
129
  output_file = self.new_tmp_file
130
130
 
131
131
  @state = :recording
132
- audio_options="-f alsa -ac 1 -ab #{@audio_sample_frequency} -i #{@audio_input} -acodec #{@acodec}"
133
132
 
134
133
  # And i should probably popen here, save the pid, then fork and start
135
134
  # reading the input, updating the number of frames saved, or the time
136
135
  # recorded.
137
136
  $logger.debug "Capturing...\n"
138
137
 
139
- cmd_line = "avconv \
140
- #{audio_options} \
141
- -f x11grab \
142
- -show_region 1 \
143
- -r #{@capture_fps} \
144
- -s #{@width}x#{@height} \
145
- -i :0.0+#{@left},#{@top} \
146
- -qscale #{@qscale} \
147
- -vcodec #{@capture_vcodec} \
148
- -y \
149
- #{output_file}"
138
+ cmd_line = record_command_line(output_file)
150
139
 
151
140
  $logger.debug cmd_line
152
141
 
@@ -199,29 +188,19 @@ class Capture
199
188
  state = :encoding
200
189
  output_file =~ /.mp4$/ || output_file += ".mp4"
201
190
 
202
- $logger.debug "Encoding #{Capture.format_input_files_for_mkvmerge(@tmp_files)}...\n"
191
+ $logger.debug "Encoding #{Capture.format_input_files_for_mkvmerge(@raw_files)}...\n"
203
192
  $logger.debug("Total duration #{self.total_amount.to_s}")
204
193
 
205
- merge(Capture.tmp_file_name, @tmp_files, feedback)
194
+ merge(Capture.tmp_file_name, @raw_files, feedback)
206
195
  final_encode(output_file, Capture.tmp_file_name, feedback)
207
196
  end
208
197
 
209
- # This is ugly.
210
- # When you open co-processes, they do get stuck together.
211
- # It seems the if I don't read what's coming out of the co-process, it waits.
212
- # But if I read it, then it goes right to the end until it returns.
213
-
214
198
  def merge(output_file, input_files, feedback = proc {} )
215
199
  $logger.debug("Merging #{input_files.size.to_s} files: #{Capture.format_input_files_for_mkvmerge(input_files)}")
216
200
  $logger.debug("Feedback #{feedback}")
217
201
 
218
- # TODO: cp doesn't give feedback like mkvmerge does...
219
- if input_files.size == 1
220
- cmd_line = "cp -v #{input_files[0]} #{output_file}"
221
- #cmd_line = "sleep 5"
222
- else
223
- cmd_line = "mkvmerge -v -o #{output_file} #{Capture.format_input_files_for_mkvmerge(input_files)}"
224
- end
202
+ cmd_line = merge_command_line(output_file, input_files)
203
+
225
204
  $logger.debug "merge: command line: #{cmd_line}"
226
205
  i, oe, @coprocess = Open3.popen2e(cmd_line)
227
206
  $logger.debug "merge: Thread from popen2e: #{@coprocess}"
@@ -248,11 +227,7 @@ class Capture
248
227
  # updating progress based on what I read, while the main body
249
228
  # returns and carries on.
250
229
 
251
- cmd_line = "avconv \
252
- -i #{input_file} \
253
- -vcodec #{@encode_vcodec} \
254
- -y \
255
- '#{output_file}'"
230
+ cmd_line = encode_command_line(output_file, input_file)
256
231
 
257
232
  $logger.debug cmd_line
258
233
 
@@ -271,7 +246,8 @@ class Capture
271
246
  $logger.debug "reached end of file"
272
247
  @state = :stopped
273
248
  self.fraction_complete = 1
274
- feedback.call self.fraction_complete, self.time_remaining_s
249
+ feedback.call self.fraction_complete, "Done"
250
+ reset
275
251
  @coprocess.value # A little bit of a head game here. Either return this, or maybe have to do t.value.value in caller
276
252
  end
277
253
 
@@ -282,10 +258,53 @@ class Capture
282
258
  $logger.error("No encoding to stop.")
283
259
  end
284
260
  end
261
+
262
+ def reset
263
+ cleanup
264
+ reset_without_cleanup
265
+ end
266
+
267
+ def reset_without_cleanup
268
+ @raw_files = []
269
+ self.total_amount = 0.0
270
+ end
285
271
 
286
272
  def cleanup
287
- @tmp_files.each { |f| File.delete(f) if File.exists?(f) }
273
+ @raw_files.each { |f| File.delete(f) if File.exists?(f) }
288
274
  File.delete(Capture.tmp_file_name) if File.exists?(Capture.tmp_file_name)
289
275
  end
276
+
277
+ def record_command_line(output_file)
278
+ audio_options="-f alsa -ac 1 -ab #{@audio_sample_frequency} -i #{@audio_input} -acodec #{@acodec}"
279
+ "avconv \
280
+ #{audio_options} \
281
+ -f x11grab \
282
+ -show_region 1 \
283
+ -r #{@capture_fps} \
284
+ -s #{@width}x#{@height} \
285
+ -i :0.0+#{@left},#{@top} \
286
+ -qscale #{@qscale} \
287
+ -vcodec #{@capture_vcodec} \
288
+ -y \
289
+ #{output_file}"
290
+ end
291
+
292
+ def merge_command_line(output_file, input_files)
293
+ # TODO: cp doesn't give feedback like mkvmerge does...
294
+ if input_files.size == 1
295
+ "cp -v #{input_files[0]} #{output_file}"
296
+ #cmd_line = "sleep 5"
297
+ else
298
+ "mkvmerge -v -o #{output_file} #{Capture.format_input_files_for_mkvmerge(input_files)}"
299
+ end
300
+ end
301
+
302
+ def encode_command_line(output_file, input_file)
303
+ "avconv \
304
+ -i '#{input_file}' \
305
+ -vcodec #{@encode_vcodec} \
306
+ -y \
307
+ '#{output_file}'"
308
+ end
290
309
  end
291
310
 
@@ -10,6 +10,7 @@ module ProgressTracker
10
10
  end
11
11
 
12
12
  def fraction_complete
13
+ raise ZeroDivisionError if self.total_amount == 0
13
14
  [ self.current_amount.to_f / self.total_amount.to_f, 1.0 ].min
14
15
  end
15
16
 
@@ -27,7 +28,7 @@ module ProgressTracker
27
28
  end
28
29
 
29
30
  def total_amount
30
- @total_amount || 1.0
31
+ @total_amount || 0.0
31
32
  end
32
33
 
33
34
  def time_remaining
@@ -23,7 +23,7 @@ Create the file chooser dialogue with a default file name.
23
23
  end
24
24
 
25
25
  =begin rdoc
26
- Do the workflow around saving a file, warning the user before allow
26
+ Do the workflow around saving a file, warning the user before allowing
27
27
  them to abandon their capture.
28
28
  =end
29
29
  def self.get_file_to_save
@@ -38,11 +38,13 @@ them to abandon their capture.
38
38
  self.confirm_cancel(@dialog)
39
39
  @dialog.hide
40
40
  else
41
- LOGGER.error("Can't happen #{__FILE__} line: #{__LINE__}")
41
+ $logger.error("Can't happen #{__FILE__} line: #{__LINE__}")
42
42
  @dialog.hide
43
43
  end
44
44
  end
45
45
 
46
+ # TODO: I think it's ugly that I have two different dialogues here.
47
+
46
48
  =begin rdoc
47
49
  Confirm cancellation when the user has captured something but not
48
50
  saved it.
@@ -75,5 +77,19 @@ saved it.
75
77
  else
76
78
  end
77
79
  end
80
+
81
+ def self.are_you_sure?(parent, verb = "quit")
82
+ d = Gtk::MessageDialog.new(parent,
83
+ Gtk::Dialog::DESTROY_WITH_PARENT,
84
+ Gtk::MessageDialog::QUESTION,
85
+ Gtk::MessageDialog::BUTTONS_YES_NO,
86
+ "You have unsaved work")
87
+
88
+ d.secondary_text = "If you #{verb} now, you will lose some video that you have captured. Are you sure you want to #{verb}?"
89
+
90
+ response = d.run
91
+ d.destroy
92
+ response == Gtk::Dialog::RESPONSE_YES
93
+ end
78
94
  end
79
95
 
data/test/capture.rb ADDED
@@ -0,0 +1,21 @@
1
+ # Redefine some methods in Capture for testing purposes.
2
+
3
+ class Capture
4
+ def get_window_to_capture
5
+ @left = 100
6
+ @top = 100
7
+ @width = 100
8
+ @height = 100
9
+ @height += @height % 2
10
+ @width += @width % 2
11
+
12
+ $logger.debug "Capturing #{@left},#{@top} to #{@left+@width},#{@top+@height}. Dimensions #{@width},#{@height}.\n"
13
+ end
14
+
15
+ def define_mock_capture_success
16
+ def self.record_command_line(output_file)
17
+ "touch '#{output_file}'"
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,6 @@
1
+ class ScreencasterGtk
2
+ def stop_recording
3
+ @@logger.debug "Stopped in test stub"
4
+ @capture_window.stop_recording
5
+ end
6
+ end
@@ -0,0 +1,180 @@
1
+ require 'test/unit'
2
+ require 'screencaster-gtk/capture'
3
+ require 'logger'
4
+ require 'fileutils'
5
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'test_utils')
6
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'capture')
7
+
8
+ # TODO: How to test the actual capture?
9
+ # There's non-trivial stuff in there, like getting the overall duration.
10
+
11
+ class TestCapture < Test::Unit::TestCase
12
+ include TestUtils
13
+
14
+ def test_merge_two_files
15
+ output = file_name("c-from-two.mkv")
16
+ File.delete(output) if File.exists?(output)
17
+ input = [ file_name("a.mkv"), file_name("b.mkv") ]
18
+
19
+ c = Capture.new
20
+ $logger.debug "test_merge_two_files: before merge"
21
+ assert_equal 0, c.merge(output, input)
22
+ assert File.exists?(output), "Output file #{output} not found."
23
+ assert_equal 1, Thread.list.size
24
+ end
25
+
26
+ def test_merge_one_file
27
+ output = file_name("c-from-one.mkv")
28
+ File.delete(output) if File.exists?(output)
29
+ input = [ file_name("a.mkv") ]
30
+
31
+ c = Capture.new
32
+ assert_equal 0, c.merge(output, input)
33
+ assert File.exists?(output), "Output file #{output} not found."
34
+ assert File.exists?(input[0]), "Input file #{input[0]} gone."
35
+ # Process should be gone by now
36
+ Thread.list.each { |t| puts t }
37
+ assert_equal 1, Thread.list.size
38
+ end
39
+
40
+ def test_block_merge
41
+ output = file_name("c-from-one.mkv")
42
+ File.delete(output) if File.exists?(output)
43
+ input = [ file_name("a.mkv") ]
44
+
45
+ amount_done = 0.0
46
+ c = Capture.new
47
+ r = c.merge(output, input) do | fraction, message |
48
+ puts "+++++++++++++++++ #{fraction}, #{message}"
49
+ amount_done = fraction
50
+ end
51
+ assert_equal 0, r
52
+ assert_equal 1.0, amount_done
53
+ assert_equal 1, Thread.list.size
54
+ end
55
+
56
+ def test_final_encode
57
+ o = "test-final-encode.mp4"
58
+ i = "c-from-two.mkv"
59
+ baseline = file_name(File.join("baseline", o))
60
+ output = file_name(o)
61
+ File.delete(output) if File.exists?(output)
62
+ input = file_name(i)
63
+ FileUtils.cp(file_name(File.join("baseline", i)), input)
64
+
65
+ c = Capture.new
66
+ c.total_amount = 1.0
67
+ assert_equal 0, c.final_encode(output, input)
68
+ assert File.exists?(output), "Output file #{output} not found."
69
+ `diff #{baseline} #{output}`
70
+ assert_equal 0, $?.exitstatus, "Output file different from baseline"
71
+ assert_equal 1, Thread.list.size
72
+ end
73
+
74
+ def test_record_failure
75
+ # This will fail since the capture area isn't set up
76
+ c = Capture.new
77
+ assert_not_equal 0, c.record
78
+ assert_equal 1, Thread.list.size
79
+ end
80
+
81
+ def test_merge_failure
82
+ output = file_name("c-from-one.mkv")
83
+ File.delete(output) if File.exists?(output)
84
+ input = [ file_name("file-does-not-exist.mkv") ]
85
+
86
+ c = Capture.new
87
+ assert_not_equal 0, c.merge(output, input)
88
+ assert_equal 1, Thread.list.size
89
+ end
90
+
91
+ def test_final_encode_failure
92
+ o = "test-final-encode.mp4"
93
+ i = "file-does-not-exist.mkv"
94
+ baseline = file_name(File.join("baseline", o))
95
+ output = file_name(o)
96
+ File.delete(output) if File.exists?(output)
97
+ input = file_name(i)
98
+
99
+ c = Capture.new
100
+ c.total_amount = 1.0
101
+ assert_not_equal 0, c.final_encode(output, input)
102
+ assert_equal 1, Thread.list.size
103
+ end
104
+
105
+ def test_default_total
106
+ c = Capture.new
107
+ assert_equal 0.0, c.total_amount
108
+ end
109
+
110
+ def test_total
111
+ c = Capture.new
112
+ c.total_amount = 2.0
113
+ assert_equal 2.0, c.total_amount
114
+ end
115
+
116
+ def test_current
117
+ c = Capture.new
118
+ c.total_amount = 1.0
119
+ c.current_amount = 0.25
120
+ assert_equal 0.25, c.current_amount
121
+ assert_equal 0.25, c.fraction_complete
122
+ assert_equal 25, c.percent_complete
123
+
124
+ c.total_amount = 0.5
125
+ assert_equal 0.5, c.fraction_complete
126
+ end
127
+
128
+ def test_time_remaining
129
+ c = Capture.new
130
+ c.total_amount = 1.0
131
+ c.start_time = Time.new
132
+ c.current_amount = 0.25
133
+ assert_in_delta(0.1, 0.1, c.time_remaining)
134
+ sleep 1
135
+ assert_in_delta(3, 0.1, c.time_remaining)
136
+ end
137
+
138
+ def test_time_remaining_none_done_yet
139
+ c = Capture.new
140
+ c.total_amount = 1.0
141
+ c.start_time = Time.new
142
+ assert_in_delta(0.1, 0.1, c.time_remaining)
143
+ end
144
+
145
+ def test_time_remaining_s
146
+ c = Capture.new
147
+ c.total_amount = 1.0
148
+ c.current_amount = 0.5
149
+ c.start_time = Time.new - 3661
150
+ assert_equal("1h 01m 01s remaining", c.time_remaining_s)
151
+ end
152
+
153
+ def test_set_fraction_complete
154
+ c = Capture.new
155
+ c.total_amount = 4
156
+ c.fraction_complete = 1
157
+ assert_not_nil c.current_amount
158
+ assert_equal 4, c.current_amount
159
+ end
160
+
161
+ def test_format_seconds
162
+ assert_equal "1h 01m 01s", Capture::ProgressTracker.format_seconds(3661)
163
+ end
164
+
165
+ def test_record_with_mock
166
+ c = Capture.new
167
+ c.get_window_to_capture
168
+ c.define_mock_capture_success
169
+ assert_equal 0, c.record.exitstatus
170
+ assert File.exists?("/tmp/screencaster_#{$$}_#{"%04d" % 0}.mkv"), "Record didn't create output file"
171
+ assert_equal 1, c.video_segments_size
172
+ assert_equal 0, c.record.exitstatus
173
+ assert File.exists?("/tmp/screencaster_#{$$}_#{"%04d" % 1}.mkv"), "Record didn't create output file"
174
+ assert_equal 2, c.video_segments_size
175
+ c.reset
176
+ assert_equal 0, c.video_segments_size
177
+ assert_equal 0, Dir.glob("/tmp/screencaster_#{$$}*").size
178
+ end
179
+ end
180
+
@@ -0,0 +1,65 @@
1
+ require 'test/unit'
2
+ require 'screencaster-gtk'
3
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'test_utils')
4
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'capture')
5
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'screencaster-gtk')
6
+
7
+ class TestScreencasterGtk < Test::Unit::TestCase
8
+ include TestUtils
9
+
10
+ ScreencasterGtk.logger = STDOUT
11
+ ScreencasterGtk.logger.formatter = proc do |severity, datetime, progname, msg|
12
+ "#{msg}\n"
13
+ end
14
+
15
+ def setup
16
+ @sc = ScreencasterGtk.new
17
+ end
18
+
19
+ def test_record
20
+ ScreencasterGtk.logger.debug("Thread exception: #{Thread.abort_on_exception}")
21
+ ScreencasterGtk.logger.debug("test_record")
22
+ # Uses the test definition of select defined in the local version of capture.rb
23
+ @sc.select
24
+ ScreencasterGtk.logger.debug("selected")
25
+ @sc.spawn_record
26
+ ScreencasterGtk.logger.debug("about to sleep")
27
+ sleep 2
28
+ ScreencasterGtk.logger.debug("woke up")
29
+ assert @sc.check_background, "check_background 1 failed"
30
+ ScreencasterGtk.logger.debug("About to stop")
31
+ @sc.stop_recording
32
+ ScreencasterGtk.logger.debug("Stopped (in test_record)")
33
+ assert @sc.check_background, "check_background 2 failed"
34
+ assert @sc.background_exitstatus, "Unexpected background failure"
35
+ assert @sc.check_background, "check_background 3 failed"
36
+ end
37
+
38
+ def test_encode
39
+ output = file_name("c.mkv")
40
+ File.delete output if File.exists? output
41
+ # Uses the test definition of select defined in the local version of capture.rb
42
+ @sc.select
43
+ @sc.capture_window.tmp_files = [file_name("a.mkv")]
44
+ @sc.capture_window.total_amount = 1
45
+ assert @sc.spawn_encode(output), "spawn_encode failed"
46
+ assert @sc.check_background, "check_background failed"
47
+ assert @sc.background_exitstatus, "Unexpected background failure"
48
+ end
49
+
50
+ def test_no_background_process
51
+ assert @sc.check_background, "check_background failed when no background process"
52
+ assert @sc.background_exitstatus, "Unexpected background failure"
53
+ end
54
+
55
+ def test_background_fails
56
+ # Uses the test definition of select defined in the local version of capture.rb
57
+ @sc.select
58
+ # Force a failure by giving a bogus sound device
59
+ @sc.capture_window.audio_input = 'bogus_audio'
60
+ @sc.spawn_record
61
+ sleep 1
62
+ assert ! @sc.check_background, "check_background should have returned false"
63
+ assert @sc.check_background, "check_background should not have returned false"
64
+ end
65
+ end
@@ -0,0 +1,17 @@
1
+ module TestUtils
2
+ Thread.abort_on_exception = true
3
+ TEST_FILE_PATH = File.dirname(__FILE__)
4
+
5
+ $logger = Logger.new(STDOUT)
6
+ $logger.formatter = proc do |severity, datetime, progname, msg|
7
+ "#{msg}\n"
8
+ end
9
+
10
+ def file_name(f)
11
+ File.join(TEST_FILE_PATH, f)
12
+ end
13
+
14
+ def baseline_file_name(f)
15
+ file_name(File.join('baseline', f))
16
+ end
17
+ end
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.5.alpha1
4
+ version: 0.0.6.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-15 00:00:00.000000000 Z
12
+ date: 2013-12-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: gdk_pixbuf2
@@ -110,6 +110,11 @@ files:
110
110
  - lib/screencaster-gtk/savefile.rb
111
111
  - lib/screencaster-gtk/capture.rb
112
112
  - lib/screencaster-gtk/progresstracker.rb
113
+ - test/test_screencaster_gtk.rb
114
+ - test/capture.rb
115
+ - test/screencaster-gtk.rb
116
+ - test/test_utils.rb
117
+ - test/test_capture.rb
113
118
  - bin/screencaster
114
119
  homepage: http://github.org/lcreid/screencaster
115
120
  licenses:
@@ -140,5 +145,10 @@ rubygems_version: 1.8.24
140
145
  signing_key:
141
146
  specification_version: 3
142
147
  summary: Screencaster
143
- test_files: []
148
+ test_files:
149
+ - test/test_screencaster_gtk.rb
150
+ - test/capture.rb
151
+ - test/screencaster-gtk.rb
152
+ - test/test_utils.rb
153
+ - test/test_capture.rb
144
154
  has_rdoc: