Installing Voyage on new ALIX system

Back around 2005 I was still new to Linux. I had settled into running Debian on my desktop and I needed a new project. At the time I had a crappy DLink router / access point that would get “confused” quite consistently and had to be reset. After a roommate of mine moved out and left behind an old dell Pentium III I decided to replace the DLink. I scrapped together an extra Ethernet card and a Netgear 802.11b PCI card and started messing around. Surprisingly enough, I turned an old PIII desktop into a router / wireless access point.

I can’t remember how I ran across PCEngines but their WRAP single board computer seemed like a fun and significantly more efficient replacement for my PIII access point. Installing Debian on a CF card using debootstrap was pretty straight forward. My wrap system has been routing my network traffic for 3+ years now and has required minimal / no up-keep (except for fixing my own iptables mistakes). There’s even an article on the HowTo Forge now that you can follow step-by-step.

I was always concerned however that the number of disk writes of a general purpose Linux system (pretty much everything in /var) would eventually wear out the CF card. I suppose after 3 years of operation I can say this may not be as big an issue as I first thought. Still, after purchasing another board from PC Engines I decided to install Voyage, a Debian based distro aimed at CF based embedded systems like the ALIX2d3 I’m setting up to be a VPN end point:

alix2d3 single board computer

Installing Voyage is well documented so I won’t repeat it here. You can check out their site for the details. My general impression of Voyage so far is that it’s a bit out of date and that installing Debian directly is likely a better option. The current stable release of Voyage (version 5.2) is still based on Etch so the version of racoon is pretty old … oh yeah and I couldn’t it to boot with Grub but Lilo worked fine.

It’s not all bad though. Voyage has a really cool set-up for minimizing the number of disk writes: they symlink files that need to be writable to a tmpfs. Everything else is mounted read only. It’s also less than half the size of a minimal Debian install which in some circumstances may be important but since 2GB CF cards can be found for less than $20 this is a non-issue.

So after getting Voyage 5.2 up and running I’m going back to a minimal Lenny install using the Voyage kernel like I did on my older WRAP system … pretty much just like in the howto forge article. Maybe I’ll get fancy and mount directories under /var as a tmpfs to minimize disk writes, or even enable SELinux.

ThinkPad x61s UltraBay docking script

UPDATE: I’ve updated this script with a much better design because SELinux wouldn’t let me muck around with X’s tmp files. dock.sh.

Over a year ago I decided to pick up a new ThinkPad, this time the ultra portable x61s. My desktop was aging and I needed mobility more than anything else. I had always intended to get the X6 UltraBay so I could set it up on my desk but I never seemed to get around to it (probably on account of the phobia I developed while trying to get RedHat 9 to suspend and resume on my old T40).

Tonight, after getting my new (well new to me) UltraBay from ebay in the mail I took on setting up a script to run when docking and undocking the laptop. First off, I’m impressed with how well Linux works with the docking station “out of the box”. Even without udev rules to handle the dock/undock event the CD-RW/DVD combo drive hot plugs perfectly (I’m running a vanilla 2.6.31 kernel on Debian Lenny).

The udev rules and docking scripts for this set-up are well documented out there on the web but there were some details I had to assemble from a few different places. This post is mostly to collect the details in one place. If it’s useful to someone else out there even better.

First lets define what we’re trying to do: I’ve hooked up an external monitor to the UltraBase and I want to distribute my desktop across the external monitor and the LVDS display on the laptop when it’s docked. When it’s undocked the laptop should return to using only the LVDS display. We can script all that using a little bit of control logic and some very basic xrandr commands.

Let’s run through getting xrandr working first, then worry about when and how the script should be run. ThinkWiki has some great info for using xrandr to configure an external monitor. But when I ran some of these commands manually, nothing happened?

Turns out that when I first installed Debian on this laptop there wasn’t an external monitor attached (big surprise) so when X was configured it generated a configuration file that couldn’t handle the external monitor. This means xrandr could query properties from the connected monitor just fine, but any attempt to change the configuration did nothing. The command gave a successful status code but nothing happened.

The suggested fix is to attach the second monitor and reconfigure X. On Debian we’d expect this to be done through dpkg:

dpkg-reconfigure xserver-xorg

Which does nothing. Turns out you can tell X to generate a config file directly which does the trick this time:

sudo X -configure

I know nothing about X configuration files so I can’t say why this works but it does because now when we send X commands through xrandr it responds.

Start small by just turning on the external display:

