Saturnboy
 4.19

Open Video Player in AIR

, , ,

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):


Comments

Peter Carabeo-Nieva

4.29.2009

1

Great Post! We both happen to be very early tinkerers of OVP, there’s almost nothing else out there on the web regarding OVP at this point, just a bunch of press but nobody really putting it to use and blogging about it etc. I’m actually looking to extend it greatly so that it can tie into ad serving systems etc. Adobe Strobe also looks very promising but it won’t be out for a while. Thanks again for this excellent post!

4.29.2009

2

I must agree that the OVP is currently 95% hype…but not without reason because there’s definitely something there.

Hopefully, Adobe will come in and take web video to the next level.

alberto

7.8.2009

3

hei!
i was troubling with netstream for a while great article!

technofeeliak

5.17.2010

4

I don’t like the Flash player very much. Like everything Adobe makes it’s not user friendly and takes a lot of time to become familiar.

I don’t understand why no one has talked about making a flash menu that links multiple video files. My idea is to make a graphic interface with which I can access my entire dvd collection backed up as vob files and play them in Windows Media Player. Imagine how easy it would be to chose the movie you want to watch with a totally customized menu including and image of the dvd cover, links to IMDb.com for more information, a play button to one or more disks that came with the title, along with multiple views of the listed collection to facilitate navigation. I came so close to completing this project but couldn’t link the play buttons to the vob files for them to open in WMP. I’ve written to Adobe and of course they don’t explain anything. I’ve spent a lot of time watching tutorials on lynda.com but still nothing. I learned about the existence of AIR and some of it’s capabilities but still no clue about the line of code I need to get the buttons to open the video files in WMP (Windows Media Player). If someone can give me some kind of resource I’d really appreciate it.

5.17.2010

5

@technofeeliak: I think what you want is the Native Process API that’s available in AIR 2.0. Using this new API, you should be able to write a catalog browser app in AIR that gets its data from file system, but defers to some non-flash video player application for playback. I’d recommend using VLC for your video player, because Windows Media Player sucks.

Some googling turned up this article on using the Native Process API to control MPlayer (another video player application). Good luck.

© 2017 saturnboy.com