Saturnboy
 6.1
Code
off

The plan is simple, take the nice Degrafa-skinned components from Part 1 and assemble them into a video player powered by the OvpNetStream class from the Open Video Player project.

Design

I knew right away that the design was not going to have any right angles, but I also didn’t want to go with rounded rectangles everywhere. Modern TVs tend to have a lot of soft rounded edges, so I decided to go with a more vintage look. So I fired up Inkscape and got to work:

tv

Implementing the video player design above in Degrafa, the cabinet mapped to a RoundedRectangle, and the screen & antenna became Paths. You can read more about about translating SVG to Degrafa in my Inkscape SVG to Degrafa Path article. But for now, let’s focus on the resulting Degrafa code:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application
        xmlns:mx="http://www.adobe.com/2006/mxml"
        xmlns:Degrafa="http://www.degrafa.com/2007"
        layout="absolute">
 
    <Degrafa:GeometryComposition graphicsTarget="{[box]}">
        <!-- Antenna -->
        <Degrafa:Path data="...path data...">
            <Degrafa:transform>
                <Degrafa:TranslateTransform x="15" />
            </Degrafa:transform>
        </Degrafa:Path>
 
        <!-- TV cabinet -->
        <Degrafa:RoundedRectangle y="144" width="350" height="300"
                cornerRadius="20" />
 
        <Degrafa:fill>
            <Degrafa:SolidFill color="#333333" />
        </Degrafa:fill>
        <Degrafa:stroke>
            <Degrafa:SolidStroke color="#FF00FF" weight="4" alpha="0.4" />
        </Degrafa:stroke>
    </Degrafa:GeometryComposition>
 
    <!-- TV Screen -->
    <Degrafa:GeometryComposition graphicsTarget="{[tvscreen]}">
        <Degrafa:Path data="...path data...">
            <Degrafa:fill>
                <Degrafa:SolidFill color="#FF99FF" />
            </Degrafa:fill>
            <Degrafa:stroke>
                <Degrafa:SolidStroke color="#FF00FF" weight="2" alpha="0.4" />
            </Degrafa:stroke>
            <Degrafa:filters>
                <mx:GlowFilter color="#EEEEEE" alpha="0.2" blurX="16" blurY="16" />
            </Degrafa:filters>
        </Degrafa:Path>
    </Degrafa:GeometryComposition>
 
    <mx:Canvas width="350" height="444"
            horizontalCenter="0" verticalCenter="0">
        <mx:Canvas id="box" />
        <mx:Canvas x="50" y="174" id="tvscreen" />
    </mx:Canvas>
</mx:Application>

I ended up using a pair of GeometryCompositions to wrap my three shapes to help keep my fills, strokes, and filters organized. It made sense to do it this way, but I won’t claim it’s the best way. Throw the control bar on below the TV screen, and the design is done.

Backend

The backend is build on the OvpNetStream class provided by the Open Video Player project. OvpNetStream extends NetStream and smooths out some of the rough edges as I discussed previously. Basically, it provides a sane interface (no need to construct a dynamic object with function callbacks) and useful events (like metadata and progress events).

For this demo, we simply instantiate OvpNetStream on creationComplete and wire up all the event handlers:

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);
    ns.addEventListener(OvpEvent.COMPLETE, streamCompleteHandler);
 
    vid = new Video();
    vid.attachNetStream(ns);
    vidContainer.addChild(vid);
}

The most interesting events are the metadata and progress events. The metadata event delivers the duration of the video and its size. The progress event arrives periodically (theoretically every 100ms by default, but in reality I see them come in just a couple of times per second) and delivers the current video time.

Control Bar

The control bar consists of three components: a play-pause button, a scrubber, and a volume slider. They were skinned using Degrafa in Part 1. In order to control video playback, we need to wire the control bar components to the instance of OvpNetStream created above.

Here are the event handlers for the three control bar components:

// PlayPause event handler
private function playPauseClick():void {
    if (first) {
        first = false;
        ns.play(filename);
    } else {
        ns.togglePause();
     }
}
 
// Scrub event handlers
private function scrubPress():void {
    ns.pause();
    playPause.selected = false;
}
private function scrubDrag():void {
    ns.seek(scrub.value);
}
private function scrubRelease():void {
    ns.togglePause();
    playPause.selected = true;
}
 