xrandr --output LVDS --auto --output VGA --auto

Your displays may be named a bit differently which you can check using the query option (-q). The external monitor turned on but the screens overlapped in a funny way … progress.

The effect I want is to have my desktop extend across both screens so they’re side by side. xrandr does this without any hassle:

xrandr --output LVDS --mode 1024x768 --output VGA --mode 1024x768 --left-of LVDS

The command above makes the layout pretty obvious: both screens are 1024 by 768 and the VGA screen is positioned to the left of the built in LVDS. Some people want their external screen to have a higher resolution but it’s easy to change the configuration so I’m going for symmetry to start off. That and the larger the screens get the more RAM the video card borrows from the rest of the system.

This is the command we want to run when the laptop is docked, now the command when it’s undocked:

xrandr --output VGA --off

The two commands can be combined into a script which we can pass a parameter, maybe a 1 when the system is being docked, a 0 when it’s undocked. Throw in a case statement and then we can test it:

#!/bin/sh
case $1 in
    0)
        echo "undock" | logger -t $0
        xrandr --output VGA --off
        ;;
    1)
        echo "dock" | logger -t $0
        xrandr --output LVDS --mode 1024x768 --output VGA --mode 1024x768 --left-of LVDS
        ;;
    *)
        echo "unexpected input" | logger -t $0
        exit 1
        ;;
esac
exit 0

Make this file executable and put it in /etc/thinkpad, call it dock.sh

Now to get the system to run the script for us when the laptop is docked and undocked. Debian Lenny uses udev so this is as simple as adding the following in a file in /etc/udev/rules.d/

KERNEL=="dock.0", ATTR{docked}=="1", RUN+="/etc/thinkpad/dock.sh 1"
KERNEL=="dock.0", ATTR{docked}=="0", RUN+="/etc/thinkpad/dock.sh 0"

I named this file 55-thinkpad-local.rules based on someone’s reported success on the linux thinkpad mailing list. The order in which udev rules are run, why the order is important and how to write them is still a mystery to me and will remain that way for now.

Now we put the two together. The log messages that are sent to the syslog should be verified to be sure the script is running right because when the laptop is docked/undocked … the screen layout won’t change! Great right? We can get the error message by redirecting the output of the xrandr commands to syslog too like so:

xrandr --output LVDS --mode 1024x768 --output VGA --mode 1024x768 --left-of LVDS 2>&1 | logger -t $0

The error message tells us that the script “Can’t open display”. Wait, root (the script is run as roor) doesn’t have permission to open the display? This turns out to be some X magic that’s explained on the ThinkWiki Fn-F7 page. The important part is way down at the end of the script where root enumerates the X sessions and then one by one exports the server identifiers and the Xauthority cookie. After this root can change the display all it wants. We’ve gotta include this in the original script and while we’re at it throw in some extra stuff to make it pretty. The final script is here. Works pretty good for me, but YMMV.

Winter Project: Raleigh

Two weeks ago I took a few hours to start in on my “winter project”. In Syracuse it’s always good to have a project lined up for the winter months, when the passing of days can sometimes only be measured by the shift from a dark gray to a lighter shade and back (not like we’ve had much snow so far though).

In a previous post I put up some pictures of my BMX but I’m getting pretty old (one ankle surgery is enough) and as much as I love that bike I need something I can ride without being tempted to do tricks that inevitably cause swelling … I told you I’m getting old. Enter the winter project:

Raleigh Before

That’s right. A Raleigh road bike probably from back in the 80’s. I’m completely guessing at the age of the bike but I decided it’s gotta be pretty old when I tore off the tires I found it had cloth rim strips!

cloth rim-stip dating a bike is much like carbon dating a fossil, just not as accurate

It’s actually a pretty big frame. Standing over it I’m just clear of the top tube so it’s about 3 times the size of my BMX. It’s gona take some getting used to once it’s ridable … but let’s not get ahead of ourselves. I’ve still gotta clean close to 20 years of basement-induced corrosion off of the steel. It’s in pretty rough shape. From the amount of crap on the headset / neck / handlebars you get a feeling for how bad it is.

Neck Left
Neck Right

They almost look fuzzy in the photos with all of the crap that’s on them. There’s still a few spots that look really nice though. This is what’s gona get me through the job:

Headset Stamp

That’s pretty sharp. So what follows here are some “before” photos. I’ll post “after” photos as I progress through the clean up effort. I’ve already cleaned up one wheel so I’ll post photos once I’ve finished the second one. Brakes, derailers and anything with cables or bearings are a bit scary though.

