Saturnboy
 5.19

The AIR Install Badge is a very handy little flash application for delivering AIR applications to your users via the web. The badge allows your users to download and install both your application and the Adobe AIR runtime. Additionally, the install badge will automatically prompt users to upgrade if a previously installed version is detected. At Gorilla Logic, we use the AIR Install Badge on the FlexMonkey download page (free registration required).

Alas, flash is opaque to analytics. We have no idea what our users are doing inside the AIR Install Badge application. Are they installing? Or upgrading? No problem, we just need to write some code…

The Code

Using flash’s ExternalInterface, we can manually push the data out of flash and into javascript. Once we have the data in javascript, we have total control. One option is to use google analytics to store our badge data. In the case of FlexMonkey, we send the badge data along with the user’s credentials to our CRM platform, SalesForce.com.

Step 1: First, open AIRInstallBadge.as and add this to the top:

import flash.external.ExternalInterface;

Step 2: Next, add the ExternalInterface call to the top of the handleActinClick() function in AIRInstallBadge.as:

protected function handleActionClick(evt:MouseEvent):void {
    if (action == 'install' || action == 'upgrade') {
        //send data to js
        ExternalInterface.call('badgeJS',action);
    }
    ...
}

Since I only care about the install or upgrade actions, I’ll only send those out to javascript. Re-compile the badge and deploy.

Step 3: Last, add the badgeJS() javascript callback to the page containing the badge and do whatever you want with the incoming badge data:

function badgeJS(action) {
    //do metrics here...
    alert('badge action=' + action);
}

Conclusion

With an hour of effort, and a very small amount of code, we’ve managed to get the useful metrics of installs and upgrades out of the AIR Install Badge and into our analytics engine of choice. A job well done.


 4.19

Recently, I’ve been doing quite a bit of video work in Flex and AIR. My main gripe (that I’ll try to rectify below) is that there sure is a lot of info around the web about streaming, FMS, and friends, but almost none about playing local video. Even the Adobe Media Player, which is a really nice AIR app, is all about streaming content from the web.

And like those before me, I’ve come to the same conclusion about video in Flash: it’s pretty cool, but I sure wish it was better. Enter the Open Video Player as the answer to some of NetStream‘s woes. Alas, all of the OVP docs and samples are, once again, all about streaming, Akamai, bandwidth, etc.

The elephant in the room for an AIR video player: What if I’m offline?

The Easy Way: VideoDisplay

I gonna get started with the easiest possible local video player, which in Flex means using VideoDisplay. Here’s a simple AIR app that allows you to open a FLV file and play it:

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
        width="700" height="500">
 
    <mx:Script>
        <![CDATA[
            [Bindable] private var filename:String = '';
 
            private function openVideo():void {
                if (vd.playing) {
                    vd.pause();
                    playPause.label = 'PLAY';
                }
 
                var docs:File = File.documentsDirectory;
                docs.browseForOpen('Select a Video', [new FileFilter("Video", "*.flv")]);
                docs.addEventListener(Event.SELECT, openFileHandler);
            }
 
            private function openFileHandler(e:Event):void {
                var f:File = e.target as File;
                filename = f.name;
 
                vd.source = 'file://' + f.nativePath;
                vd.play();
            }
 
            private function stopClick():void {
                vd.stop();
                playPause.label = 'PLAY';
            }
 
            private function toggleClick():void {
                if (vd.playing) {
                	vd.pause();
                	playPause.label = 'PLAY';
                } else {
                	vd.play();
                	playPause.label = 'PAUSE';
                } 
            }
        ]]>
    </mx:Script>
 
    <mx:ApplicationControlBar dock="true">
        <mx:Button label="Open Video" click="openVideo()" />
        <mx:Label text="{filename}" fontSize="14" fontWeight="bold" />
    </mx:ApplicationControlBar>
 
    <mx:Canvas width="100%" height="100%" backgroundColor="#000000" backgroundAlpha="0.1">
    	<mx:VideoDisplay id="vd" autoPlay="false" />
    </mx:Canvas>
 
    <mx:HBox>
        <mx:Button id="playPause" label="PAUSE" width="100" click="toggleClick()" />
        <mx:Button label="STOP" width="100" click="stopClick()" />
    </mx:HBox>
</mx:WindowedApplication>

The only real magic is the vd.source = 'file://' + f.nativePath line. Apparently, source wants a URL even for a local file, so just prepend file:// and everything works fine.

Getting Fancy with NetStream

