Previously on my blog: In my TiVo2Podcast stuff I automated the process of putting chapters around commercials, but had to call out to a small C++ app I wrote to put the chapters in using libmp4v2.
A few weeks ago I was looking at some ruby gems for a project I was working on and stumbled across ffi, a foreign function interface gem for ruby, or as its docs put it: “a ruby extension for programmatically loading dynamic libraries, binding functions within them, and calling those functions from Ruby code.” As long as you know the function signatures that you need, its pretty trivial to make the calls from Ruby. You do need to be aware of memory management stuff sometimes, but overall its pretty easy, especially for basic use. If you’re only going to be working in Ruby and need access to a C library, this is much easier than mucking with swig, that’s for sure.
The mind-blowing part for me is that the authors of the gem have made it smart enough to know what flavor of ruby vm and platform the code is running in and it does the right thing, no matter if its JRuby or on Windows or whatever. While I haven’t had a chance to use it yet, I suspect this property will be useful with JRuby at work in the future.
As a quick example, to add chapters to an mp4/mv4 file, I need 5 functions: MP4Modify, MP4AddChapterTextTrack, MP4AddChapter, MP4Close, and MP4Optimize. All the functions except MP4Close have default arguments that the C preprocessor puts in place, so we’ll need to recreate that in Ruby. For those functions, I bind them in ruby as c_function_name, and then wrap a call to that function in a normal ruby method with the defaults supplied. The subset of libmp4v2 then looks like this in ruby:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
module Mp4v2 extend FFI::Library # Behind the scenes in C land this is using dlopen, so library path # semantics from there will be followed ffi_lib ['libmp4v2'] typedef :pointer, :MP4FileHandle typedef :uint32, :MP4TrackId typedef :uint64, :MP4Duration MP4_INVALID_FILE_HANDLE = nil attach_function(:c_mp4_modify, :MP4Modify, [:string, :uint32, :uint32], :MP4FileHandle) def Mp4v2.mp4_modify(filename, verbosity = 0) # Per mp4v2 documentation, flags is currently ignored, so we won't # even expose that to ruby and just pass it as 0 all the time. c_mp4_modify(filename, verbosity, 0) end attach_function(:c_mp4_optimize, :MP4Optimize, [:string, :string, :uint32], :bool) def Mp4v2.mp4_optimize(filename, new_filename = nil, verbosity = 0) c_mp4_optimize(filename, new_filename, verbosity) end attach_function(:c_mp4_add_chapter_text_track, :MP4AddChapterTextTrack, [:MP4FileHandle, :MP4TrackId, :uint32], :MP4TrackId) def Mp4v2.mp4_add_chapter_text_track(h_file, ref_track_id, timescale = 0) c_mp4_add_chapter_text_track(h_file, ref_track_id, timescale) end attach_function(:c_mp4_add_chapter, :MP4AddChapter, [:MP4FileHandle, :MP4TrackId, :MP4Duration, :string], :void) def Mp4v2.mp4_add_chapter(h_file, chapter_track_id, chapter_duration, chapter_title = nil) c_mp4_add_chapter(h_file, chapter_track_id, chapter_duration, chapter_title) end attach_function :mp4_close, :MP4Close, [:MP4FileHandle], :void end
Then to use the functions, it’s like using any other methods/functions in ruby. As a quick and dirty example, here’s adding two chapters to a file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
FILENAME = '/Path/to/some/file.m4v' m4vfile = Mp4v2::mp4_modify(FILENAME) chapter_track = Mp4v2::mp4_add_chapter_text_track(m4vfile, 1, 1) Mp4v2::mp4_add_chapter(m4vfile, chapter_track, 10) Mp4v2::mp4_add_chapter(m4vfile, chapter_track, 10) Mp4v2::mp4_close(m4vfile) if Mp4v2::mp4_optimize(FILENAME) puts "SUCCESS" else puts "Boo!" end
Now that I have this working, I can do all the work in ruby, avoiding a system() call to launch a C++ program, which will simplify install for anyone in the future.