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