As far as I can tell, no one actually uses VideoDisplay at the core of a video player component in a production system. The many benefits of NetStream are not really clear to me, but it is the darling of production systems. Thankfully, despite its name, NetStream is perfectly happy playing local video. Rebuilding the above example yields:

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
        width="700" height="500"
        applicationComplete="complete()">
 
    <mx:Script>
        <![CDATA[
            [Bindable] private var filename:String = '';
            private var nc:NetConnection;
            private var ns:NetStream;
            private var vid:Video;
            private var t:Timer;
 
            private function complete():void {
                nc = new NetConnection();
                nc.addEventListener(AsyncErrorEvent.ASYNC_ERROR, errorHandler);
                nc.addEventListener(NetStatusEvent.NET_STATUS, connStatusHandler);
                nc.connect(null);
 
                ns = new NetStream(nc); 
                ns.addEventListener(AsyncErrorEvent.ASYNC_ERROR, errorHandler);
                ns.addEventListener(NetStatusEvent.NET_STATUS, streamStatusHandler);
                ns.client = { onMetaData:streamMetadataHandler };
 
                t = new Timer(300);
                t.addEventListener(TimerEvent.TIMER, streamProgressHandler);
 
                vid = new Video();
                uic.addChild(vid);
            }
 
            private function openVideo():void {
                if (ns) {
                    ns.pause();
                    playPause.label = 'PLAY';
                }
 
                var docs:File = File.documentsDirectory;
                docs.addEventListener(Event.SELECT, openFileHandler);
                docs.browseForOpen('Select a Video', [new FileFilter("Video", "*.flv")]);
            }
 
            private function openFileHandler(e:Event):void {
                var f:File = e.target as File;
                filename = f.name;
 
                ns.play('file://' + f.nativePath);
                playPause.label = 'PAUSE';
 
                vid.attachNetStream(ns);
 
                t.start();
            }
 
            private function errorHandler(e:AsyncErrorEvent):void {
                trace('ERROR: ' + e.text);
            }
 
            private function connStatusHandler(e:NetStatusEvent):void {
                trace('CONN_STATUS: ' + e.info.code);
            }
 
            private function streamStatusHandler(e:NetStatusEvent):void {
                trace('STREAM_STATUS: ' + e.info.code);
            }
 
            private function streamMetadataHandler(info:Object):void {
                for (var key:String in info) {
                    trace("STREAM_METADATA: " + key + "=" + info[key]);
                }
                uic.width = info.width;
                uic.height = info.height;
 
                vid.width = info.width;
                vid.height = info.height;
                vid.x = 0;
                vid.y = 0;
 
                len.text = parseFloat(info.duration).toFixed(1);    
            }
 
            private function streamProgressHandler(e:TimerEvent):void {
                   timer.text = ns.time.toFixed(1);
            }
 
            private function stopClick():void {
                ns.pause();
                ns.seek(0);
                playPause.label = 'PLAY';
            }
 
            private function toggleClick():void {
                ns.togglePause();
                playPause.label = (playPause.label == 'PAUSE' ? 'PLAY' : 'PAUSE');
            }
        ]]>
    </mx:Script>
 
    <mx:ApplicationControlBar dock="true">
        <mx:Button label="Open Video" click="openVideo()" />
        <mx:Label text="{filename}" fontSize="14" fontWeight="bold" />
        <mx:Label id="timer" fontSize="14" fontWeight="bold" />
        <mx:Label id="len" fontSize="14" fontWeight="bold" />
    </mx:ApplicationControlBar>
 
    <mx:Canvas width="100%" height="100%" backgroundColor="#000000" backgroundAlpha="0.1">
        <mx:UIComponent id="uic" width="100" height="100" />
    </mx:Canvas>
 
    <mx:HBox>
        <mx:Button id="playPause" label="PAUSE" width="100" click="toggleClick()" />
        <mx:Button label="STOP" width="100" click="stopClick()" />
    </mx:HBox>
</mx:WindowedApplication>

NetStream is a wonder of engineering. Basic usage is understandable, just feed in null to the NetConnection to switch to progressive download mode, and use the same file:// trick above to play a local video. But after that, things get weird fast. First, the metadata handling to totally backwards: you have to create a dynamic object with a onMetaData property that maps to a callback function. My guess would be that there is some legacy bullshit going on that breaks addEventListener. Second, you can’t tell if a video is playing or not. Third, the lack of progress event means you need to use a timer to track playhead time. Add it up – LAME.

Reaching for the Grail: Open Video Player