Ok on to the pictures:
FrontChainWheel
FrontDerailerDown
CrankLeft
RearDerailer
FrontBrake
RearBreakRight
NeckFront
DownTubeStamp

mod_mono group initialization patch

About a month back I installed the latest mono release on on one of my servers. A friend of a friend needed a place to do some ASP.Net development. He’d previously had an account with a Windows ASP.Net web host but wasn’t happy with the support he was getting and was trying to cut costs (as everyone always is).

I had the space and the know how to get him up and running so I pointed him to the mono-project ASP.Net page and told him to be sure all of the .Net features he was using were supported by Mono. On my end I installed all of the necessary mono stuff from source which, on Debian 5.0 (Lenny) was very simple. I then set him up with an account just like everyone else that uses this system.

Typically I use Unix DAC Groups to separate users from eachother on the shared host. I create a group for each user (call it www-username) and add both the user and the apache user to this group. This way users can make files they wish to be served up by the web server readable by this group. Nothing fancy, standard DAC stuff.

Funny thing though: this set-up works for static web pages, php scripts etc but didn’t work for the mod-mono-server. The apache error logs showed a stack trace leading back to a permission denied exception on the directory serving up the test ASP web page I was using. This directory had the following permissions as showed by ls -l

drwxr-s--- 3 username    www-username 4096 2009-11-01 16:50 web_dir

The DAC permissions are set up to keep everyone out except the owner and the web server. The server is given read access through the shared group and I’ve also set the sticky bit on the directory to keep files created by the user readable by this group. This last bit isn’t relevant to the denial but it’s useful and apparent in the output above.

Time to debug! We know the apache user (www-data) is in this group and can access files owned by the shared group (www-username) but why not mono? First off the Apache2 server only starts the mono server, it doesn’t process the ASP.Net pages itself. Through the modules interface Apache2 forks off the mod-mono-server2 process which it then feeds requests for the ASP.Net pages. Looking at the output of ps we can check to see that the mod-mono-server2 process has the right effective user and group (which were both correct) but I don’t know of a tool to inspect the supplementary groups for a running process. So we turn to the mod_mono source code for details. Note: Wikipedia has a good explanation of what Unix supplementary groups are if you need more background.

I’m a pretty good C coder but I’ve never looked at the mod_mono code in my life and I know nothing about writing modules for Apache2. But to debug this all I needed was a basic understanding of Unix DAC users and groups. The first thing I looked for was the place in the code where the mono server is started: either a fork or an exec system call is where we should start. These calls are both made in a function called ‘fork_mod_mono_server’ in the mod_mono.c file … yeah that’s probably where we should start looking.

This function also makes calls to setuid and setgid which is the forked process dropping the root privileges of its parent and taking on the identity of a lesser privileged user and group (specified in the apache configuration). On Debian this is the www-data user and group. As I say above we’ve already verified that the uid and gid are being set properly, it’s the supplementary groups that seem to be missing. We can check to see what group membership the forked process has by calling getgroups. Add a few debug statements (ouput the group ids from getgroups to the apache log file), rebuild the module, install it and restart the server and we can see that our suspicions have been confirmed: the only group id returned is 0, the root group?

Yeah, that’s not right. After flipping through a few man pages it looks like the initgroups has the functionality we’re looking for. All we need to do now is pass it the group name for the apache group and it does all the hard work for us. A quick test shows that after rebuilding the module and restarting the server it now has the expected behavior: the mod-mono-server2 process is able to access files that are readable by this shared group!

Now that the problem’s been fixed we want to contribute the it back to the Mono project. I’ve never submitted a patch to a project as high profile as Mono before so I was very conscious about making this patch clean and easy. This wasn’t hard since it was only one system call 🙂 Still it’s important to use formatting and logging statements that are consistent with the rest of the code. After looking at the code around my small fix I added a debug statement to output some information about the group membership initialization and some error handling to output an error message should the system call fail.

After running this fix on two of my servers for a few days I sent the patch to the mono-devel mailing list. Kinda sucks that the mail archive thinks the attachment is a binary. ::shrug:: Either way I got an off-list reply the same day from Marek Harbersack saying that the patch had bee accepted and applied to the mod_mono svn tree! The patch is revision #145618 and they even gave me credit in the svn log!

Awesome.

New Macneil Cranks

