Media duration in Python on Mac OS X
We’re working on a podcast server and building the upload application using Django. Towards the bottom of my task list on this project is “Populate duration on upload for appropriate media”. That is, find a way to get the duration of each media file from within Python. It’s a step I’ve been dreading, because it looked like not only was it going to require some complicated libraries, it looked like it was going to require complicated libraries per media type: one for mp3, one for mpeg movies, etc.
The server we’re running this on is Mac OS X Leopard. We chose not to use Podcast Producer for it because we need anyone, regardless of platform, to be able to upload files. But I was wondering today if Podcast Producer’s workflows might be able to automatically optimize the uploaded media.
As I browsed through the Podcast Producer Workflow Tutorial, I discovered that the workflows are just Ruby scripts. And then I saw this snippet in a script that joined two media files:
- first_input_movie_time_range = OSX::QTMakeTimeRange(zero_time, first_input_movie.duration)
That looked a whole lot like a command line script to get the duration of a media file. I quickly used that as an example to whip up a Ruby script to get the duration of a media file.
[toggle code]
- #!/usr/bin/ruby
- require 'osx/cocoa'
- OSX.require_framework 'QTKit'
-
if ARGV.size != 1
- $stderr.puts "duration filename"
- exit(-1)
- end
- filename = ARGV[0]
- media, error = OSX::QTMovie.movieWithFile_error(filename)
-
if error != nil or media == nil:
- $stderr.puts "Could not load media file"
- exit(1)
- end
- duration = media.duration.timeValue/media.duration.timeScale
- $stderr.puts filename, media.duration.timeValue, media.duration.timeScale
- $stderr.puts "Duration: ", duration
Durations from QTMovie come with a time scale and a time value. The time value needs to be divided by the scale to provide the duration of that media file in seconds.
This Ruby script uses the Ruby/Cocoa bridge. But there’s also a Python/Cocoa bridge, PyObjC, included with Mac OS X. The next step was to build the same thing in Python:
[toggle code]
- #!/usr/bin/python
- from QTKit import QTMovie
- import sys
- from optparse import OptionParser
- parser = OptionParser()
- (options, args) = parser.parse_args()
-
if len(args) != 1:
- print "duration filename"
- sys.exit(0)
- filename = args[0]
- attributes = {'QTMovieFileNameAttribute': filename}
- movie, error = QTMovie.movieWithAttributes_error_(attributes, None)
-
if error or not movie:
- print "Problem with movie", filename, ":", error.description()
- sys.exit(0)
- duration = movie.duration().timeValue/movie.duration().timeScale
- print filename, "is", duration, "seconds long"
This script will provide the duration of mp3 files, QuickTime files, m4a files, mp4 files, and mpeg files, any file that QuickTime can open. The only file I ended up having trouble with was swf files, and for our purposes we can live without giving them a duration.
No Matching Architecture
So that is an example of how cool it is, sometimes, to work on the OS X command line: easy integration with multimedia from Python and Ruby scripts. I almost never give programming articles headline status on Mimsy, but this was cool enough that I considered it. It opens up an amazing amount of functionality to simple command line scripts. Some people get a tingle talking to politicians; I get a tingle typing “import WebKit”. Here, for example, is a command line script to convert the first page of a web site to a PDF file:
[toggle code]
- #!/usr/bin/python
- import WebKit, Foundation, AppKit
- from optparse import OptionParser
- import os.path
-
class appDelegate(Foundation.NSObject):
-
def applicationDidFinishLaunching_(self, notification):
- webview = notification.object().windows()[0].contentView()
- webview.frameLoadDelegate().loadURL(webview)
-
def applicationDidFinishLaunching_(self, notification):
-
class webPrinter(Foundation.NSObject, WebKit.protocols.WebFrameLoadDelegate):
-
def loadURL(self, webview):
- page = Foundation.NSURL.URLWithString_(url)
- pageRequest = Foundation.NSURLRequest.requestWithURL_(page)
- webview.mainFrame().loadRequest_(pageRequest)
-
def webView_didFinishLoadForFrame_(self, webView, frame):
- printAttributes = AppKit.NSPrintInfo.sharedPrintInfo().dictionary()
- printAttributes[AppKit.NSPrintSavePath] = filePath
- printAttributes[AppKit.NSPrintJobDisposition] = AppKit.NSPrintSaveJob
- printInfo = AppKit.NSPrintInfo.alloc().initWithDictionary_(printAttributes)
- #scale the page horizontally to fit on one page, width-wise
- printInfo.setHorizontalPagination_(AppKit.NSFitPagination)
- preferences = webView.preferences()
-
if (options.screen):
- preferences.setShouldPrintBackgrounds_(True)
- webView.setMediaStyle_("screen")
-
else:
- #shouldPrintBackgrounds seems to stick
- preferences.setShouldPrintBackgrounds_(False)
- #pdf = frame.frameView().printOperationWithPrintInfo_(printInfo)
- pdf = AppKit.NSPrintOperation.printOperationWithView_printInfo_(webView, printInfo)
- pdf.setJobTitle_(webView.mainFrameTitle())
- pdf.setShowsPrintPanel_(False)
- pdf.setShowsProgressPanel_(False)
- pdf.runOperation()
- AppKit.NSApplication.sharedApplication().terminate_(None)
-
def loadURL(self, webview):
- parser = OptionParser()
- parser.add_option("-s", "--screen", help="print as it appears on the screen", action="store_true", dest="screen")
- (options, args) = parser.parse_args()
-
if len(args) != 2:
- print "Usage: webToPDF URL filename"
-
else:
- url = args[0]
- filePath = args[1]
- #NSPrintOperation requires a full path or it will ignore NSPrintSaveJob and print instead
- filePath = os.path.abspath(filePath)
- #this looks a lot like inches times 100
- viewRect = Foundation.NSMakeRect(0, 0, 850, 1100)
- app = AppKit.NSApplication.sharedApplication()
- delegate = appDelegate.alloc().init()
- AppKit.NSApp().setDelegate_(delegate)
- window = AppKit.NSWindow.alloc()
- window.initWithContentRect_styleMask_backing_defer_(viewRect, 0, AppKit.NSBackingStoreBuffered, False)
- view = WebKit.WebView.alloc()
- view.initWithFrame_(viewRect)
- view.mainFrame().frameView().setAllowsScrolling_(False)
- window.setContentView_(view)
- loadDelegate = webPrinter.alloc().init()
- view.setFrameLoadDelegate_(loadDelegate)
- app.run()
Unfortunately, the coolness is mitigated by the inability to use either of these scripts in a server context. In the above example, it won’t run under most servers, because the windowing portions of it requires an active login. And the duration script runs into problems on Intel servers because Apache is 64-bit but PyObjC is not. OS X won’t load PyObjC into Apache because of that mismatch. If I try to “import QTKit” into our Django application, I get:
Could not import files.views. Error was: dlopen(/System/Library/Frameworks/Python.framework/Versions/2.5/Extras/lib/python/PyObjC/objc/_objc.so, 2): no suitable image found. Did find: /System/Library/Frameworks/Python.framework/Versions/2.5/Extras/lib/python/PyObjC/objc/_objc.so: no matching architecture in universal wrapper
Fear of that error was the main reason I dreaded having to install a bunch of libraries in order to get media file durations. There’s pretty much no way around this without seriously hacking either the default PyObjC installation or the default Apache installation. Fortunately, for this purpose I only need to get the duration when someone uploads a large media file. Another second or two from shelling out after an upload won’t matter.
[toggle code]
- import datetime, subprocess
- …
-
class Upload(models.Model):
- file = models.FileField(upload_to=fileUploadPath, validator_list=[fileValidator])
- …
-
def save(self):
- super(Upload, self).save()
-
if self.duration == None:
-
if self.filetype.extension in ['mp3', 'mov', 'm4a', 'mp4', 'mpg']:
- cmd = "/Users/holden/bin/duration"
- pipe = subprocess.Popen([cmd, self.file.path], stdout=subprocess.PIPE)
- duration = pipe.stdout.read()
- pipe.stdout.close
- duration = int(duration)
-
if duration:
- duration = datetime.timedelta(seconds=duration)
- self.duration = str(duration)
- self.save()
-
if self.filetype.extension in ['mp3', 'mov', 'm4a', 'mp4', 'mpg']:
In the “duration” script, change the “print” line to simply “print duration”, so that it only outputs the number of seconds. Also, in the “fileUploadPath” function that I give upload_to, I have it set instance.duration to None so that I only grab the duration when a new file has been uploaded.
The next version of PyObjC is 64-bit compatible, so if we’re lucky shelling out won’t be necessary in a future update of Leopard server.
- February 8, 2011: QTKit duration in Snow Leopard Server
-
In Snow Leopard, the duration method returns a three-value tuple instead of whatever it was returning in Leopard. Instead of named values, you’ll need to change the last two lines (of the Python script) to:
[toggle code]
- timevalue, timescale, unknownInt = movie.duration()
- duration = timevalue/timescale
- print filename, "is", duration, "seconds long"
If you’re running it in a terminal on your workstation while you have a GUI session going, everything is fine.
If you’re running it on a server, you will then see an error that looks like this:
[toggle code]
- $ bin/duration octopus.mp4
- Tue Feb 8 09:07:38 www.example.com QTKitServer[20450] <Error>: kCGErrorFailure: Set a breakpoint @ CGErrorBreakpoint() to catch errors as they are logged.
- _RegisterApplication(), FAILED TO establish the default connection to the WindowServer, _CGSDefaultConnection() is NULL.
- octopus.mp4 is 13 seconds long
Look closely, and you can see that despite complaining about the lack of a WindowServer, the script did work. Fortunately, the warnings go to stderr, so if you’re using this in a script and parsing the results, you only get the printed duration.
There still appears to be an issue importing QTMovie into models.py on Snow Leopard when running in a server context, although I’m not sure what it is—when I added “from QTKit import QTMovie” to the top of models.py, the server never completed its response, which meant that I never received an error.
Finally, I don’t know what the third item in the tuple is, other than being an int and always zero. I thought at first it might be an error response like in QTMovie.movieWithAttributes_error_(), but that value is None when there’s no error, not zero, so who knows. The QTKit documentation calls it “flags”, but doesn’t explain what the flags are.
QTTime. Defines the value and time scale of a time.
[toggle code]
-
typedef struct {
- long long timeValue;
- long timeScale;
- long flags;
- } QTTime;
QTTime is a simple data structure that consists of three fields. In this case, the timeScale field is the number of units per second you work with when dealing with time. The timeValue field is the number of those units in duration.
To be safe, you may want to check the value and warn if it is ever not zero.
- Podcast Producer Workflow Tutorial
- “Podcast Producer Workflow Tutorial takes you step by step through the process of developing a workflow incrementally.”
- Creating iPhone-compatible movies with PyObjC and QTKit
- “As part of some general messing around with PyObjC and QTKit, I wrote a short script for converting a movie (anything readable by QuickTime) into an iPhone-compatible format.”
- PyObjC
- “The PyObjC project aims to provide a bridge between the Python and Objective-C programming languages. The bridge is intended to be fully bidirectional, allowing the Python programmer to take full advantage of the power provided by various Objective-C based toolkits and the Objective-C programmer transparent access to Python based functionality.”
- webkit2png
- “webkit2png is a command line tool that creates png screenshots of webpages.”
- WebKit: Simple Browsing
- “By writing just a few lines of code using the Web Kit, you can embed web content in your application and enable your users to navigate the web.”
- Cocoa Dev Central: Create a PDF
- “In this tutorial we'll look at how to add PDF exportation to an application. It is an easy to add feature that can add a lot of functionality to your program. We'll start off by briefly looking at the commonly shown export method and then implement a method that will export multi-page PDFs.”
- WebView Class Reference
- “WebView is the core view class in the Web Kit Framework that manages interactions between WebFrame and WebFrameView classes. To embed web content in your application, you just create a WebView object, attach it to a window, and send a loadRequest: message to its main frame.”
- NSPrintOperation Class Reference
- “An NSPrintOperation object controls operations that generate Encapsulated PostScript (EPS) code, Portable Document Format (PDF) code, or print jobs. An NSPrintOperation object works in conjunction with two other objects: an NSPrintInfo object, which specifies how the code should be generated, and an NSView object, which generates the actual code.”
More macOS tricks
- 42 Astoundingly Useful Scripts and Automations for the Macintosh
- MacOS uses Perl, Python, AppleScript, and Automator and you can write scripts in all of these. Build a talking alarm. Roll dice. Preflight your social media comments. Play music and create ASCII art. Get your retro on and bring your Macintosh into the world of tomorrow with 42 Astoundingly Useful Scripts and Automations for the Macintosh!
- Save clipboard text to the current folder
- Use the Finder toolbar to save text on the current clipboard directly to a file in the folder that Finder window is displaying.
- Avoiding lockFocus when drawing images in Swift on macOS
- Apple’s recommendation is to avoid lockFocus if you’re not creating images directly for the screen. Here are some examples from my own Swift scripts. You can use this to draw text into an image, and to resize images.
- What app keeps stealing focus?
- I’ve been having a problem on Mac OS X with something stealing focus. Here’s how to at least find out what that something is.
- Enable AirPrint for all connected Mac printers
- I have an iPad and an old workhorse of a printer, an HP 1012 LaserJet, connected to my iMac. I almost never need to print from the iPad, but when I do, handyPrint works.
- 14 more pages with the topic macOS tricks, and other related pages
More Python
- Quick-and-dirty old-school island script
- Here’s a Python-based island generator using the tables from the Judges Guild Island Book 1.
- Astounding Scripts on Monterey
- Monterey removes Python 2, which means that you’ll need to replace it if you’re still using any Python 2 scripts; there’s also a minor change with Layer Windows and GraphicConverter.
- Goodreads: What books did I read last week and last month?
- I occasionally want to look in Goodreads for what I read last month or last week, and that currently means sorting by date read and counting down to the beginning and end of the period in question. This Python script will do that search on an exported Goodreads csv file.
- Test classes and objects in python
- One of the advantages of object-oriented programming is that objects can masquerade as each other.
- Timeout class with retry in Python
- In Paramiko’s ssh client, timeouts don’t seem to work; a signal can handle this—and then can also perform a retry.
- 30 more pages with the topic Python, and other related pages