My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Monday, January 04, 2010

Flash Satay Revisited

The most famous and W3 friendly technique to serve an SWF has been described in this excellent A List Apart Article ages ago. Unfortunately there are few problems with that technique, and being Flash Player still widely adopted, plus nobody has created yet a HTML5 friendly Satay method, here I am with this proposal/suggestion.

Satay Problems

There are at least a couple of problems with the good old Satay method:

  1. double SWF request, performed by almost every browser

  2. SWF impossible to cache, due necessary queryString for each different SWF so that classic c.swf will be required for each call

Object Tag Behavior

The Satay method is based over the assumption that an object tag, with a forced type, will help the browser to recognize the SWF.
At the same time, if Flash Player is not present or it has been disabled, Satay trick allows us to provide an alternative content inside the Object itself.
What we should be aware is that Internet Explorer won't execute any script tag inside the object.
At the same time, Internet Explorer and for some reason Opera as well, is the only one able to recognize and correctly execute the SWF without effort at all.
This is why my proposal relies into JavaScript, excluding Opera from the list of browser that should replace the object tag with the embed one.

Removed Data Property

The reason Satay loads two times the SWF, even if this is executed only once, is the data property. Once it is interpreted, Firefox and every other will understand the object itself and the classic "param name movie" will be re-evaluated. On the other hand, if there is a "param name movie" this is the one used by Internet Explorer to understand which movie should be loaded and interpreted via Flash Player plug-in.
Being this node available for every other browser as well, we can use its value to understand the movie replacing the whole object with an embed that will have the same src.

Alternative Content

If the Flash Player is not present/enabled the script won't perform anything except remove itself from the DOM since once executed, it does not need to do anything else. This means that the browser will simply show the alternative content being unable to understand what kind of object has been encountered, so we still have the nice layout fallback.

WebReflection Solution: HTML5 Layout Example

<!DOCTYPE html>
// generic function to initialize the SWF
function ASReady(msg) {
// IE + Others way to access the SWF exposed properties
(window["my-swf"] || document["my-swf"]).hello(msg);
<!--// regular object as if it is for IE/Opera only //-->
<object id="my-swf" type="application/x-shockwave-flash" width="43" height="72">
<param name="movie" value="alert.swf" />
<param name="FlashVars" value="msg=<?php echo rand(0, 100000); ?>" />
<!--// optional stuff to show if Flash Player is disabled //-->
<img src="alert.png" width="43" height="72" alt="" />
<!--// the tricky script, interpreted by every browser but IE //-->
<script src="satay2.min.js"></script>

Above layout passed without problems the W3 validator.
Please note that I am not using the classic query string to understand which file should be eventually loaded but a better FlashVars dedicated param node.
In this way the generic c.swf file can be reused without problems even via absolute url inclusion and finally cached directly via browser.

WebReflection Solution: satay2.js

(function (window) {
// (C) WebReflection - Mit Style License

// let's try to be future proof ...
"use strict";
for (var
// just ashortcut
type = "application/x-shockwave-flash",
// plugins property or ...
plugins = navigator.plugins,
// mimeTypes one
mimeTypes = navigator.mimeTypes,
// this operation should be performed only if it's not opera
enabledPlugin = !window.opera &&
// if plugins exists and "Shockwave Flash" is a valid property
(plugins && plugins["Shockwave Flash"]) ||
// or if mimeTypes plus $type exists and enabledPlugin is true
(mimeTypes && mimeTypes[type] && mimeTypes[type].enabledPlugin),
// grab the current script
script = document.getElementsByTagName("script"),
// set the current script as the last one (normal flow)
// find the parentNode (NOTE: quirks only)
object = (script = script[script.length - 1]).parentNode,
// create the element if necessary
embed = enabledPlugin && document.createElement("embed"),
// find out each param node, if necessary
param = enabledPlugin && object.getElementsByTagName("param"),
// set loop range
i = 0, length = enabledPlugin ? param.length : 0;
i < length; ++i
) {
// for each param, set relative embed tag property
embed.setAttribute(param[i].name === "movie" ? "src" : param[i].name, param[i].value);
// only if Flash player is present and enabled ...
if (enabledPlugin) {
// for each object attribute, set it into the embed node (id included, if any)
for (var attributes = object.attributes, i = 0, length = attributes.length; i < length; ++i) {
embed.setAttribute(attributes[i].name, attributes[i].value);
// replace the object with the embed node
object.parentNode.replaceChild(embed, object);
} else {
// remove this script, nothing to do

Via few checks and a couple of loops, above script, about 350 bytes minified and gzipped, will replace the object with the only element able to serve SWFs with browsers such: Firefox, Chrome, Safari, and few others.

WebReflection Solution: AS 2.0 Example

Just for this demo I have created an SWF which aim is to tell us if everything is working properly.

// dependencies
import flash.external.ExternalInterface;

// expose a hello callback to JavaScript.
// this callback accepts a string and it will perform a JavaScript alert
// with the text "Hello! " plus the received string.
ExternalInterface.addCallback("hello", this, function (msg:String):Void {"alert", "Hello! " + msg);

// initialize this SWF calling global JavaScript ASReady function.
// this.msg will be the variable set via FlashVars param
// for this demo, simply a random number generated via PHP"ASReady", this.msg);

// good old stop(); to avoid loops over the current frame

You can find a live demo in this page monitoring, or not, network traffic to discover that finally the SWF is requested only once rather than twice.
This should even speed up the execution thanks to non blocking embed.src nature and the missed, useless, second request to the server (a 304 that is still an extra pointless request).

As Summary

If we would like to make markup as clear as possible respecting validation and using all we can use to speed up layout execution, the only MUST for this method is a proper non quirk layout.
In any case, this was the purpose of Satay method as well so, if we liked that one, I am sure we can appreciate mine proposal.


Anonymous said...

You shouldn't call this satay2. That implies a sequel, version 2, upgraded, better. It was a beautiful, elegant piece of code - this is not. I am not reviewing your code, but the entirety of this post and it should be titled Flash Satay Encumbered.

Andrea Giammarchi said...

I love constructive comments!