My bike spent the better part of the summer down in the basement. I bought a pair of Demolition Medial cranks for it last summer and rode on them for a while but they were never right. The threads were machined a bit funny which made getting a puller on difficult. The face on the crank arm where the chain wheel attached was a bit crooked too. I didn’t notice the later defect till it had ruined a great HTP 25T chain wheel and broke a chain. Here’s the culprit:

DemolitionCrankarm

Either way I got pissed and took the whole bottom bracket apart then left it sitting on the stand for a few months. The summer was super busy so I kept putting it off but eventually I couldn’t take the sad sight of it any longer:

Bike with no Bearings

I picked up a set of Macneil Conjoined cranks with bearings to match the 19mm spindle. The Macneil hubs I’m running now are great so I figured I’d make a safe bet and give their cranks a go. Paid off so far:

Macneil Conjoined Crank Arm

I still can’t figure out why all of the threads are painted. I didn’t take a picture of the threads for the pedals but they were painted a few millimeters deep like the opening for the spindle. Likely it’s just cheaper to make them that way but it makes the first installation a pain.

So now that all the parts were together, next was pressing the bottom bracket. With the right tools this doesn’t take much effort. At one point I had made a cup press from a long threaded bolt with washers and nuts at each end. It did the trick but eventually I bought the Park Tools equivalent. It’s pretty much the same thing but with handles on each end coated in blue plastic with “Park Tools” stamped on it (I’m guessing that’s why they cost $60, not worth it!):

Cartridge bearings and spacer lined up on press.

This is the whole bottom bracket kit lined up on the cup press. Crappy thing about this press is that the handles can’t clear the frames chain stay completely so you end up with about 2 millimeters of bearing stuck out when the press can’t be turned any further:

Press Clearance
Not pressed completely

That’s why we keep a stack of extra washers around. Stack these on either end of the cup press between the plates and you’ll get enough clearance to press the bearings flush:

washers to the rescue
bearings pressed flush

Once the bearings are pressed it’s just a matter of putting the new spindle in and fitting the cranks on with a chain wheel. It’s always a pain to get the spacers right so that the cranks clear the chain stays and the chain line is straight. I’m pretty anal about this so I always end up taking the drive side crank arm off and putting it back on a few times. It’s a curse. In the end when it’s all back together it looks something like this:

CranksComplete

I’m pretty happy with the Conjoined cranks so far. I’ve been riding on them for a few weeks now and they’re as solid as any cranks I’ve had in the past. I wouldn’t consider this much of a stress test though since I pretty much just roll around and try to keep from hurting myself so YMMV.

I’m still pretty pissed about losing that 25T chain wheel caus of the Demolition cranks being crappy. I picked it up at an Easter Boarder shop out in Massachusetts. There’s this guy out there that machines them himself under the name “Home Town Products”. Last time I was out that way he said he wasn’t making them anymore so that’s a loss.

In the background of the first picture you can see a yellow road bike frame sitting on my basement floor. That’s a my winter project. It spent the past year+ sitting outside on a sort-of covered porch so it’s in need of some rehab. More on this later.

Extracting table schema from database

As part of rehabilitating an old project I’ve had to go through a fairly sizable MySQL database and extract the table structure. I’m doing this for knowledge capture. Currently the schema is only known to the internals of the software that operates on the database, there’s no documentation to speak of (oh what I’d do to get an ER diagram and some notes from the original developers).

In the scheme of things this is a pretty small database (well two of them actually), with only a few dozen tables. Doing the extraction by hand wouldn’t be overly time consuming but it would be overly mind numbing. And as the adage goes: “anything worth doing once is worth scripting”, so I wrote up a quick shell script that will dump the CREATE TABLE statements for each table in a set of databases.

#!/bin/sh
#  extract table schema from some set of databases
#
DB_USER=you
DB_PASSWD=yourpasswd
PARAMS="--skip-column-names -u ${DB_USER} -p${DB_PASSWD}"
DB_SELECTOR='^(db0|db1|db2)$'
TABLE_SELECTOR='.*'

echo "SHOW DATABASES;" 
    | mysql ${PARAMS} 
    | grep ${DB_SELECTOR} 
    | while read DB; do
    echo "CONNECT ${DB}; SHOW TABLES;" 
        | grep "${TABLE_SELECTOR}" 
        | mysql ${PARAMS} 
        | while read TABLE; do
        echo "CONNECT ${DB}; SHOW CREATE TABLE ${TABLE}" 
            | mysql ${PARAMS} 
            | sed -e  "s/\\n/\n/g;s/^${TABLE}[t]//;s/AUTO_INCREMENT=[0-9]*//
