screen-recorder 1.0.0 → 1.1.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 +7 -5
- data/.gitignore +99 -107
- data/.rspec +3 -3
- data/.rubocop.yml +45 -37
- data/.travis.yml +35 -32
- data/CHANGES.md +78 -70
- data/LICENSE.txt +21 -21
- data/README.md +218 -210
- data/Rakefile +6 -1
- data/bin/console +0 -0
- data/bin/setup +8 -8
- data/lib/screen-recorder.rb +53 -45
- data/lib/screen-recorder/common.rb +146 -127
- data/lib/screen-recorder/desktop.rb +40 -30
- data/lib/screen-recorder/errors.rb +9 -9
- data/lib/screen-recorder/options.rb +151 -148
- data/lib/screen-recorder/titles.rb +49 -81
- data/lib/screen-recorder/type_checker.rb +11 -11
- data/lib/screen-recorder/version.rb +2 -2
- data/lib/screen-recorder/window.rb +22 -22
- data/screen-recorder.gemspec +40 -31
- metadata +54 -21
@@ -1,10 +1,10 @@
|
|
1
|
-
module ScreenRecorder
|
2
|
-
# @since 1.0.0-beta5
|
3
|
-
module Errors
|
4
|
-
# @since 1.0.0-beta3
|
5
|
-
class ApplicationNotFound < StandardError; end
|
6
|
-
|
7
|
-
# @since 1.0.0-beta5
|
8
|
-
class DependencyNotFound < StandardError; end
|
9
|
-
end
|
1
|
+
module ScreenRecorder
|
2
|
+
# @since 1.0.0-beta5
|
3
|
+
module Errors
|
4
|
+
# @since 1.0.0-beta3
|
5
|
+
class ApplicationNotFound < StandardError; end
|
6
|
+
|
7
|
+
# @since 1.0.0-beta5
|
8
|
+
class DependencyNotFound < StandardError; end
|
9
|
+
end
|
10
10
|
end
|
@@ -1,148 +1,151 @@
|
|
1
|
-
# @since 1.0.0-beta11
|
2
|
-
module ScreenRecorder
|
3
|
-
# @since 1.0.0-beta11
|
4
|
-
class Options
|
5
|
-
DEFAULT_LOG_FILE
|
6
|
-
DEFAULT_FPS
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
#
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
vals
|
77
|
-
vals <<
|
78
|
-
|
79
|
-
|
80
|
-
vals <<
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
#
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
#
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
#
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
if
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
end
|
1
|
+
# @since 1.0.0-beta11
|
2
|
+
module ScreenRecorder
|
3
|
+
# @since 1.0.0-beta11
|
4
|
+
class Options
|
5
|
+
DEFAULT_LOG_FILE = 'ffmpeg.log'.freeze
|
6
|
+
DEFAULT_FPS = 15.0
|
7
|
+
DEFAULT_INPUT_PIX_FMT = 'uyvy422'.freeze # For macOS / avfoundation
|
8
|
+
DEFAULT_OUTPUT_PIX_FMT = 'yuv420p'.freeze
|
9
|
+
|
10
|
+
def initialize(options)
|
11
|
+
TypeChecker.check options, Hash
|
12
|
+
TypeChecker.check options[:advanced], Hash if options[:advanced]
|
13
|
+
@options = verify_options options
|
14
|
+
advanced[:framerate] ||= DEFAULT_FPS
|
15
|
+
advanced[:log] ||= DEFAULT_LOG_FILE
|
16
|
+
advanced[:pix_fmt] ||= DEFAULT_OUTPUT_PIX_FMT
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# Returns given input file or input
|
21
|
+
#
|
22
|
+
def input
|
23
|
+
@options[:input]
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Returns capture device in use
|
28
|
+
#
|
29
|
+
def capture_device
|
30
|
+
determine_capture_device
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Returns given output filepath
|
35
|
+
#
|
36
|
+
def output
|
37
|
+
@options[:output]
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Returns given values that are optional
|
42
|
+
#
|
43
|
+
def advanced
|
44
|
+
@options[:advanced] ||= {}
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Returns given framerate
|
49
|
+
#
|
50
|
+
def framerate
|
51
|
+
advanced[:framerate]
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Returns given log filename
|
56
|
+
#
|
57
|
+
def log
|
58
|
+
advanced[:log]
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Returns all given options
|
63
|
+
#
|
64
|
+
def all
|
65
|
+
@options
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Returns a String with all options parsed and
|
70
|
+
# ready for the ffmpeg process to use
|
71
|
+
#
|
72
|
+
def parsed
|
73
|
+
vals = "-f #{capture_device} "
|
74
|
+
vals << "-pix_fmt #{DEFAULT_INPUT_PIX_FMT} " if OS.mac? # Input pixel format
|
75
|
+
vals << advanced_options unless advanced.empty?
|
76
|
+
vals << "-i #{input} "
|
77
|
+
vals << "-pix_fmt #{advanced[:pix_fmt]} " if advanced[:pix_fmt] # Output pixel format
|
78
|
+
# Fix for using yuv420p
|
79
|
+
# @see https://www.reck.dk/ffmpeg-libx264-height-not-divisible-by-2/
|
80
|
+
vals << '-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" ' if advanced[:pix_fmt] == 'yuv420p'
|
81
|
+
vals << output
|
82
|
+
vals << ffmpeg_log_to(log)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
#
|
88
|
+
# Verifies the required options are provided and returns
|
89
|
+
# the given options Hash. Raises ArgumentError if all required
|
90
|
+
# options are not present in the given Hash.
|
91
|
+
#
|
92
|
+
def verify_options(options)
|
93
|
+
missing_options = required_options.select { |req| options[req].nil? }
|
94
|
+
err = "Required options are missing: #{missing_options}"
|
95
|
+
raise(ArgumentError, err) unless missing_options.empty?
|
96
|
+
|
97
|
+
options
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Returns Array of required options as Symbols
|
102
|
+
#
|
103
|
+
def required_options
|
104
|
+
%i[input output]
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Returns advanced options parsed and ready for ffmpeg to receive.
|
109
|
+
#
|
110
|
+
def advanced_options
|
111
|
+
arr = []
|
112
|
+
|
113
|
+
# Log file and output pixel format is handled separately
|
114
|
+
# at the end of the command
|
115
|
+
advanced.reject { |k, _| %i[log pix_fmt].include? k }
|
116
|
+
.each do |k, v|
|
117
|
+
arr.push "-#{k} #{v}"
|
118
|
+
end
|
119
|
+
arr.join(' ') + ' '
|
120
|
+
end
|
121
|
+
|
122
|
+
#
|
123
|
+
# Returns logging command with user given log file
|
124
|
+
# from options or the default file.
|
125
|
+
#
|
126
|
+
def ffmpeg_log_to(file)
|
127
|
+
file ||= DEFAULT_LOG_FILE
|
128
|
+
" 2> #{file}"
|
129
|
+
end
|
130
|
+
|
131
|
+
#
|
132
|
+
# Returns input capture device based on user given value or the current OS.
|
133
|
+
#
|
134
|
+
def determine_capture_device
|
135
|
+
# User given capture device or format
|
136
|
+
# @see https://www.ffmpeg.org/ffmpeg.html#Main-options
|
137
|
+
return advanced[:f] if advanced[:f]
|
138
|
+
return advanced[:fmt] if advanced[:fmt]
|
139
|
+
|
140
|
+
if OS.windows?
|
141
|
+
'gdigrab'
|
142
|
+
elsif OS.linux?
|
143
|
+
'x11grab'
|
144
|
+
elsif OS.mac?
|
145
|
+
'avfoundation'
|
146
|
+
else
|
147
|
+
raise NotImplementedError, 'Your OS is not supported.'
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -1,82 +1,50 @@
|
|
1
|
-
module ScreenRecorder
|
2
|
-
# @since 1.0.0-beta4
|
3
|
-
module Titles
|
4
|
-
# Regex to filter out "Window Title: N/A" from Chrome extensions and "Window Title: ".
|
5
|
-
# This is done to remove unusable titles and to match the Ffmpeg expected input format
|
6
|
-
# for capturing specific windows.
|
7
|
-
# For example, "Window Title: Google - Mozilla Firefox" becomes "Google - Mozilla Firefox".
|
8
|
-
FILTERED_TITLES = %r{^Window Title:( N/A|\s+)?}
|
9
|
-
|
10
|
-
#
|
11
|
-
# Returns a list of available window titles for the given application (process) name.
|
12
|
-
#
|
13
|
-
def self.fetch(application)
|
14
|
-
ScreenRecorder.logger.debug "Retrieving available windows for: #{application}"
|
15
|
-
WindowGrabber.new.available_windows_for application
|
16
|
-
end
|
17
|
-
|
18
|
-
# @since 1.0.0-beta4
|
19
|
-
class WindowGrabber
|
20
|
-
#
|
21
|
-
# Returns a
|
22
|
-
#
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
def linux_os_window(application)
|
51
|
-
ScreenRecorder.logger.warn 'Default capture device on Linux (x11grab) does not support window recording.'
|
52
|
-
raise DependencyNotFound, 'wmctrl is not installed. Run: sudo apt install wmctrl.' unless wmctrl_installed?
|
53
|
-
|
54
|
-
titles = `wmctrl -l | awk '{$3=""; $2=""; $1=""; print $0}'` # Returns all open windows
|
55
|
-
.split("\n")
|
56
|
-
.map(&:strip)
|
57
|
-
.select { |t| t.match?(/#{application}/i) } # Narrow down to given application
|
58
|
-
raise Errors::ApplicationNotFound, "No open windows found for: #{application}" if titles.empty?
|
59
|
-
|
60
|
-
titles
|
61
|
-
end
|
62
|
-
|
63
|
-
#
|
64
|
-
# Returns true if wmctrl is installed
|
65
|
-
#
|
66
|
-
def wmctrl_installed?
|
67
|
-
!`which wmctrl`.empty? # "" when not found
|
68
|
-
end
|
69
|
-
|
70
|
-
#
|
71
|
-
# Prints a warning if the retrieved list of window titles does no include
|
72
|
-
# the given application process name, which applications commonly do.
|
73
|
-
#
|
74
|
-
def warn_on_mismatch(titles, application)
|
75
|
-
unless titles.map(&:downcase).join(',').include? application.to_s
|
76
|
-
ScreenRecorder.logger.warn "Process name and window title(s) do not match: #{titles}"
|
77
|
-
ScreenRecorder.logger.warn "Please manually provide the displayed window title."
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end # class WindowGrabber
|
81
|
-
end # module Windows
|
1
|
+
module ScreenRecorder
|
2
|
+
# @since 1.0.0-beta4
|
3
|
+
module Titles
|
4
|
+
# Regex to filter out "Window Title: N/A" from Chrome extensions and "Window Title: ".
|
5
|
+
# This is done to remove unusable titles and to match the Ffmpeg expected input format
|
6
|
+
# for capturing specific windows.
|
7
|
+
# For example, "Window Title: Google - Mozilla Firefox" becomes "Google - Mozilla Firefox".
|
8
|
+
FILTERED_TITLES = %r{^Window Title:( N/A|\s+)?}.freeze
|
9
|
+
|
10
|
+
#
|
11
|
+
# Returns a list of available window titles for the given application (process) name.
|
12
|
+
#
|
13
|
+
def self.fetch(application)
|
14
|
+
ScreenRecorder.logger.debug "Retrieving available windows for: #{application}"
|
15
|
+
WindowGrabber.new.available_windows_for application
|
16
|
+
end
|
17
|
+
|
18
|
+
# @since 1.0.0-beta4
|
19
|
+
class WindowGrabber
|
20
|
+
#
|
21
|
+
# Returns a list of available window titles for the given application (process) name.
|
22
|
+
#
|
23
|
+
def available_windows_for(application)
|
24
|
+
raise NotImplementedError, 'Only Microsoft Windows (gdigrab) supports window capture.' unless OS.windows?
|
25
|
+
|
26
|
+
titles = `tasklist /v /fi "imagename eq #{application}.exe" /fo list | findstr Window`
|
27
|
+
.split("\n")
|
28
|
+
.map { |i| i.gsub(FILTERED_TITLES, '') }
|
29
|
+
.reject(&:empty?)
|
30
|
+
raise Errors::ApplicationNotFound, "No open windows found for: #{application}.exe" if titles.empty?
|
31
|
+
|
32
|
+
warn_on_mismatch(titles, application)
|
33
|
+
titles
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
#
|
39
|
+
# Prints a warning if the retrieved list of window titles does no include
|
40
|
+
# the given application process name, which applications commonly do.
|
41
|
+
#
|
42
|
+
def warn_on_mismatch(titles, application)
|
43
|
+
return if titles.map(&:downcase).join(',').include? application.to_s
|
44
|
+
|
45
|
+
ScreenRecorder.logger.warn "Process name and window title(s) do not match: #{titles}"
|
46
|
+
ScreenRecorder.logger.warn 'Please manually provide the displayed window title.'
|
47
|
+
end
|
48
|
+
end # class WindowGrabber
|
49
|
+
end # module Windows
|
82
50
|
end # module FFMPEG
|