screen-recorder 1.3.0 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE.md +1 -0
- data/.github/workflows/tests.yml +97 -0
- data/.gitignore +99 -99
- data/.rspec +3 -3
- data/.rubocop.yml +41 -7
- data/CHANGELOG.md +20 -1
- data/LICENSE.txt +21 -21
- data/README.md +79 -18
- data/bin/console +0 -0
- data/bin/setup +8 -8
- data/lib/screen-recorder.rb +32 -4
- data/lib/screen-recorder/common.rb +82 -39
- data/lib/screen-recorder/desktop.rb +5 -3
- data/lib/screen-recorder/errors.rb +17 -9
- data/lib/screen-recorder/options.rb +18 -17
- data/lib/screen-recorder/screenshot.rb +39 -0
- data/lib/screen-recorder/version.rb +1 -1
- data/lib/screen-recorder/window.rb +5 -1
- data/screen-recorder.gemspec +13 -13
- metadata +28 -26
- data/.travis.yml +0 -42
- data/appveyor.yml +0 -39
- data/support/install_jruby.ps1 +0 -7
- data/support/start_test_reporter.sh +0 -7
- data/support/start_xvfb.sh +0 -5
- data/support/stop_test_reporter.sh +0 -5
data/README.md
CHANGED
@@ -1,19 +1,16 @@
|
|
1
1
|
# ScreenRecorder
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/screen-recorder.svg)](https://badge.fury.io/rb/screen-recorder)
|
4
|
-
[![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](https://www.rubydoc.info/
|
5
|
-
[![
|
6
|
-
[![AppVeyor status](https://ci.appveyor.com/api/projects/status/u1qashueuw82r235/branch/master?svg=true)](https://ci.appveyor.com/project/kapoorlakshya/screen-recorder/branch/master)
|
7
|
-
[![Maintainability](https://api.codeclimate.com/v1/badges/b6049dfee7375aed9bc8/maintainability)](https://codeclimate.com/github/kapoorlakshya/screen-recorder/maintainability)
|
8
|
-
[![Test Coverage](https://api.codeclimate.com/v1/badges/b6049dfee7375aed9bc8/test_coverage)](https://codeclimate.com/github/kapoorlakshya/screen-recorder/test_coverage)
|
4
|
+
[![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](https://www.rubydoc.info/gems/screen-recorder/)
|
5
|
+
[![Tests](https://github.com/kapoorlakshya/screen-recorder/actions/workflows/tests.yml/badge.svg)](https://github.com/kapoorlakshya/screen-recorder/actions/workflows/tests.yml)
|
9
6
|
|
10
|
-
A Ruby gem to video record your computer screen - desktop or specific
|
7
|
+
A Ruby gem to video record or take screenshots of your computer screen - desktop or specific
|
11
8
|
window - using [FFmpeg](https://www.ffmpeg.org/). Primarily
|
12
9
|
geared towards recording automated UI (Selenium) test executions for
|
13
10
|
debugging and documentation.
|
14
11
|
|
15
12
|
#### Demo
|
16
|
-
[https://kapoorlakshya.github.io/introducing-screen-recorder-ruby-gem](https://kapoorlakshya.github.io/introducing-screen-recorder-ruby-gem)
|
13
|
+
[https://kapoorlakshya.github.io/introducing-screen-recorder-ruby-gem](https://kapoorlakshya.github.io/introducing-screen-recorder-ruby-gem)
|
17
14
|
|
18
15
|
## Compatibility
|
19
16
|
|
@@ -23,11 +20,13 @@ Works on Windows, Linux, and macOS. Requires Ruby 2.0+ or JRuby 9.2+.
|
|
23
20
|
|
24
21
|
##### 1. Setup FFmpeg
|
25
22
|
|
26
|
-
Linux and macOS instructions are [here](https://
|
23
|
+
Linux and macOS instructions are [here](https://trac.ffmpeg.org/wiki/CompilationGuide).
|
27
24
|
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
> macOS: Follow [these steps](https://github.com/kapoorlakshya/screen-recorder/issues/88#issuecomment-629139032) to avoid
|
26
|
+
> issues related to Privacy settings.
|
27
|
+
|
28
|
+
For Microsoft Windows, download the binary from [here](https://www.videohelp.com/software/ffmpeg). Once downloaded,
|
29
|
+
add location of the `ffmpeg/bin` folder to the `PATH` environment variable ([instructions](https://windowsloop.com/install-ffmpeg-windows-10/)).
|
31
30
|
|
32
31
|
Alternatively, you can point to the binary file using
|
33
32
|
`ScreenRecorder.ffmpeg_binary = '/path/to/ffmpeg'` in your project.
|
@@ -38,13 +37,13 @@ Next, add these lines to your application's Gemfile:
|
|
38
37
|
|
39
38
|
```ruby
|
40
39
|
gem 'ffi' # Windows only
|
41
|
-
gem 'screen-recorder'
|
40
|
+
gem 'screen-recorder', '~> 1.0'
|
42
41
|
```
|
43
42
|
|
44
43
|
The [`ffi`](https://github.com/ffi/ffi) gem is used by the
|
45
44
|
[`childprocess`](https://github.com/enkessler/childprocess) gem on
|
46
45
|
Windows, but it does not explicitly require it. More information
|
47
|
-
on this [here](https://github.com/enkessler/childprocess/issues/
|
46
|
+
on this [here](https://github.com/enkessler/childprocess/issues/160).
|
48
47
|
|
49
48
|
|
50
49
|
And then execute:
|
@@ -80,7 +79,7 @@ require 'screen-recorder'
|
|
80
79
|
```
|
81
80
|
|
82
81
|
Linux and macOS users can optionally provide a display or input device number.
|
83
|
-
Read more about it in the wiki [here](https://github.com/kapoorlakshya/screen-recorder/wiki/Input-
|
82
|
+
Read more about it in the wiki [here](https://github.com/kapoorlakshya/screen-recorder/wiki/Display-or-Input-Device-Selection).
|
84
83
|
|
85
84
|
#### Record Application Window (Microsoft Windows only)
|
86
85
|
|
@@ -96,7 +95,7 @@ browser = Watir::Browser.new :firefox
|
|
96
95
|
@recorder.stop
|
97
96
|
browser.quit
|
98
97
|
```
|
99
|
-
This mode has
|
98
|
+
This mode has a few limitations which are listed in the wiki
|
100
99
|
[here](https://github.com/kapoorlakshya/screen-recorder/wiki/Window-Capture-Limitations).
|
101
100
|
|
102
101
|
##### Fetch Title
|
@@ -105,14 +104,76 @@ A helper method is available to fetch the title of the active window
|
|
105
104
|
for the given process name.
|
106
105
|
|
107
106
|
```ruby
|
108
|
-
ScreenRecorder::
|
107
|
+
ScreenRecorder::('firefox') # Name of exe
|
109
108
|
#=> ["Mozilla Firefox"]
|
110
109
|
|
111
|
-
ScreenRecorder::
|
110
|
+
ScreenRecorder::('chrome')
|
112
111
|
#=> ["New Tab - Google Chrome"]
|
113
112
|
```
|
114
113
|
|
115
|
-
####
|
114
|
+
#### Capture Audio
|
115
|
+
|
116
|
+
Provide the following `advanced` configurations to capture audio:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
# Linux
|
120
|
+
advanced = { f: 'alsa', ac: 2, i: 'hw:0'} # Using ALSA
|
121
|
+
# Or using PulseAudio
|
122
|
+
advanced = { 'f': 'pulse', 'ac': 2, 'i': 'default' } # Records default sound output device
|
123
|
+
|
124
|
+
# macOS
|
125
|
+
advanced = { input: { i: '1:1' } } # -i video:audio input device ID
|
126
|
+
|
127
|
+
# Windows
|
128
|
+
advanced = { f: 'dshow', i: 'audio="Microphone (Realtek High Definition Audio)"' }
|
129
|
+
```
|
130
|
+
|
131
|
+
You can retrieve a list of audio devices by running these commands:
|
132
|
+
|
133
|
+
```
|
134
|
+
# Linux
|
135
|
+
$ arecord -L # See https://trac.ffmpeg.org/wiki/Capture/ALSA
|
136
|
+
|
137
|
+
# macOS
|
138
|
+
$ ffmpeg -f avfoundation -list_devices true -i ""
|
139
|
+
|
140
|
+
# Windows
|
141
|
+
> ffmpeg -list_devices true -f dshow -i dummy
|
142
|
+
```
|
143
|
+
|
144
|
+
#### Screenshots
|
145
|
+
|
146
|
+
Screenshots can be captured at any point after initializing the recorder:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
# Desktop
|
150
|
+
@recorder = ScreenRecorder::Desktop.new(output: 'recording.mkv')
|
151
|
+
@recorder.screenshot('before-recording.png')
|
152
|
+
@recorder.start
|
153
|
+
@recorder.screenshot('during-recording.png')
|
154
|
+
@recorder.stop
|
155
|
+
@recorder.screenshot('after-recording.png')
|
156
|
+
|
157
|
+
# Window (Microsoft Windows only)
|
158
|
+
browser = Watir::Browser.new :chrome, options: { args: ['--disable-gpu'] } # Hardware acceleration must be disabled
|
159
|
+
browser.goto('watir.com')
|
160
|
+
window_title = ScreenRecorder::('chrome').first
|
161
|
+
@recorder = ScreenRecorder::Window.new(title: window_title, output: 'recording.mkv')
|
162
|
+
@recorder.screenshot('before-recording.png')
|
163
|
+
@recorder.start
|
164
|
+
@recorder.screenshot('during-recording.png')
|
165
|
+
@recorder.stop
|
166
|
+
@recorder.screenshot('after-recording.png')
|
167
|
+
browser.quit
|
168
|
+
```
|
169
|
+
|
170
|
+
You can even specify a custom capture resolution:
|
171
|
+
|
172
|
+
```rb
|
173
|
+
@recorder.screenshot('screenshot.png', '1024x768')
|
174
|
+
```
|
175
|
+
|
176
|
+
#### Video Output
|
116
177
|
|
117
178
|
Once the recorder is stopped, you can view the video metadata or transcode
|
118
179
|
it if desired.
|
data/bin/console
CHANGED
File without changes
|
data/bin/setup
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
#!/usr/bin/env bash
|
2
|
-
set -euo pipefail
|
3
|
-
IFS=$'\n\t'
|
4
|
-
set -vx
|
5
|
-
|
6
|
-
bundle install
|
7
|
-
|
8
|
-
# Do any other automated setup that you need to do here
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
set -euo pipefail
|
3
|
+
IFS=$'\n\t'
|
4
|
+
set -vx
|
5
|
+
|
6
|
+
bundle install
|
7
|
+
|
8
|
+
# Do any other automated setup that you need to do here
|
data/lib/screen-recorder.rb
CHANGED
@@ -12,14 +12,41 @@ module ScreenRecorder
|
|
12
12
|
# ScreenRecorder.ffmpeg_binary = 'C:\ffmpeg.exe'
|
13
13
|
#
|
14
14
|
def self.ffmpeg_binary=(bin)
|
15
|
+
ScreenRecorder.logger.debug 'Setting ffmpeg path...'
|
15
16
|
FFMPEG.ffmpeg_binary = bin
|
17
|
+
ScreenRecorder.logger.debug "ffmpeg path set: #{bin}"
|
18
|
+
ScreenRecorder.ffmpeg_binary
|
16
19
|
end
|
17
20
|
|
18
21
|
#
|
19
|
-
# Returns path to ffmpeg binary
|
22
|
+
# Returns path to ffmpeg binary or raises DependencyNotFound
|
20
23
|
#
|
21
24
|
def self.ffmpeg_binary
|
22
25
|
FFMPEG.ffmpeg_binary
|
26
|
+
rescue Errno::ENOENT # Raised when binary is not set in project or found in ENV
|
27
|
+
raise Errors::DependencyNotFound
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Uses user given ffprobe binary
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# ScreenRecorder.ffprobe_binary= = 'C:\ffprobe.exe'
|
35
|
+
#
|
36
|
+
def self.ffprobe_binary=(bin)
|
37
|
+
ScreenRecorder.logger.debug 'Setting ffprobe path...'
|
38
|
+
FFMPEG.ffprobe_binary = bin
|
39
|
+
ScreenRecorder.logger.debug "ffprobe path set: #{bin}"
|
40
|
+
ScreenRecorder.ffmpeg_binary
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Returns path to ffprobe binary or raises DependencyNotFound
|
45
|
+
#
|
46
|
+
def self.ffprobe_binary
|
47
|
+
FFMPEG.ffprobe_binary
|
48
|
+
rescue Errno::ENOENT # Raised when binary is not set in project or found in ENV
|
49
|
+
raise Errors::DependencyNotFound
|
23
50
|
end
|
24
51
|
|
25
52
|
#
|
@@ -35,9 +62,9 @@ module ScreenRecorder
|
|
35
62
|
def self.logger
|
36
63
|
return @logger if @logger
|
37
64
|
|
38
|
-
logger
|
39
|
-
logger.level
|
40
|
-
logger.progname
|
65
|
+
logger = Logger.new($stdout)
|
66
|
+
logger.level = Logger::ERROR
|
67
|
+
logger.progname = 'ScreenRecorder'
|
41
68
|
logger.formatter = proc do |severity, time, progname, msg|
|
42
69
|
"#{time.strftime('%F %T')} #{progname} - #{severity} - #{msg}\n"
|
43
70
|
end
|
@@ -51,5 +78,6 @@ require 'screen-recorder/errors'
|
|
51
78
|
require 'screen-recorder/options'
|
52
79
|
require 'screen-recorder/titles'
|
53
80
|
require 'screen-recorder/common'
|
81
|
+
require 'screen-recorder/screenshot'
|
54
82
|
require 'screen-recorder/desktop'
|
55
83
|
require 'screen-recorder/window'
|
@@ -9,6 +9,8 @@ module ScreenRecorder
|
|
9
9
|
attr_reader :options, :video
|
10
10
|
|
11
11
|
def initialize(input:, output:, advanced: {})
|
12
|
+
raise Errors::DependencyNotFound unless ffmpeg_exists?
|
13
|
+
|
12
14
|
@options = Options.new(input: input, output: output, advanced: advanced)
|
13
15
|
@video = nil
|
14
16
|
@process = nil
|
@@ -19,8 +21,8 @@ module ScreenRecorder
|
|
19
21
|
#
|
20
22
|
def start
|
21
23
|
ScreenRecorder.logger.debug 'Starting recorder...'
|
22
|
-
@video
|
23
|
-
@process
|
24
|
+
@video = nil # New file
|
25
|
+
@process = start_ffmpeg
|
24
26
|
ScreenRecorder.logger.info 'Recording...'
|
25
27
|
@process
|
26
28
|
end
|
@@ -30,10 +32,12 @@ module ScreenRecorder
|
|
30
32
|
#
|
31
33
|
def stop
|
32
34
|
ScreenRecorder.logger.debug 'Stopping ffmpeg...'
|
33
|
-
stop_ffmpeg
|
35
|
+
exit_code = stop_ffmpeg
|
36
|
+
return if exit_code == 1 # recording failed
|
37
|
+
|
34
38
|
ScreenRecorder.logger.debug 'Stopped ffmpeg.'
|
35
39
|
ScreenRecorder.logger.info 'Recording complete.'
|
36
|
-
@video =
|
40
|
+
@video = prepare_video unless exit_code == 1
|
37
41
|
end
|
38
42
|
|
39
43
|
#
|
@@ -42,7 +46,7 @@ module ScreenRecorder
|
|
42
46
|
# needed.
|
43
47
|
#
|
44
48
|
def discard
|
45
|
-
|
49
|
+
File.delete options.output
|
46
50
|
end
|
47
51
|
|
48
52
|
alias delete discard
|
@@ -54,17 +58,9 @@ module ScreenRecorder
|
|
54
58
|
# the given options.
|
55
59
|
#
|
56
60
|
def start_ffmpeg
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
process = build_command
|
61
|
-
@log_file = File.new(options.log, 'w+')
|
62
|
-
process.io.stdout = process.io.stderr = @log_file
|
63
|
-
@log_file.sync = true
|
64
|
-
process.duplex = true
|
65
|
-
process.start
|
66
|
-
sleep(1.5) # Takes ~1.5s on average to initialize
|
67
|
-
# Stopped because of an error
|
61
|
+
process = execute_command(ffmpeg_command, options.log)
|
62
|
+
sleep(1.5) # Takes ~1.5s to initialize ffmpeg
|
63
|
+
# Check if it exited unexpectedly
|
68
64
|
raise FFMPEG::Error, "Failed to start ffmpeg. Reason: #{lines_from_log(:last, 2)}" if process.exited?
|
69
65
|
|
70
66
|
process
|
@@ -78,37 +74,51 @@ module ScreenRecorder
|
|
78
74
|
@process.io.stdin.puts 'q' # Gracefully exit ffmpeg
|
79
75
|
@process.io.stdin.close
|
80
76
|
@log_file.close
|
81
|
-
@process
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
77
|
+
wait_for_process_exit(@process)
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Runs ffprobe on the output video file and returns
|
82
|
+
# a FFMPEG::Movie object.
|
83
|
+
#
|
84
|
+
def prepare_video
|
85
|
+
max_attempts = 3
|
86
|
+
attempts_made = 0
|
87
|
+
delay = 1.0
|
88
|
+
|
89
|
+
begin # Fixes #79
|
90
|
+
ScreenRecorder.logger.info 'Running ffprobe to prepare video (output) file.'
|
91
|
+
FFMPEG::Movie.new(options.output)
|
92
|
+
rescue Errno::EAGAIN, Errno::EACCES
|
93
|
+
attempts_made += 1
|
94
|
+
ScreenRecorder.logger.error "Failed to run ffprobe. Retrying... (#{attempts_made}/#{max_attempts})"
|
95
|
+
sleep(delay)
|
96
|
+
retry if attempts_made < max_attempts
|
97
|
+
raise
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def ffmpeg_bin
|
102
|
+
"#{ScreenRecorder.ffmpeg_binary} -y"
|
87
103
|
end
|
88
104
|
|
89
105
|
#
|
90
106
|
# Generates the command line arguments based on the given
|
91
107
|
# options.
|
92
108
|
#
|
93
|
-
def
|
94
|
-
|
95
|
-
cmd << @options.parsed
|
109
|
+
def ffmpeg_command
|
110
|
+
"#{ffmpeg_bin} #{@options.parsed}"
|
96
111
|
end
|
97
112
|
|
98
113
|
#
|
99
114
|
# Returns true if ffmpeg binary is found.
|
100
115
|
#
|
101
116
|
def ffmpeg_exists?
|
102
|
-
return
|
103
|
-
|
104
|
-
return !`where ffmpeg`.empty? if OS.windows?
|
117
|
+
return true if FFMPEG.ffmpeg_binary
|
105
118
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
return false if ScreenRecorder.ffmpeg_binary == 'ffmpeg'
|
110
|
-
|
111
|
-
true
|
119
|
+
false
|
120
|
+
rescue Errno::ENOENT # Raised when binary is not set in project or found in ENV
|
121
|
+
false
|
112
122
|
end
|
113
123
|
|
114
124
|
#
|
@@ -125,15 +135,48 @@ module ScreenRecorder
|
|
125
135
|
end
|
126
136
|
|
127
137
|
#
|
128
|
-
#
|
138
|
+
# Executes the given command and outputs to the
|
139
|
+
# optional logfile
|
140
|
+
#
|
141
|
+
def execute_command(cmd, logfile = nil)
|
142
|
+
ScreenRecorder.logger.debug "Executing command: #{cmd}"
|
143
|
+
process = new_process(cmd)
|
144
|
+
process.duplex = true
|
145
|
+
if logfile
|
146
|
+
@log_file = File.new(logfile, 'w+')
|
147
|
+
process.io.stdout = process.io.stderr = @log_file
|
148
|
+
@log_file.sync = true
|
149
|
+
end
|
150
|
+
process.start
|
151
|
+
process
|
152
|
+
end
|
153
|
+
|
129
154
|
#
|
130
|
-
|
131
|
-
|
155
|
+
# Calls Childprocess.new with OS specific arguments
|
156
|
+
# to start the given process.
|
157
|
+
#
|
158
|
+
def new_process(process)
|
159
|
+
ChildProcess.posix_spawn = true if RUBY_PLATFORM == 'java' # Support JRuby.
|
132
160
|
if OS.windows?
|
133
|
-
ChildProcess.
|
161
|
+
ChildProcess.new('cmd.exe', '/c', process)
|
134
162
|
else
|
135
|
-
ChildProcess.
|
163
|
+
ChildProcess.new('sh', '-c', process)
|
136
164
|
end
|
137
165
|
end
|
166
|
+
|
167
|
+
#
|
168
|
+
# Waits for given process to exit.
|
169
|
+
# Forcefully kills the process if it does not exit within 5 seconds.
|
170
|
+
# Returns exit code.
|
171
|
+
#
|
172
|
+
def wait_for_process_exit(process)
|
173
|
+
process.poll_for_exit(PROCESS_TIMEOUT)
|
174
|
+
process.exit_code # 0
|
175
|
+
rescue ChildProcess::TimeoutError
|
176
|
+
ScreenRecorder.logger.error 'ffmpeg failed to stop. Force killing it...'
|
177
|
+
process.stop # Tries increasingly harsher methods to kill the process.
|
178
|
+
ScreenRecorder.logger.error 'Forcefully killed ffmpeg. Recording failed!'
|
179
|
+
1
|
180
|
+
end
|
138
181
|
end
|
139
182
|
end
|
@@ -2,14 +2,16 @@
|
|
2
2
|
module ScreenRecorder
|
3
3
|
# @since 1.0.0-beta11
|
4
4
|
class Desktop < Common
|
5
|
-
|
5
|
+
include Screenshot
|
6
|
+
|
7
|
+
DEFAULT_INPUT_WIN = 'desktop'.freeze
|
6
8
|
DEFAULT_INPUT_LINUX = ':0'.freeze
|
7
|
-
DEFAULT_INPUT_MAC
|
9
|
+
DEFAULT_INPUT_MAC = '1'.freeze
|
8
10
|
|
9
11
|
#
|
10
12
|
# Desktop recording mode.
|
11
13
|
#
|
12
|
-
def initialize(input: input_by_os,
|
14
|
+
def initialize(output:, input: input_by_os, advanced: {})
|
13
15
|
super(input: determine_input(input), output: output, advanced: advanced)
|
14
16
|
end
|
15
17
|
|
@@ -1,10 +1,18 @@
|
|
1
|
-
module ScreenRecorder
|
2
|
-
# @since 1.0.0-beta5
|
3
|
-
module Errors
|
4
|
-
# @since 1.0.0-beta3
|
5
|
-
class ApplicationNotFound < StandardError
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
1
|
+
module ScreenRecorder
|
2
|
+
# @since 1.0.0-beta5
|
3
|
+
module Errors
|
4
|
+
# @since 1.0.0-beta3
|
5
|
+
class ApplicationNotFound < StandardError
|
6
|
+
def message
|
7
|
+
'expected application was not found by ffmpeg.'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# @since 1.0.0-beta5
|
12
|
+
class DependencyNotFound < StandardError
|
13
|
+
def message
|
14
|
+
'ffmpeg/ffprobe binary path not set or not found in ENV.'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
10
18
|
end
|