1
+ module OSXRubyDLHelpers
2
+ def osx_ruby_dl_header
3
+ <<-EOS
4
+ require 'dl'
5
+ require 'dl/import'
6
+
7
+ #### Patches to DL (for compatibility between 1.8->1.9)
8
+
9
+ Importer = if defined?(DL::Importer) then DL::Importer else DL::Importable end
10
+
11
+ def ruby_1_9_or_higher?
12
+ RUBY_VERSION.to_f >= 1.9
13
+ end
14
+
15
+ def malloc(size)
16
+ if ruby_1_9_or_higher?
17
+ DL::CPtr.malloc(size)
18
+ else
19
+ DL::malloc(size)
20
+ end
21
+ end
22
+
23
+ # the old Ruby Importer defaults methods to downcase every import
24
+ # This is annoying, so we'll patch with method_missing
25
+ if not ruby_1_9_or_higher?
26
+ module DL
27
+ module Importable
28
+ def method_missing(meth, *args, &block)
29
+ str = meth.to_s
30
+ lower = str[0,1].downcase + str[1..-1]
31
+ if self.respond_to? lower
32
+ self.send lower, *args
33
+ else
34
+ super
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ EOS
41
+ end
42
+
43
+ def osx_capture_media ( opts )
44
+ capture_code = <<-EOS
45
+ #{ osx_ruby_dl_header }
46
+
47
+ options = {
48
+ :action => '#{ opts [ :action ] } ', # or list|snapshot|record
49
+ :snap_filetype => '#{ opts [ :snap_filetype ] } ', # jpg|png|gif|tiff|bmp
50
+ :audio_enabled => #{ opts [ :audio_enabled ] } ,
51
+ :video_enabled => #{ opts [ :video_enabled ] } ,
52
+ :num_chunks => #{ opts [ :num_chunks ] } , # wachawa!
53
+ :chunk_len => #{ opts [ :chunk_len ] } , # save chunks every 5 seconds
54
+ :video_device => #{ opts [ :video_device ] } , # automatic
55
+ :audio_device => #{ opts [ :audio_device ] } ,
56
+ :snap_jpg_compression => #{ opts [ :snap_jpg_compression ] } , # compression ratio (between 0 & 1), JPG ONLY
57
+ :video_compression => '#{ opts [ :video_compression ] } ',
58
+ :audio_compression => '#{ opts [ :audio_compression ] } ',
59
+ :record_file => '#{ opts [ :record_file ] } ',
60
+ :snap_file => '#{ opts [ :snap_file ] } '
61
+ }
62
+
63
+ RUN_LOOP_STEP = 0.1 # "tick" duration for spinning NSRunLoop
64
+
65
+ # NSTIFFFileType 0
66
+ # NSBMPFileType 1
67
+ # NSGIFFileType 2
68
+ # NSJPEGFileType 3
69
+ # NSPNGFileType 4
70
+ SNAP_FILETYPES = %w(tiff bmp gif jpg png)
71
+
72
+ snap_filetype_index = SNAP_FILETYPES.index(options[:snap_filetype].to_s)
73
+
74
+ require 'fileutils'
75
+ FileUtils.mkdir_p File.dirname(options[:record_file])
76
+ FileUtils.mkdir_p File.dirname(options[:snap_file])
77
+
78
+ #### Helper methods for objc message passing
79
+
80
+ if not ruby_1_9_or_higher?
81
+ # ruby < 1.9 freaks when you send int -> void* or flout -> void*
82
+ # so we have to reload the lib into separate modules with different
83
+ # exported typedefs, and patch objc_call to do our own typechecking.
84
+ # this can probably be done better.
85
+ module LibCWithInt
86
+ extend Importer
87
+ dlload 'libSystem.B.dylib'
88
+ extern 'void *sel_getUid(void*)'
89
+ extern 'void *objc_msgSend(void *, void *, int, int)'
90
+ end
91
+ module LibCWithFloat
92
+ extend Importer
93
+ dlload 'libSystem.B.dylib'
94
+ extern 'void *sel_getUid(void*)'
95
+ extern 'void *objc_msgSend(void *, void *, double, double)'
96
+ end
97
+ module LibCWithVoidPtrInt
98
+ extend Importer
99
+ dlload 'libSystem.B.dylib'
100
+ extern 'void *sel_getUid(void*)'
101
+ extern 'void *objc_msgSend(void *, void *, void*, int)'
102
+ end
103
+ module LibCWithIntVoidPtr
104
+ extend Importer
105
+ dlload 'libSystem.B.dylib'
106
+ extern 'void *sel_getUid(void*)'
107
+ extern 'void *objc_msgSend(void *, void *, int, void*)'
108
+ end
109
+ end
110
+
111
+
112
+ def objc_call(instance, method, arg=nil, arg2=nil)
113
+ # ruby < 1.9 freaks when you send int -> void* or flout -> void*
114
+ # so we have to reload the lib into a separate with different exported typedefs,
115
+ # and call
116
+ if not ruby_1_9_or_higher? and arg.kind_of?(Integer)
117
+ if not arg2.kind_of?(Integer) and not arg2.nil?
118
+ LibCWithIntVoidPtr.objc_msgSend(instance, LibCWithIntVoidPtr.sel_getUid(method), arg||0, arg2)
119
+ else
120
+ LibCWithInt.objc_msgSend(instance, LibCWithInt.sel_getUid(method), arg||0, arg2||0)
121
+ end
122
+ elsif not ruby_1_9_or_higher? and arg2.kind_of?(Integer)
123
+ LibCWithVoidPtrInt.objc_msgSend(instance, LibCWithVoidPtrInt.sel_getUid(method), arg||0, arg2)
124
+ elsif not ruby_1_9_or_higher? and arg.kind_of?(Float)
125
+ LibCWithFloat.objc_msgSend(instance, LibCWithFloat.sel_getUid(method), arg||0.0, arg2||0.0)
126
+ else
127
+ QTKit.objc_msgSend(instance, QTKit.sel_getUid(method), arg, arg2)
128
+ end
129
+ end
130
+
131
+ def objc_call_class(klass, method, arg=nil, arg2=nil)
132
+ objc_call(QTKit.objc_getClass(klass), QTKit.sel_getUid(method), arg, arg2)
133
+ end
134
+
135
+ def nsstring(str)
136
+ objc_call(objc_call(objc_call_class(
137
+ 'NSString', 'alloc'),
138
+ 'initWithCString:', str),
139
+ 'autorelease')
140
+ end
141
+
142
+
143
+ #### External dynamically linked code
144
+
145
+ VID_TYPE = 'vide'
146
+ MUX_TYPE = 'muxx'
147
+ AUD_TYPE = 'soun'
148
+
149
+ module QTKit
150
+ extend Importer
151
+ dlload 'QTKit.framework/QTKit'
152
+ extern 'void *objc_msgSend(void *, void *, void *, void*)'
153
+ extern 'void *sel_getUid(void*)'
154
+ extern 'void *objc_getClass(void *)'
155
+ end
156
+
157
+ #### Actual Webcam code
158
+ autorelease_pool = objc_call_class('NSAutoreleasePool', 'new')
159
+
160
+ vid_type = nsstring(VID_TYPE)
161
+ mux_type = nsstring(MUX_TYPE)
162
+ aud_type = nsstring(AUD_TYPE)
163
+
164
+ devices_ref = objc_call_class('QTCaptureDevice', 'inputDevices')
165
+ device_count = objc_call(devices_ref, 'count').to_i
166
+ if device_count.zero? and not options[:actions] =~ /list/i
167
+ raise "Invalid device. Check devices with `set ACTION LIST`. Exiting."
168
+ exit
169
+ end
170
+
171
+ device_enum = objc_call(devices_ref, 'objectEnumerator')
172
+ devices = (0...device_count).
173
+ map { objc_call(device_enum, 'nextObject') }.
174
+ select do |device|
175
+ vid = objc_call(device, 'hasMediaType:', vid_type).to_i > 0
176
+ mux = objc_call(device, 'hasMediaType:', mux_type).to_i > 0
177
+ vid or mux
178
+ end
179
+
180
+ device_enum = objc_call(devices_ref, 'objectEnumerator')
181
+ audio_devices = (0...device_count).
182
+ map { objc_call(device_enum, 'nextObject') }.
183
+ select { |d| objc_call(d, 'hasMediaType:', aud_type).to_i > 0 }
184
+
185
+ def device_names(devices)
186
+ devices.
187
+ map { |device| objc_call(device, 'localizedDisplayName') }.
188
+ map { |name| objc_call(name, 'UTF8String') }.
189
+ map(&:to_s)
190
+ end
191
+
192
+ def device_stati(devices)
193
+ devices.
194
+ map { |d| objc_call(d, 'isInUseByAnotherApplication').to_i > 0 }.
195
+ map { |b| if b then 'BUSY' else 'AVAIL' end }
196
+ end
197
+
198
+ def print_devices(devices)
199
+ device_names(devices).zip(device_stati(devices)).each_with_index do |d, i|
200
+ puts "\# {i}. \# {d[0]} [\# {d[1]}]"
201
+ end
202
+ end
203
+
204
+ def print_compressions(type)
205
+ compressions = objc_call_class('QTCompressionOptions',
206
+ 'compressionOptionsIdentifiersForMediaType:', type)
207
+ count = objc_call(compressions, 'count').to_i
208
+ if count.zero?
209
+ puts "No supported compression types found."
210
+ else
211
+ comp_enum = objc_call(compressions, 'objectEnumerator')
212
+ puts((0...count).
213
+ map { objc_call(comp_enum, 'nextObject') }.
214
+ map { |c| objc_call(c, 'UTF8String').to_s }.
215
+ join("\n ")
216
+ )
217
+ end
218
+ end
219
+
220
+ def use_audio?(options)
221
+ options[:audio_enabled] and options[:action].to_s == 'record'
222
+ end
223
+
224
+ def use_video?(options)
225
+ (options[:video_enabled] and options[:action].to_s == 'record') or options[:action].to_s == 'snapshot'
226
+ end
227
+
228
+ if options[:action].to_s == 'list'
229
+ if options[:video_enabled]
230
+ puts "===============\n Video Devices:\n ===============\n "
231
+ print_devices(devices)
232
+ puts "\n Available video compression types:\n \n "
233
+ print_compressions(vid_type)
234
+ end
235
+ puts "\n ===============\n Audio Devices:\n ===============\n "
236
+ print_devices(audio_devices)
237
+ puts "\n Available audio compression types:\n \n "
238
+ print_compressions(aud_type)
239
+ exit
240
+ end
241
+
242
+ # Create a session to add I/O to
243
+ session = objc_call_class('QTCaptureSession', 'new')
244
+
245
+ # open the AV devices
246
+ if use_video?(options)
247
+ video_device = devices[options[:video_device]]
248
+ if not objc_call(video_device, 'open:', nil).to_i > 0
249
+ raise 'Failed to open video device'
250
+ end
251
+ input = objc_call_class('QTCaptureDeviceInput', 'alloc')
252
+ input = objc_call(input, 'initWithDevice:', video_device)
253
+ objc_call(session, 'addInput:error:', input, nil)
254
+ end
255
+
256
+ if use_audio?(options)
257
+ # open the audio device
258
+ audio_device = audio_devices[options[:audio_device]]
259
+ if not objc_call(audio_device, 'open:', nil).to_i > 0
260
+ raise 'Failed to open audio device'
261
+ end
262
+ input = objc_call_class('QTCaptureDeviceInput', 'alloc')
263
+ input = objc_call(input, 'initWithDevice:', audio_device)
264
+ objc_call(session, 'addInput:error:', input, nil)
265
+ end
266
+
267
+ # initialize file output
268
+ record_file = options[:record_file]
269
+ output = objc_call_class('QTCaptureMovieFileOutput', 'new')
270
+ file_url = objc_call_class('NSURL', 'fileURLWithPath:', nsstring(record_file))
271
+ objc_call(output, 'recordToOutputFileURL:', file_url)
272
+ objc_call(session, 'addOutput:error:', output, nil)
273
+
274
+ # set up video/audio compression options
275
+ connection = nil
276
+ connection_enum = objc_call(objc_call(output, 'connections'), 'objectEnumerator')
277
+
278
+ while (connection = objc_call(connection_enum, 'nextObject')).to_i > 0
279
+ media_type = objc_call(connection, 'mediaType')
280
+
281
+ compress_opts = if objc_call(media_type, 'isEqualToString:', vid_type).to_i > 0 ||
282
+ objc_call(media_type, 'isEqualToString:', mux_type).to_i > 0
283
+ objc_call_class('QTCompressionOptions', 'compressionOptionsWithIdentifier:',
284
+ nsstring(options[:video_compression]))
285
+ elsif use_audio?(options) and objc_call(media_type, 'isEqualToString:', aud_type).to_i > 0
286
+ objc_call_class('QTCompressionOptions', 'compressionOptionsWithIdentifier:',
287
+ nsstring(options[:audio_compression]))
288
+ end
289
+
290
+ unless compress_opts.to_i.zero?
291
+ objc_call(output, 'setCompressionOptions:forConnection:', compress_opts, connection)
292
+ end
293
+ end
294
+
295
+ # start capturing from the webcam
296
+ objc_call(session, 'startRunning')
297
+
298
+ # we use NSRunLoop, which allows QTKit to spin its thread? somehow it is needed.
299
+ run_loop = objc_call_class('NSRunLoop', 'currentRunLoop')
300
+
301
+ # wait until at least one frame has been captured
302
+ while objc_call(output, 'recordedFileSize').to_i < 1
303
+ time = objc_call(objc_call_class('NSDate', 'new'), 'autorelease')
304
+ objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
305
+ end
306
+
307
+ if options[:action] == 'record' # record in a loop for options[:record_len] seconds
308
+ curr_chunk = 0
309
+ last_roll = Time.now
310
+ # wait until at least one frame has been captured
311
+ while curr_chunk < options[:num_chunks]
312
+ time = objc_call(objc_call_class('NSDate', 'new'), 'autorelease')
313
+ objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
314
+
315
+ if Time.now - last_roll > options[:chunk_len].to_i # roll that movie file
316
+ base = File.basename(record_file, '.*') # returns it with no extension
317
+ num = ((base.match(/\\ d+$/)||['0'])[0].to_i+1).to_s
318
+ ext = File.extname(record_file) || 'o'
319
+ record_file = File.join(File.dirname(record_file), base+num+'.'+ext)
320
+
321
+ # redirect buffer output to new file path
322
+ file_url = objc_call_class('NSURL', 'fileURLWithPath:', nsstring(record_file))
323
+ objc_call(output, 'recordToOutputFileURL:', file_url)
324
+ # remember we hit a chunk
325
+ last_roll = Time.now
326
+ curr_chunk += 1
327
+ end
328
+ end
329
+ end
330
+
331
+ # stop recording and stop session
332
+ objc_call(output, 'recordToOutputFileURL:', nil)
333
+ objc_call(session, 'stopRunning')
334
+
335
+ # give QTKit some time to write to file
336
+ objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
337
+
338
+ if options[:action] == 'snapshot' # user wants a snapshot
339
+ # read captured movie file into QTKit
340
+ dict = objc_call_class('NSMutableDictionary', 'dictionary')
341
+ objc_call(dict, 'setObject:forKey:', nsstring('NSImage'), nsstring('QTMovieFrameImageType'))
342
+ # grab a frame image from the move
343
+ m = objc_call_class('QTMovie', 'movieWithFile:error:', nsstring(options[:record_file]), nil)
344
+ img = objc_call(m, 'currentFrameImage')
345
+ # set compression options
346
+ opts = objc_call_class('NSDictionary', 'dictionaryWithObject:forKey:',
347
+ objc_call_class('NSNumber', 'numberWithFloat:', options[:snap_jpg_compression]),
348
+ nsstring('NSImageCompressionFactor')
349
+ )
350
+ # convert to desired format
351
+ bitmap = objc_call(objc_call(img, 'representations'), 'objectAtIndex:', 0)
352
+ data = objc_call(bitmap, 'representationUsingType:properties:', snap_filetype_index, opts)
353
+ objc_call(data, 'writeToFile:atomically:', nsstring(options[:snap_file]), 0)
354
+
355
+ objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
356
+
357
+ # # delete the original movie file
358
+ File.delete(options[:record_file])
359
+ end
360
+
361
+ objc_call(autorelease_pool, 'drain')
362
+
363
+ EOS
364
+ if opts [ :action ] == 'record'
365
+ capture_code = %Q|
366
+ cpid = fork do
367
+ #{ capture_code }
368
+ end
369
+ Process.detach(cpid)
370
+ puts cpid
371
+ |
372
+ end
373
+ File . write ( '/Users/joe/Desktop/fl.rb' , capture_code )
374
+ capture_code
375
+ end
376
+ end
0 commit comments