Skip to content

Commit 7ebe663

Browse files
committed
Finish fixing ruby 1.8.7 regressions. Works on 10.8 and 10.7.
1 parent 54af292 commit 7ebe663

File tree

3 files changed

+657
-0
lines changed

3 files changed

+657
-0
lines changed

lib/osx_ruby_ld_helpers.rb

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
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 "===============\nVideo Devices:\n===============\n"
231+
print_devices(devices)
232+
puts "\nAvailable video compression types:\n\n"
233+
print_compressions(vid_type)
234+
end
235+
puts "\n===============\nAudio Devices:\n===============\n"
236+
print_devices(audio_devices)
237+
puts "\nAvailable 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

Comments
 (0)