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