According to their website, it’s Open Video Player to the rescue. And I’ll have to agree that it fixes many of NetStream issues. Swaping in OvpNetStream yields:

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
        width="700" height="500"
        applicationComplete="complete()">
 
    <mx:Script>
        <![CDATA[
            import org.openvideoplayer.net.OvpConnection;
            import org.openvideoplayer.net.OvpNetStream;
            import org.openvideoplayer.events.OvpEvent;
 
            [Bindable] private var filename:String = '';
            private var nc:OvpConnection;
            private var ns:OvpNetStream;
            private var vid:Video;
 
            private function complete():void {
                nc = new OvpConnection();
                nc.addEventListener(OvpEvent.ERROR, errorHandler);
                nc.addEventListener(NetStatusEvent.NET_STATUS, connStatusHandler);
                nc.connect(null);
 
                ns = new OvpNetStream(nc);
                ns.addEventListener(OvpEvent.ERROR, errorHandler);
                ns.addEventListener(NetStatusEvent.NET_STATUS, streamStatusHandler);
                ns.addEventListener(OvpEvent.NETSTREAM_METADATA, streamMetadataHandler);
                ns.addEventListener(OvpEvent.PROGRESS, streamProgressHandler);
 
                vid = new Video();
                uic.addChild(vid);
            }
 
            private function openVideo():void {
                if (ns) {
                    ns.pause();
                    playPause.label = 'PLAY';
                }
 
                var docs:File = File.documentsDirectory;
                docs.addEventListener(Event.SELECT, openFileHandler);
                docs.browseForOpen('Select a Video', [new FileFilter("Video", "*.flv")]);
            }
 
            private function openFileHandler(e:Event):void {
                var f:File = e.target as File;
                filename = f.name;
 
                ns.play('file://' + f.nativePath);
                playPause.label = 'PAUSE';
 
                vid.attachNetStream(ns);
            }
 
            private function errorHandler(e:OvpEvent):void {
                trace('ERROR: ' + e.data.errorNumber + ': ' + e.data.errorDescription);
            }
 
            private function connStatusHandler(e:NetStatusEvent):void {
                trace('CONN_STATUS: ' + e.info.code);
            }
 
            private function streamStatusHandler(e:NetStatusEvent):void {
                trace('STREAM_STATUS: ' + e.info.code);
            }
 
            private function streamMetadataHandler(e:OvpEvent):void {
                for (var key:String in e.data) {
                    trace("STREAM_METADATA: " + key + "=" + e.data[key]);
                }
                uic.width = e.data.width;
                uic.height = e.data.height;
 
                vid.width = e.data.width;
                vid.height = e.data.height;
                vid.x = 0;
                vid.y = 0;
 
                len.text = nc.streamLengthAsTimeCode(e.data.duration);
            }
 
            private function streamProgressHandler(e:OvpEvent):void {
                   timer.text = ns.timeAsTimeCode;
            }
 
            private function stopClick():void {
                ns.pause();
                ns.seek(0);
                playPause.label = 'PLAY';
            }
 
            private function toggleClick():void {
                ns.togglePause();
                playPause.label = (playPause.label == 'PAUSE' ? 'PLAY' : 'PAUSE');
            }
        ]]>
    </mx:Script>
 
    <mx:ApplicationControlBar dock="true">
        <mx:Button label="Open Video" click="openVideo()" />
        <mx:Label text="{filename}" fontSize="14" fontWeight="bold" />
        <mx:Label id="timer" fontSize="14" fontWeight="bold" />
        <mx:Label id="len" fontSize="14" fontWeight="bold" />
    </mx:ApplicationControlBar>
 
    <mx:Canvas width="100%" height="100%" backgroundColor="#000000" backgroundAlpha="0.1">
        <mx:UIComponent id="uic" width="100" height="100" />
    </mx:Canvas>
 
    <mx:HBox>
        <mx:Button id="playPause" label="PAUSE" width="100" click="toggleClick()" />
        <mx:Button label="STOP" width="100" click="stopClick()" />
    </mx:HBox>
</mx:WindowedApplication>

It’s not a revolution because OvpNetStream just extends NetStream, but we instantly get standard events thrown for progress ticks and metadata, and also some pretty time formatting as a bonus. The OVP also does about a hundred other things related to streaming, streaming servers, and Akamai. That’s about all I know about video right now, so before I can post again I’ll need to learn something new…

Files

Just tarballs (it’s an AIR app after all):


© 2017 saturnboy.com