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.
@@ -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
- 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
+ # @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
- 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
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