Skinning the Longtail Player (aka JW)

This site is quickly becoming (just as we planned it ... Mu-hu-ha-ha!) a repository documenting things that took ages to figure out, we know we'll need again, but not often enough to trust our (rapidly aging) memories. The fact that its public and you get to play along is a bonus. I say that because writing this stuff up takes time, especially to do so clearly, neatly, and accurately. I have four projects going on, but I'm taking a moment to save this before I loose it; though perhaps muddled, disheveled, and ... suspicious.

Documentation for the Jeroen Wijering video player (http://www.longtailvideo.com/) leaves something to be desired -- it's great if you want to use it as they describe. Either:
1) embed a video in an HTML page where the Flash document is (and nothing but) the video player.

2) embed a player (and perhaps its skin) by loading those components at runtime.

These are great approaches. What I'd like to do is embed my player and skin at compile time causing it to bake directly into the swf I'm distributing. As a bonus, I'd like to compile against swc files in both cases so that:

a) Compiles are faster.

b) There is one-and-only-one version of the ActionScript floating around my harddrive which is messy enough already thank you.

I know I can accomplish "b" without a swc by adding the jw project folder to my classpath, but then that's one more thing to grab a copy of when I'm sending source to clients. Which I will forget about, and they won't actually try compiling anything until six months later at which point my copy of the source has changed ... See? I need a swc and I have good reasons.

Step 1: Create the player.swc

Open the player.fla that comes with the jw source. In the publish settings, check off create swc. Compile. Grab player.swc and add it to the classpath in your build tool of choice.

Step 2: The skin. (I'll post mine here later, but you should understand the steps)

Make a new fla, grab a copy of the library from the original and drop it in the new Skin.fla. Edit the properties of the "player" symbol. Set it to export with a class that we'll use in a bit CustomPlayerSkin. Follow the previous instructions for exporting a swc and adding it to your classpath.

In your project, you'll want to do something like this:


package {
	import com.jeroenwijering.events.ModelEvent;
	import com.jeroenwijering.events.ViewEvent;
	import com.jeroenwijering.player.View;

	import flash.events.Event;

	import com.jeroenwijering.events.PlayerEvent;
	import com.jeroenwijering.player.Player;

	import flash.display.MovieClip;

	/**
	 * @author projects
	 */
	public class PlayerHarness extends MovieClip {
		private var jwPlayer : Player;
		private var skin : CustomPlayerSkin;

		public function PlayerHarness()
		{
			addEventListener (Event.ADDED_TO_STAGE, onAdded)
		}

		private function onAdded(event : Event) : void
		{
		  populateAssets()
		}

		private function populateAssets(e:Event=null)
		{
			//jwPlayer
			jwPlayer = new Player()
			//remove default skin
			jwPlayer.removeChild (jwPlayer.player)
			//skin
			skin = new CustomPlayerSkin()
			jwPlayer.addChild(skin)

			//set new skin
			jwPlayer.skin = skin
			jwPlayer.width = skin.width
			jwPlayer.height = skin.height

			jwPlayer.config.autostart = true
			jwPlayer.config.fullscreen = true
			jwPlayer.config.resizing = false

			//size and position
			jwPlayer.config.width = this.stage.stageWidth
			jwPlayer.config.height = this.stage.stageHeight
			//skin.x = 0
			//skin.y = 0

			jwPlayer.addEventListener(PlayerEvent.READY, playerReady);

			addChild(jwPlayer)
		}

		private function playerReady(evt:Event=null) {
			var view:View = evt.target.view;

			//LOAD AN FLV!
			view.sendEvent(ViewEvent.LOAD, {file:"phone.flv"});

			//ADD AN EVENT
			view.addModelListener(ModelEvent.TIME, onPlayerTimeUpdate)
		}

		private function onPlayerTimeUpdate (e:ModelEvent)
		{
			trace ("pos" + e.data.position)
		}
	}
}

Photoshop Action – Save Slice for Web

Rob noticed today that every time he re-opens his site design doc the save-for-web window forgets that he's set it to "single slice" and not "all slices." We fumbled for a solution for a bit before settling on this: an action that copies the current slice to a fresh document and then invokes save-for-web from there.

This works because, as long as there's no current selection (make sure there's no current selection), the copy command will copy the content of the currently selected slice. Nice, eh?

Save the downloaded file as /Users/YOURUSER/Library/Application Support/Adobe/Adobe Photoshop CS4/Presets/Actions/shovemedia Actions.atn

Open Photoshop, go to the Actions pallete, click the menu in the upper right corner and select Load Actions...

I made this on (a Mac) CS4 -- your mileage may vary. Shouldn't matter though -- it's at least as easy to just record the action yourself.

Photoshop Tip – Transform Selection

I'm not sure how I missed this -- it's been in Photoshop since at least CS2 and from some of my Google results, probably far earlier than that.

When we cut images for the web, we use the Marquee tool a lot, but it can be a real pain to hit your corners exactly on the first try -- sometimes the pixels you're using as a visual guide are off screen if you've zoomed in far enough to see what you're doing! The solution (of course) is Transform Selection which gives you scale, rotate, and skew control over your current selection just like Transform Layer without messing with the underlying pixels. Very handy for expanding and trimming those rectangular selections.