g" > ${DB}-${TABLE}.sql;
    done;
done;

I’m a big fan of piping things into while read VAR statements. I’m sure some hate it since it’s a bit hard to read / see where the loop begins but it seems elegant to me and it’s very useful.

The script is basically three sql commands:

  • The first gets the names of all databases the user has access to. This could feasibly be a large number of databases so we add a “selector” to give us a regex we can use to narrow this set down.
  • The second SQL command gets the tables from the selected database. This is within a while loop so it is executed once for each database. This set of tables is also filtered through another regex.
  • Finally in the third SQL command we dump the create table statement for each table (within yet another while loop).

This gets us the CREATE TABLE statements but MySQL puts some stuff in here we don’t want:

  • It prefixes each CREATE TABLE statement with the table name for some reason
  • It also includes the AUTO_INCREMENT value which we don’t care about either
  • It also doesn’t put new line characters in the output but does put the string “n”.

We pipe this output through a pretty gnarly looking sed find/replace to clean it up. The output is then redirected to a file with a name formatted to contain both the name of database and the name of the table. That’s all there is to it. I’ve put up dummy values for some of the selectors as an example.

The script can be downloaded here.

NOTE: The grep and sed regexs can be pretty confusing because of some bash double quoting and escaping. Knowing the rules for bash escaping is essential … and not covered here 🙂

Cheers!

Nokia N97 v20 firmware update requires Windows

Rant alert!

I was so pumped for this new firmware. Anything that makes my phone run better is a good thing. It’s even better when I can actually install it though! What the crap?!?! That’s right I can’t install it. Nokia is only distributing it initially for installation through their software updater that’s tied to some Microsoft Windows ™ software. Check out the release announcement in the Nokia forums.

Seriously? I’m not sure I follow the logic behind only distributing the update exclusively through the Windows ™ updater initially. Aren’t people running Windows ™ the ones that never update their software anyways? (ouch) The phone has a built in software updater for goodness sake! Sure it’s a big update (they list this as the reason it’s not distributed over the air yet) but so what!?!? I updated my firmware to v12 over the air the same day I bought the phone.

Gah! waiting != cool. For shame Nokia, for shame.

The Joys of Inheriting an Old Web Site

Recently I’ve been helping out a buddy of mine with a website he’s recently taken over. I’ve taken on hosting it and I’m helping getting some “best practices” into their development process. I’m no veteran of open source but I’m a long time spectator (some would say a lurker) so I figured it would be a good learning experience for the both of us.

When we took over the code was pretty much just dumped on our laps. We got a tar ball of the code (well the whole site really), a database dump, and a pat on the back. No install script, no documentation, just 53000 lines of PHP, some HTML and some users … great. We didn’t even know how much PHP code there was beyond a file count till I wrote a little script to count the number of lines of code in the PHP files. This script probably gives an inflated sense of code size since it doesn’t account for comments, whitespace or in line HTML. It’s probably safe to say the line count is a few thousand lines heavy but it’s a good estimate (better than what we started with).

find ./ -name '*.php' | 
  while read FILE; do
    SUM=$((SUM+$(cat $FILE | wc -l)));
    echo $SUM;
  done;

Simple right? But pretty useful. I had initially tried to replace the cat $FILE | wc -l with just a wc -l $FILE but I guess wc prints the file name after the line count and I couldn’t figure out how to turn this off. Meh, take the file name away from wc and all is well.

So this how it all started. I’ll be posting periodically as interesting things happen with this project. I’ll post useful scripts, whatever interesting PHP stuff I pick up along the way and anything else that may be interesting. The code is pretty old (dates on some files go back to 2003) so this should be interesting. I’ve even spotted a few frames on the site … yeah.

I’ll try to be constructive as much as possible but I can’t guarantee that I won’t post a few WTFs as I go through the code and find ’em … bad coding makes for good comedy (to the right audience or course).

Stay tuned.

Minimal GStreamer App

So in my last post (way back when) I was talking about transcoding some video. I wanted to use GStreamer to do the job but HandBreak was so much easier (pretty cool app too). So after I got the transcoding working with HandBreak I started playing around with GStreamer to see what it would take to code up a small app to do the job.

The first thing is just to code up a little bit of C to get the video playing. The GStreamer part here is pretty simple. It’s just like the gst-launch command I laid out in my last post. I’ve added a few bells and whistles to get command line parameters and to do some input validation. I’m using as much glib as I can for practice.

