screen-recorder 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +99 -99
- data/.rspec +3 -3
- data/.rubocop.yml +1 -1
- data/.travis.yml +49 -35
- data/CHANGES.md +89 -78
- data/LICENSE.txt +21 -21
- data/README.md +212 -218
- data/appveyor.yml +23 -0
- data/bin/console +0 -0
- data/bin/setup +8 -8
- data/lib/screen-recorder.rb +54 -53
- data/lib/screen-recorder/common.rb +152 -146
- data/lib/screen-recorder/desktop.rb +40 -40
- data/lib/screen-recorder/errors.rb +9 -9
- data/lib/screen-recorder/options.rb +168 -151
- data/lib/screen-recorder/titles.rb +52 -49
- data/lib/screen-recorder/type_checker.rb +13 -11
- data/lib/screen-recorder/version.rb +2 -2
- data/lib/screen-recorder/window.rb +22 -22
- data/screen-recorder.gemspec +41 -39
- metadata +34 -6
@@ -1,41 +1,41 @@
|
|
1
|
-
# @since 1.0.0-beta11
|
2
|
-
module ScreenRecorder
|
3
|
-
# @since 1.0.0-beta11
|
4
|
-
class Desktop < Common
|
5
|
-
DEFAULT_INPUT_WIN = 'desktop'.freeze
|
6
|
-
DEFAULT_INPUT_LINUX = ':0'.freeze
|
7
|
-
DEFAULT_INPUT_MAC = '1'.freeze
|
8
|
-
|
9
|
-
#
|
10
|
-
# Desktop recording specific initializer.
|
11
|
-
#
|
12
|
-
def initialize(input: input_by_os, output:, advanced: {})
|
13
|
-
super(input: determine_input(input), output: output, advanced: advanced)
|
14
|
-
end
|
15
|
-
|
16
|
-
private
|
17
|
-
|
18
|
-
#
|
19
|
-
# Returns default input value for current OS
|
20
|
-
#
|
21
|
-
def input_by_os
|
22
|
-
return DEFAULT_INPUT_WIN if OS.windows?
|
23
|
-
|
24
|
-
return DEFAULT_INPUT_LINUX if OS.linux?
|
25
|
-
|
26
|
-
return DEFAULT_INPUT_MAC if OS.mac?
|
27
|
-
|
28
|
-
raise NotImplementedError, 'Your OS is not supported. Feel free to create an Issue on GitHub.'
|
29
|
-
end
|
30
|
-
|
31
|
-
#
|
32
|
-
# Returns FFmpeg expected input based on user given value or
|
33
|
-
# default for the current OS.
|
34
|
-
#
|
35
|
-
def determine_input(val)
|
36
|
-
return val if val
|
37
|
-
|
38
|
-
input_by_os
|
39
|
-
end
|
40
|
-
end
|
1
|
+
# @since 1.0.0-beta11
|
2
|
+
module ScreenRecorder
|
3
|
+
# @since 1.0.0-beta11
|
4
|
+
class Desktop < Common
|
5
|
+
DEFAULT_INPUT_WIN = 'desktop'.freeze
|
6
|
+
DEFAULT_INPUT_LINUX = ':0'.freeze
|
7
|
+
DEFAULT_INPUT_MAC = '1'.freeze
|
8
|
+
|
9
|
+
#
|
10
|
+
# Desktop recording specific initializer.
|
11
|
+
#
|
12
|
+
def initialize(input: input_by_os, output:, advanced: {})
|
13
|
+
super(input: determine_input(input), output: output, advanced: advanced)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
#
|
19
|
+
# Returns default input value for current OS
|
20
|
+
#
|
21
|
+
def input_by_os
|
22
|
+
return DEFAULT_INPUT_WIN if OS.windows?
|
23
|
+
|
24
|
+
return DEFAULT_INPUT_LINUX if OS.linux?
|
25
|
+
|
26
|
+
return DEFAULT_INPUT_MAC if OS.mac?
|
27
|
+
|
28
|
+
raise NotImplementedError, 'Your OS is not supported. Feel free to create an Issue on GitHub.'
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Returns FFmpeg expected input based on user given value or
|
33
|
+
# default for the current OS.
|
34
|
+
#
|
35
|
+
def determine_input(val)
|
36
|
+
return val if val
|
37
|
+
|
38
|
+
input_by_os
|
39
|
+
end
|
40
|
+
end
|
41
41
|
end
|
@@ -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,151 +1,168 @@
|
|
1
|
-
# @since 1.0.0-beta11
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
vals
|
76
|
-
vals <<
|
77
|
-
vals << "-
|
78
|
-
|
79
|
-
|
80
|
-
vals <<
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
1
|
+
# @since 1.0.0-beta11
|
2
|
+
#
|
3
|
+
# @api private
|
4
|
+
module ScreenRecorder
|
5
|
+
# @since 1.0.0-beta11
|
6
|
+
class Options
|
7
|
+
attr_reader :all
|
8
|
+
|
9
|
+
DEFAULT_LOG_FILE = 'ffmpeg.log'.freeze
|
10
|
+
DEFAULT_FPS = 15.0
|
11
|
+
DEFAULT_MAC_INPUT_PIX_FMT = 'uyvy422'.freeze # For avfoundation
|
12
|
+
DEFAULT_PIX_FMT = 'yuv420p'.freeze
|
13
|
+
YUV420P_SCALING = '"scale=trunc(iw/2)*2:trunc(ih/2)*2"'.freeze
|
14
|
+
|
15
|
+
def initialize(options)
|
16
|
+
# @todo Consider using OpenStruct
|
17
|
+
@all = verify_options options
|
18
|
+
advanced[:input] = default_advanced_input.merge(advanced_input)
|
19
|
+
advanced[:output] = default_advanced_output.merge(advanced_output)
|
20
|
+
advanced[:log] ||= DEFAULT_LOG_FILE
|
21
|
+
|
22
|
+
# Fix for using yuv420p pixel format for output
|
23
|
+
# @see https://www.reck.dk/ffmpeg-libx264-height-not-divisible-by-2/
|
24
|
+
advanced_output[:vf] = YUV420P_SCALING if advanced_output[:pix_fmt] == 'yuv420p'
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Returns given input file or input
|
29
|
+
#
|
30
|
+
def input
|
31
|
+
@all[:input]
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# Returns capture device in use
|
36
|
+
#
|
37
|
+
def capture_device
|
38
|
+
determine_capture_device
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Returns given output filepath
|
43
|
+
#
|
44
|
+
def output
|
45
|
+
@all[:output]
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Returns given values that are optional
|
50
|
+
#
|
51
|
+
def advanced
|
52
|
+
@all[:advanced] ||= {}
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Returns given framerate
|
57
|
+
#
|
58
|
+
def framerate
|
59
|
+
ScreenRecorder.logger.warn '#framerate will not be available in the next release. Use #advanced instead.'
|
60
|
+
advanced[:output][:framerate]
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# Returns given log filename
|
65
|
+
#
|
66
|
+
def log
|
67
|
+
advanced[:log]
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Returns a String with all options parsed and
|
72
|
+
# ready for the ffmpeg process to use
|
73
|
+
#
|
74
|
+
def parsed
|
75
|
+
vals = "-f #{capture_device} "
|
76
|
+
vals << parse_advanced(advanced_input)
|
77
|
+
vals << "-i #{input} "
|
78
|
+
vals << parse_advanced(advanced_output)
|
79
|
+
vals << parse_advanced(advanced)
|
80
|
+
vals << output
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
#
|
86
|
+
# Verifies the required options are provided and returns
|
87
|
+
# the given options Hash. Raises ArgumentError if all required
|
88
|
+
# options are not present in the given Hash.
|
89
|
+
#
|
90
|
+
def verify_options(options)
|
91
|
+
TypeChecker.check options, Hash
|
92
|
+
TypeChecker.check options[:advanced], Hash if options[:advanced]
|
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
|
+
def advanced_input
|
101
|
+
advanced[:input] ||= {}
|
102
|
+
end
|
103
|
+
|
104
|
+
def advanced_output
|
105
|
+
advanced[:output] ||= {}
|
106
|
+
end
|
107
|
+
|
108
|
+
def default_advanced_input
|
109
|
+
{
|
110
|
+
pix_fmt: OS.mac? ? DEFAULT_MAC_INPUT_PIX_FMT : nil
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
def default_advanced_output
|
115
|
+
{
|
116
|
+
pix_fmt: DEFAULT_PIX_FMT,
|
117
|
+
framerate: advanced[:framerate] || DEFAULT_FPS
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
#
|
122
|
+
# Returns Array of required options as Symbols
|
123
|
+
#
|
124
|
+
def required_options
|
125
|
+
%i[input output]
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
# Returns given Hash parsed and ready for ffmpeg to receive.
|
130
|
+
#
|
131
|
+
def parse_advanced(opts)
|
132
|
+
# @todo Replace arr with opts.each_with_object([])
|
133
|
+
arr = []
|
134
|
+
# Do not parse input/output and log as they're placed separately in #parsed
|
135
|
+
opts.reject { |k, _| %i[input output log].include? k }
|
136
|
+
.each do |k, v|
|
137
|
+
arr.push "-#{k} #{v}" unless v.nil? # Ignore blank params
|
138
|
+
end
|
139
|
+
arr.join(' ') + ' '
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Returns input capture device based on user given value or the current OS.
|
144
|
+
#
|
145
|
+
def determine_capture_device
|
146
|
+
# User given capture device or format
|
147
|
+
# @see https://www.ffmpeg.org/ffmpeg.html#Main-options
|
148
|
+
return advanced[:f] if advanced[:f]
|
149
|
+
|
150
|
+
return advanced[:fmt] if advanced[:fmt]
|
151
|
+
|
152
|
+
os_specific_capture_device
|
153
|
+
end
|
154
|
+
|
155
|
+
#
|
156
|
+
# Returns input capture device for current OS.
|
157
|
+
#
|
158
|
+
def os_specific_capture_device
|
159
|
+
return 'gdigrab' if OS.windows?
|
160
|
+
|
161
|
+
return 'x11grab' if OS.linux?
|
162
|
+
|
163
|
+
return 'avfoundation' if OS.mac?
|
164
|
+
|
165
|
+
raise NotImplementedError, 'Your OS is not supported.'
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -1,50 +1,53 @@
|
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
titles
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
#
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
+
# @return [Array]
|
14
|
+
def self.fetch(application)
|
15
|
+
ScreenRecorder.logger.debug "Retrieving available windows for: #{application}"
|
16
|
+
WindowGrabber.new.available_windows_for application
|
17
|
+
end
|
18
|
+
|
19
|
+
# @since 1.0.0-beta4
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
class WindowGrabber
|
23
|
+
#
|
24
|
+
# Returns a list of available window titles for the given application (process) name.
|
25
|
+
#
|
26
|
+
def available_windows_for(application)
|
27
|
+
raise NotImplementedError, 'Only Microsoft Windows (gdigrab) supports window capture.' unless OS.windows?
|
28
|
+
|
29
|
+
titles = `tasklist /v /fi "imagename eq #{application}.exe" /fo list | findstr Window`
|
30
|
+
.split("\n")
|
31
|
+
.map { |i| i.gsub(FILTERED_TITLES, '') }
|
32
|
+
.reject(&:empty?)
|
33
|
+
raise Errors::ApplicationNotFound, "No open windows found for: #{application}.exe" if titles.empty?
|
34
|
+
|
35
|
+
warn_on_mismatch(titles, application)
|
36
|
+
titles
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
#
|
42
|
+
# Prints a warning if the retrieved list of window titles does no include
|
43
|
+
# the given application process name, which applications commonly do.
|
44
|
+
#
|
45
|
+
def warn_on_mismatch(titles, application)
|
46
|
+
return if titles.map(&:downcase).join(',').include? application.to_s
|
47
|
+
|
48
|
+
ScreenRecorder.logger.warn "Process name and window title(s) do not match: #{titles}"
|
49
|
+
ScreenRecorder.logger.warn 'Please manually provide the displayed window title.'
|
50
|
+
end
|
51
|
+
end # class WindowGrabber
|
52
|
+
end # module Windows
|
50
53
|
end # module FFMPEG
|