// Volume event handler
private function volumeChange():void {
    ns.volume = volume.value;
}

In the playPauseHandler(), the initial click calls play() which actually loads the video (and then starts playback), all subsequent clicks just toggle between play or pause. For the scrubber handlers, I chose to break them up into three separate steps: on mouse down pause the video, on mouse up restart playback, and on drag attempt to seek to the to the new time.

That’s it. Here is the resulting Degrafa-skinned video player (view source enabled):

Flash is required. Get it here!

Click Play to start playing Elephants Dream (which is the “world’s first open movie,” and pretty cool too). Right away you’ll notice some visual issues because the dimensions of the video are unknown until the metadata arrives. Also, scrubbing has some problems which I believe are related to cue points in progressive downloads. Lastly, I didn’t implement any indicators for buffering or download progress, so you’ll need to be patient. Since this is just a demo, I’ll have to leave fixing those bugs as an exercise for the reader.

Files

 5.26

I’m going to combine my earlier Degrafa skinning efforts with my more recent video work to create a Degrafa-skinned video player. In this post, I’ll build out all the components required for a video player control bar. And in Part 2, I’ll weld the control bar to an Open Video Player OvpNetStream backend.

Button

To get started, I built out a simple button skin with a slight shine on the top half. I also added rounded corners to give the classic web 2.0 look-and-feel. There’s really not much to look at:

Flash is required. Get it here!
Play-Pause Button

I’m using a toggleable Button component as the base of my Play-Pause button. By leveraging Degrafa’s stateful skins, I simply change the visibility of the Play and Pause geometry depending on the skin’s state. Here’s a snippet from the states block of the skin:

<states>
    <State name="upSkin">
        <SetProperty target="{rect}" name="fill" value="{upFill}"/>
        <SetProperty target="{play}" name="visible" value="true"/>
    </State>
    <State name="selectedUpSkin">
        <SetProperty target="{rect}" name="fill" value="{upFill}"/>
        <SetProperty target="{play}" name="visible" value="false"/>
    </State>
    ...
</states>

Additionally, the play arrow and pause bars are dynamically sized based on the button height, so they readily scale to any size. The result:

Flash is required. Get it here!
Scrubber

Getting total control over a Slider skin is not easy in Flex 3. There are two issue that I find quite annoying: the thumb size is hard-coded into the class and the highlight skin is offset from the track. Fortunately, the web is full of workarounds for the thumb size issue, but for this skin I only needed to hard-code an offset (+6,+6) to bring the thumb’s origin to (0,0) and make all my geometry line up correctly. Similarly, I added a 1 pixel offset to the highlight skin to bring its origin to (0,0) to match the track. The result:

Flash is required. Get it here!
Volume Slider

For consistency, the Volume slider re-uses the Scrubber’s thumb skin, but the track and highlight are wedges (Polygon‘s in Degrafa). The only clever piece of code is the highlight skin’s overridden updateDisplayList() function:

override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
    super.updateDisplayList(unscaledWidth, unscaledHeight);
    awidth = unscaledWidth;
    aheight = unscaledHeight;
 
    if (this.hasOwnProperty('parent') &&
             this.parent.hasOwnProperty('parent') &&
             this.parent.parent.hasOwnProperty('width') &&
             this.parent.parent.width > 0) {
        _ratio = awidth / this.parent.parent.width;
    } else {
        _ratio = 0.00001;
    }
}

In order to correctly draw the highlight wedge, we first need to know the dimensions of the track. For simplicity, the Volume track height is hard-coded in both the track skin and highlight skin, but the width of the track is variable. In this case, the width of the track is the same as the width of the HSlider component itself. So in the updateDisplayList() code above, the track width is found and used to compute the ratio of widths. This ratio is later used to draw the highlight geometry. The end result:

Flash is required. Get it here!
The Full Control Bar

Just displaying all the components side-by-side gives us the beginnings of a video player control bar:

Flash is required. Get it here!

Not too shabby. In Part 2, I’ll assemble the final video player frontend including the pretty control bar components above, and weld it to an OvpNetStream-powered backend.

Files

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


© 2014 saturnboy.com