The result is a pretty short (for C) application that is a bare-bones GStreamer media player. Naturally you’d want a few additional things from a media player but that’s not our end goal. We want to take the playbin (or something like it) and hook it up to another pipeline of our own design that will encode the video & audio for our specific purposes. This is what’s coming up … soon hopefully. Honestly with the class I’m currently taking at SU it may be a while.

For now read the code and enjoy. It’s available for download below. I even made a Makefile for ya.

Download: gst-play-min.tar.gz

Video transcoding for the N97

A big part of why I bought the N97 was the media player. It has some pretty impressive specs: supports the h264 and MPEG4 codecs, the mp4 and 3gp containers, AAC audio … so yeah that’s pretty sweet. First thing I did when I got the phone all charged up was go into the old media library, copy over a file I thought met there specs and load it up into the media player. Yeah it definitely didn’t work.

Upon closer look there are some constraints I wasn’t meeting and unfortunately some bugs I didn’t know about. Let’s start with what I was doing wrong.

First off the constraints on the video dimensions are 630 by 320 pixels. Anything over this won’t play, the phone won’t scale the image down. So we’ve gotta convert the video, no getting around that. First thing I did was look into using GStreamer. I wanted to mock up a pipeline and throw it at gst-launch to do the transcoding. First thing I did was break out the playbin to do the decoding:


gst-launch-0.10 playbin uri:file:///path/to/vid

This does some GStreamer magic detecting all of the media file’s attributes and builds a pipeline in the bin to do the decoding. If you execute the above the bin will build default sinks for both the audio and video streams which will basically play the file.

Now it’s a matter of linking the audio and video streams from the playbin to a pipeline, simple right? I couldn’t figure out a way to do this from gst-launch. In fact I found a mailing list post indicating that this can’t be done through gst-launch. So now it’s build my own application or find one that already does this. As much as I like rolling my own, this is no small feat.

Coincidentally I’m at LinuxCon this week and I ran into another guy with the same phone. He pointed me to HandBrake. That was the ticket. This app has a great CLI interface and supports the codecs we need. It also has some built in profiles for popular devices.

My goal was to produce transcode to h264 at 630×320, AAC (don’t really care about bitrate etc) in an mp4 container. The HandBrake iPhone profile comes pretty close with a few minor modifications:


HandBrakeCLI -i ./infile.avi -o outfile.mp4 -e x264 -q 0.589999973773956 -a 1 -E faac -B 128 -R 48 -6 dpl2 -f mp4 --maxWidth 640 --maxHeight 320 -m -x level=30:cabac=0:ref=2:mixed-refs:analyse=all:me=umh:no-fast-pskip=1

This is a pretty compute intensive task. On my laptop it maxed out both CPUs for about a half hour for a 24 minute TV episode. In the end I had an mp4 file that was about 150MB with the desired attributes. Playing it with Totem was successful. The properties menu showed all the attributes were as expected and it looked great. Success?

Not quite. Turns out my N97 still couldn’t play it. The media player just threw up an error, wouldn’t play audio or video. I spent a while playing around with the parameters for the x264 encoder (on a much smaller file) but wasn’t able to get the phone to play any h264 encoded video. On a few occasions it would play the audio but never the video.

So h264’s out and MPEG4’s in. After getting rid of the h264 stuff from the above command I ended up settling on something like this:


HandBrakeCLI --input ./infile.avi --output outfile.mp4 --encoder ffmpeg --rate 24 --audio 1 --aencoder faac --ab 128 --arate 48 --mixdown dpl2 --format mp4 --maxWidth 640 --maxHeight 320 --markers

That’s the trick. Originally I had included the quality flag form the h264 command line. This produced a super small video file, 3.5 times smaller than the original but the video produced looked kinda grainy. Since I’m not pressed for space on my phone yet (32GB of storage is a lot of space) I dropped the quality flag and let it run presumably at 100%. This produced a file about 25MB smaller than the original (likely attributable to the decreased image resolution) with excellent picture quality. Better yet my N97 played it perfectly.

Conclusion: looks like the h264 implementation on the N97 isn’t all it’s cracked up to be. This is likely a bug. So for now, stick to MPEG4 through HandBrake and enjoy some videos on your N97. The new firmware update (v20) has been in the Nokia press recently but I haven’t seen anything about fixing h264 support. Meh.