Access it via right-click (command-click on Mac) of the selection or the Selection menu.

Flash TextField <img>

This wasn't the first time I struggled to incorporate <img> tag support into a project, but it might be the last. The bugs with this "feature" are so bad, that for all but the simplest of cases (and some of those), it's rendered completely useless. Today, I worked around another problem with links exhibiting wild layout schizophrenia. This time, it would appear to be a problem with the combination of a:hover styles and <img> tags.

Now, I've held back talking trash on the TextField because of all the version 10 metric goodness I'd heard we were getting. Finally the TextField has gotten some much needed attention. Decent support for multiple languages going different directions in the same document. Solid, hard stuff. So surely Adobe fixed all (some of?) those bugs that have been around for, what, 2, 3, maybe even 4 versions (god I'm getting old)? ... No.

Can we maybe have html support that doesn't suck, with stylesheet support that doesn't suck? Together? At the same time? I'll give you a pass on font embedding even though that's been ... superbly difficult even longer because I understand the pressure from the foundaries.

In our next episode, I'll show you how to carve up your htmlText into multiple TextFields so your links stay where you put them (I tried everything else). Because what else are you going to do but hack around it?

Load HTML from XML – Part II

Update: had to take another look at this due to Flash busting my character entities!

In the last installment, I showed you a function that will walk through an XML node's (multi-part) children and return an HTML string. This approach is unfortunately flawed -- whitespace collapsing is a bit over-eager resulting in XML such as the following:
This is a <a href="page.html">link</a>

converting to:
This is alink
(Note the missing space.)

Fortunately, I have not only a solution, but since it uses regex, it ought to be a good bit more efficient:

package {

	import StringUtils;
	import CharacterEntity;

	public class XmlUtil {

		static public function getHTMLContent (xml:*):String {
			//trace (typeof(xml) + "   " + xml.toXMLString())

			if (typeof (xml) == 'string') xml = new XML(xml)

			var html = ""
			var prettyPrint = XML.prettyPrinting
			XML.prettyPrinting = false
			var ignoreWhite = XML.ignoreWhitespace
			XML.ignoreWhitespace = false

			var children = xml.children()
			var len = children.length()
			if (len)
			{
				//trace ('Multiple Children')
				for ( var i=0; i<len; i++ )
				{
					var decoded = CharacterEntity.decodeXHTML(children[i].toXMLString() , true)
					html += decoded
				}
				html = StringUtils.removeExtraWhitespace( html )

			}
			else
			{
				//trace ('Simple Content')
				var str = StringUtils.removeExtraWhitespace( CharacterEntity.decodeXHTML(xml.toXMLString(), true) )
				html += str
			}

			XML.prettyPrinting = prettyPrint
			XML.ignoreWhitespace = ignoreWhite

			//logger.info ("HTML " + escape(html))

			return html
		}
	}
}

You'll need two libraries: StringUtils from the worship-worthy studio of Grant Skinner CharacterEntity, originally written for AS2 by Jim Cheng and kindly converted to AS3 by Thirdparty Labs.

The code is a lot simpler now, but for completeness, I'll give you a quick run-down. If you pass in a String (accessing an attribute or text node could actually cause this), we convert it to XML first. First, we turn ignoreWhitespace off since it's the source of the issue above. Walk through the children (if they exist) decoding the entities and remove any additional whitespace. The "true" parameter on the decodeXHTML method is explained in this post.

Load HTML from XML source – Part I

UPDATE:
I managed to forget TextField.condenseWhite = true!

Now, although that will handle 90% of your XML -> HTML whitespace issues. There are a few scenarios where the hints in this series come in handy:

  • You want more control over XML -> HTML tag handling. For instance you want to avoid an extraneous wrapping tag.
  • The CharacterEntity class is still relevant and useful.
  • You're using TextField.styleSheet. If you're not using a stylesheet, you can set the htmlText and then access the html with collapsed whitespace by reading the htmlText property back. If you do use a stylesheet, sniffing TextField.htmlText won't show collapsed whitespace).
  • We often, pull our dynamic content into Flash via XML. A Lot. As in pretty much exclusively. Sometimes, we'll have honest-to-goodness information architecture to carve up that data semantically. We end up with a structured tree of simple text nodes. The XML parsing routine figures out how to turn this into a view. Great for all kinds of reasons. But sometimes, we just want to treat a node as a block of HTML and pass it into the htmlText property of a TextField.

    This is unnecessarily hard. Neither XML.toString() nor XML.toXMLString() do what you want for this case. But the following function will:

    private function getHTMLContent (xmlString:String):String
    {
      var html = ""
      var prettyPrint = XML.prettyPrinting
      XML.prettyPrinting = false
      var ignoreWhite = XML.ignoreWhitespace
      XML.ignoreWhitespace = true

      var xml:XML = new XML (xmlString)
      var children = xml.children()

      if (children.length())
      {
        for each (var i in children)
        {
          html += i.toXMLString()
        }
      }
      else
      {
        html += xml
      }

      XML.prettyPrinting = prettyPrint
      XML.ignoreWhitespace = ignoreWhite

      return html
    }

    To use it, pass the XML node containing the HTML, and the function will return the appropriate HTML String. Let's say your XML looks like:

    <services>
    <description>Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br /><br />Aliquam in lectus quis nisl lacinia dignissim.
    <ul>
      <li>Proin viverra.</li>
      <li>Phasellus tristique.</li>
    </ul>
    </description>
    </services>

    Then, after loading the XML document, you'd do something like:

    var desc = getHTMLContent(myXML.description)
    content_tf.condenseWhite = true
    content_tf.htmlText = desc

    If you skip this step, you'll end up including the description node (which isn't what you want) or mangling the mixed-content nature (omitting the br tags or the parent-less "Lorem ipsum" opening sentence or something equally as wrong). The script should also works if your content is a straight-forward text node.

    The prettyPrint and ignoreWhitespace stuff keeps your whitespace from wreaking havok with Flash's HTML format (which is different from a browser in that it'll happily add extra newlines when they appear in the source).

    Next time, I'll incorporate Rob's entity decoding script since Flash's built-in entity support is pretty lacking.

    FDT trace w/o Debug Revisited

    My last post on this subject outlined a pretty good solution to this problem. I had one minor annoyance, and the further I chased it, the further from my original solution I drifted. Tail-ing the Flash debug log works great. It's fast, simple, easy to set up -- but I wasn't quite satisfied. I wanted to clear the log at compile so that Eclipse's console window would only contain text from the current run. Clearing the log file is easy enough, but getting Eclipse's console window to refresh turned out to be next to impossible -- at least via an Ant workflow (comments very welcome).

    I dropped a note on the FDT message board, and one of the moderators pointed me to SosMax -- their free XML-based logging server. I was immediately resistant. That's just way way too much overhead for something as simple as trace output. But I gave it a chance, and am I ever glad I did. After struggling with the initial setup (as I always do with these packages), I quickly became a convert. SosMax really is a sweet little system, and after a bit of hackery, I was able to get it incorporated into my project without converting all my trace statements to myLogger.debug calls.

    In addition to the great filtering, color-coding, etc features you'd expect with a custom logger, SosMax will also pull the trace log file, and it has an option for clearing the console automatically when a new connection is established. Ah ha!

    Rather than write a log connector class from scratch, I downloaded one from Sönke Rohde . His class was almost perfect for my needs: by default, he only connects to the logging server if there's a message to be sent (ie, the first time you attempt to send a message). I only wanted to connect for the purpose of clearing the console, so I moved the connection logic into its own public method. Then, in my Main class:


    import com.soenkerohde.logging.SOSLoggingTarget; 

    import mx.logging.Log;
    import mx.logging.ILogger;
    import mx.logging.LogEventLevel;

    public class Main {

    private static const logger:ILogger = Log.getLogger("Main");

    public function Main()
      {

       var sosLoggerTarget = new SOSLoggingTarget();
       //sosLoggerTarget.includeCategory = true;
        sosLoggerTarget.includeLevel = true;

       Log.addTarget(sosLoggerTarget) 
        sosLoggerTarget.connect()

     }
    }

    Trace in FDT without Debug Perspective

    I recently switched to OS X, so I can't use my beloved FlashDevelop. FDT is really the most viable option despite the price tag. It takes awhile to get used to Eclipse and get all the settings dialed in, but it's worth it.

    The only last major issue I had was getting trace output from the debug player to appear in the Eclipse console without launching a Debug task (way too much additional overhead just to see trace messages).

    I'm on OS X which has 'tail' -- if you're on windows (why aren't you using FlashDevelop?), you'll have to use cygwin or get fancy with the batch scripting. There's also something called tail.exe from MS...

    Found this: http://flash-focus.blogspot.com/2007/06/creating-bare-bones-output-window-in.html which outlines how to configure the Debug Player to drop trace messages into a log file.

    First, I followed those instructions ... then in Eclipse added an External tool configuration:

    Run >> External Tools >> External Tools Configurations
    New Program
    Location: /usr/bin/tail
    Arguments: -f "/Users/MYUSER/Library/Preferences/Macromedia/Flash Player/Logs/flashlog.txt"

    I wasn't able to point to the user Library via ~/Library -- maybe that's an Eclipse shortcoming...

    Launch that, and it appears as one of the running console logs, then you can run your build normally, ANT or what-have-you, and you'll still get the trace output without all the debug perspective stuff, and it's about a zillion times faster. Yay!

    Seems like there ought to be console level configuration somewhere, but I'm no Eclipse expert.

    Hope this helps someone else.

    Weird note of the day. If you place a multiline input Textfield on the stage (as opposed to creating it with the class constructor), it will contain a newline by default rather than the expected null. Simply reset it to proceed normally:
    myTextField.text = ""

    AS3 package names vs property names

    Tattoo it on your arm, whatever it takes to remember:

    Package names can't match property names in your classes. Try to plan accordingly. If you accidentally create a conflict, the compiler might not be as helpful as you'd hope.

    Let this be a warning to you.