screen-recorder 1.1.0 → 1.2.0
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.
- 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
|