Skip to content

Polyfill Embedding

chameleonxxl edited this page May 18, 2026 · 3 revisions

dirplayer-rs ships a polyfill bundle (polyfill/src/standalone.tsx) that you can drop into any HTML page to transparently replace Shockwave/Director <embed> and <object> elements with the dirplayer-rs runtime. It's the recommended way to keep legacy .dcr pages working — the page's existing <embed src="movie.dcr"> markup is detected and swapped for a React-mounted player without touching the host site's HTML.

This page documents:

  • Quick start
  • The script-tag attributes you can put on the polyfill
  • The window.DirPlayer API
  • The window.__dirplayerFlashConfig config object
  • How element detection works
  • Accessing params from Lingo
  • Gestures

Quick start

Add the polyfill script to your page, then use <embed> or <object> as you normally would:

<script src="/path/to/dirplayer-polyfill.js"></script>

<embed
  src="/movies/game.dcr"
  type="application/x-director"
  width="640"
  height="480"
/>

The polyfill auto-initializes on page load. It detects Director embeds by MIME type (application/x-director, application/x-shockwave-director) and file extension (.dcr, .dxr, .dir).


Including the polyfill

<script src="dirplayer-polyfill.js"></script>

By default it auto-initialises on DOMContentLoaded, walks the DOM, and replaces every .dcr <embed> and every <object> with classid="clsid:166B1BCA-3F9C-11CF-8075-444553540000" (the legacy Shockwave Director ActiveX class id) with a dirplayer-rs player. A MutationObserver keeps watching, so dynamically inserted Director elements are picked up too.

<noscript>-wrapped Director elements (a common pattern when the page gates legacy plugin content) are also extracted into the live DOM before scanning.


Script-tag attributes

These go on the <script> tag that loads the polyfill. They're read synchronously from document.currentScript, so they only work if the polyfill is loaded as an external script (not inlined / dynamically injected).

data-require-click

Boolean attribute (presence = enabled). When set, the player renders a click-to-play overlay instead of starting the movie immediately. The movie loads + initialises only after the user clicks. Useful for pages that need to defer audio context creation, network sockets, or WASM init until a user gesture (e.g. for autoplay-policy compliance, or to avoid loading multiple movies on a hub page).

<script src="dirplayer-polyfill.js" data-require-click></script>

Internally it sets requireClickToPlay: true on the polyfill config, which is forwarded to the EmbedPlayer component.

data-manual-init

Boolean attribute (presence = enabled). When set, the polyfill does not auto-initialise on DOMContentLoaded — you have to call window.DirPlayer.init() yourself. Useful when:

  • You want to set __dirplayerFlashConfig after the polyfill script loads (e.g. because the config comes from a later async source)
  • You want to delay the DOM walk until your own bootstrap finishes
  • You're embedding the polyfill in a host that needs to control init ordering
<script src="dirplayer-polyfill.js" data-manual-init></script>
<script>
  window.__dirplayerFlashConfig = { /* ... */ };
  window.DirPlayer.init();
</script>

data-ruffle-url

Custom URL string for the bundled Ruffle script. By default the polyfill loads ruffle/ruffle.js relative to its own URL; this attribute overrides that lookup.

<script src="https://cdn.example.com/dirplayer-polyfill.js"
        data-ruffle-url="/static/ruffle/ruffle.js"></script>

Use it when you serve Ruffle from a different path than the polyfill, or when you've pinned to a specific Ruffle version.

If Ruffle fails to load, the polyfill continues to initialise dirplayer-rs but Flash content will not work; a warning is logged.

data-disable-flash

Boolean attribute (presence = enabled). When set, the polyfill skips loading Ruffle entirely — no <script> tag is injected, no network request for ruffle/ruffle.js, no RufflePlayer global created.

<script src="dirplayer-polyfill.js" data-disable-flash></script>

Internally this writes disableFlash: true into __dirplayerFlashConfig so that createFlashInstance short-circuits cleanly when the WASM side encounters a Flash member.

Safety guarantees:

  • Movies that have no Flash cast members run identically to a Flash-enabled build.
  • Movies that do have Flash cast members still load and run; the Flash sprites just stay invisible / inert. No errors are thrown.
  • Lingo calls into Flash sprites (sprite(N).gotoFrame(2), getVariable, callFunction, etc.) are safe no-ops — every window.ruffle* bridge function early-returns on missing instance: setters return false / void, getters return null / 0 / -1 / false as appropriate, and wasm_bindgen externs marked catch get a normal "no result" Ok(null) instead of a JS exception.
  • The [Flash] disableFlash is set; skipping Ruffle instance for X:Y log line is printed once per Flash member when the WASM side tries to instantiate it — useful as a positive confirmation that Flash was requested and skipped.

Use it when:

  • You're hosting a movie that has zero Flash cast members and want to trim startup time + bundle size (no Ruffle download, no script parse, no RufflePlayer setup).
  • The page CSP / sandbox forbids loading Ruffle's WASM and you want dirplayer-rs to keep working for non-Flash content.
  • You're debugging and want to bisect whether a runtime issue is inside Ruffle or inside dirplayer.
  • The host page already has Ruffle (via the official browser extension, or its own <script src="ruffle.js">) for non-Director Flash content. With data-disable-flash, dirplayer-rs leaves that Ruffle completely alone — no double-load, no RufflePlayer.config mutation, no fight over <embed> tags.

Co-existence with an external Ruffle (important caveat):

dirplayer-rs requires its bundled Ruffle fork — not stock Ruffle.

Our ruffle/ directory is a custom Ruffle build with patches in core/src/avm1/object.rs, core/src/avm1/activation.rs, and core/src/player.rs that the Lingo↔Flash bridge depends on:

  • A triggerLingoCallbackOnScript extern that fires when AS calls ExternalInterface.call, routing into dirplayer-rs's WASM-side handler dispatcher (setCallback flow). Stock Ruffle has no such extern.
  • __dirplayer_ref_<N> object storage at _root.__dirplayer_ref_N so Lingo's FlashObjectRef property chains can resurface AS-returned objects. Stock Ruffle returns objects that go out of scope after the call.
  • dirplayer-specific player methods (getVariable, SetVariable, CallFunction) with extended object-handle semantics.

A stock Ruffle (from the official browser extension or a generic ruffle.js script tag) is not API-compatible with this bridge. getVariable of primitives may still work, but anything involving AS→Lingo callbacks, returned object references, or method chaining will silently break.

The Ruffle browser extension (and standalone ruffle.js setups) only touch DOM elements with the Adobe Flash classid D27CDB6E-AE6D-11cf-96B8-444553540000 and <embed type="application/x-shockwave-flash">. dirplayer-rs's polyfill only touches Director's .dcr <embed> elements and the Director classid 166B1BCA-3F9C-11CF-8075-444553540000. There's no DOM-level overlap — both can coexist on the same page for non-Director Flash content.

The conflict is at the window.RufflePlayer global: there's only one. Whichever Ruffle script loads last wins.

Ruffle source on the page Without data-disable-flash With data-disable-flash
None Polyfill loads its bundled (custom) ruffle/ruffle.js. Director Flash works. Polyfill skips loading. Lingo Flash calls no-op.
Extension already injected (stock Ruffle) Polyfill currently detects window.RufflePlayer and skips re-loading → dirplayer falls back on stock Ruffle → Lingo Flash callbacks / object refs silently break. Polyfill leaves the extension's Ruffle alone. Director Flash inert; page-level Flash (handled by extension) keeps working.
User loaded their own stock ruffle.js Same broken state as above. Same — extension/stock Ruffle keeps working for page-level Flash.
User loaded our custom ruffle build manually Polyfill detects it and reuses it; everything works. Polyfill skips Ruffle initialisation entirely.

Recommendation:

  • If your page has the Ruffle extension or another stock Ruffle loaded, set data-disable-flash on dirplayer-rs's polyfill. You lose Director Flash content but everything else (and the extension's page-level Flash handling) keeps working cleanly.
  • If you control the page and want Director Flash to work, don't load any other Ruffle — let dirplayer-rs's polyfill load its bundled custom build.
  • We don't currently auto-detect a stock-vs-custom mismatch. If you see Lingo Flash code silently misbehave on a page that has an extension installed, that's the most likely explanation — data-disable-flash is the supported escape hatch.

The flag can also be set programmatically (see __dirplayerFlashConfig.disableFlash) — useful when you can't put attributes on the <script> tag (e.g. dynamic injection, Webpack-emitted bundle).


window.DirPlayer

The polyfill exposes a small global API:

window.DirPlayer = {
  init: () => void;
  configureFlash: (config: FlashConfig) => void;
};

DirPlayer.init()

Triggers the polyfill init flow manually. Only useful when the polyfill script tag has data-manual-init. Calling it more than once is a no-op (the polyfill has its own deferral / dedup logic — see the version-negotiation section below).

DirPlayer.configureFlash(partial)

Merges a partial FlashConfig into window.__dirplayerFlashConfig (shallow merge, so individual fields can be added without clobbering the rest). Equivalent to assigning to window.__dirplayerFlashConfig directly, except it also wires up the global socket-URL resolver (window.dirplayerResolveSocketUrl) used by the Multiuser Xtra on the WASM side.

window.DirPlayer.configureFlash({
  socketProxy: [
    { host: '127.0.0.1', port: 9000, proxyUrl: 'ws://127.0.0.1/ws-flash' }
  ],
});

To configure Flash before auto-init fires, call configureFlash() before the polyfill loads, or use data-manual-init and call it before window.DirPlayer.init():

<script src="/polyfill/dirplayer-polyfill.js" data-manual-init></script>
<script>
  window.DirPlayer.configureFlash({
    disableFlash: false,
    socketProxy: [
      { host: "game.example.com", port: 1626, proxyUrl: "wss://proxy.example.com/mus" }
    ],
    fetchRewriteRules: [
      {
        pathPrefix: "/gateway/",
        targetHost: "api.example.com",
        targetPort: "443",
        targetProtocol: "https:"
      }
    ]
  });
  window.DirPlayer.init();
</script>

window.__dirplayerFlashConfig

A page-level config object that controls how the embedded Ruffle Flash runtime resolves Lingo socket / fetch calls and which renderer it uses. Read by src/services/flashPlayerManager.ts.

Set it before the polyfill auto-initialises (i.e. before the polyfill <script> tag, or with data-manual-init and before the explicit DirPlayer.init() call).

Shape

interface FlashManagerConfig {
  socketProxy: Array<{
    host: string;        // Lingo's `connection.info.host`
    port: number;        // Lingo's `connection.info.port`
    proxyUrl: string;    // ws:// URL the bridge is listening on
  }>;
  fetchRewriteRules: Array<{
    pathPrefix: string;     // matched against the URL path
    targetHost: string;
    targetPort: number;
    targetProtocol: string; // "http" | "https"
  }>;
  renderer: string;     // Ruffle renderer hint, typically "canvas"
  logLevel: string;     // Ruffle log level, e.g. "warn" | "info"
  disableFlash: boolean; // skip Ruffle entirely; Lingo Flash calls become no-ops
}

All fields are optional; the polyfill merges shallowly with defaults (renderer: "canvas", logLevel: "info" on the Ruffle side).

Example

<script>
  window.__dirplayerFlashConfig = {
    socketProxy: [
      { host: '127.0.0.1', port: 9000, proxyUrl: 'ws://127.0.0.1/ws-flash' }
    ],
    renderer: 'canvas',
    logLevel: 'warn',
  };
</script>
<script src="dirplayer-polyfill.js"></script>

Field reference

socketProxy

When Lingo opens a socket via the Multiuser Xtra (e.g. connection.connect("127.0.0.1", 9000)), dirplayer-rs looks up the requested host:port in this list and connects to the matching proxyUrl instead. This is how raw TCP-style Lingo sockets work in a browser: a small WebSocket-to-TCP bridge runs server-side and the movie connects to it transparently.

Multiple entries are allowed for multi-server games (e.g. Habbo's info host + game host on different ports).

If a host:port isn't in the list, the connection silently fails (the resolver returns an empty string).

fetchRewriteRules

When Lingo / Flash issues an HTTP request whose URL path starts with pathPrefix, dirplayer-rs rewrites the host / port / protocol to the configured target. Useful when a movie hard-codes URLs that aren't reachable from the dev page (e.g. an old Shockwave server name) — you redirect them to a local mirror without patching the movie.

Matching is "first prefix wins" — order rules from most-specific to least-specific.

renderer

Passed through to Ruffle as its renderer config option. dirplayer-rs forces "canvas" internally so it can read pixels back from Flash sprites; setting it explicitly here makes intent visible and survives upstream Ruffle config merges.

logLevel

Ruffle's log level. Use "warn" for production-ish output, "info" or "debug" while diagnosing Flash issues. Doesn't affect dirplayer-rs's own logging — only Ruffle.

disableFlash

When true, dirplayer-rs never asks Ruffle to instantiate Flash cast members. The polyfill itself stops injecting Ruffle's <script> tag, and createFlashInstance() short-circuits with an informational log. Equivalent to the data-disable-flash script-tag attribute, exposed here for cases where you can't add attributes (e.g. dynamic script injection):

<script>
  window.__dirplayerFlashConfig = { disableFlash: true };
</script>
<script src="dirplayer-polyfill.js"></script>

Lingo Flash calls (gotoFrame, getVariable, callFunction, …) remain safe — every window.ruffle* bridge function detects the missing instance and returns a sentinel value (null, 0, -1, false, or void). Movies that don't depend on Flash content run identically; movies that do have Flash content keep loading without errors, but the Flash sprites stay inert.


Element detection

The polyfill replaces two markup patterns:

<embed>

<embed
  src="/movies/game.dcr"
  type="application/x-director"
  width="640"
  height="480"
  sw1="paramValue"
  sw2="anotherValue"
  data-enable-gestures
/>

Any <embed> whose src (or data-src) ends in .dcr, .dxr, or .dir, or whose type is application/x-director or application/x-shockwave-director, is replaced. Width and height come from the element's width/height attributes.

When the <embed> is wrapped inside an <object> (the classic IE/ Netscape fallback pattern), the parent <object> is replaced instead, and width/height fall back to the <object> if the <embed> doesn't set them.

<embed> attribute reference

Attribute Description
src URL of the Director movie. Also accepted as data-src (useful when the browser would otherwise try to load the plugin).
type MIME type. Use application/x-director or application/x-shockwave-director.
width, height Dimensions of the player. Numeric values are interpreted as pixels.
sw1sw30 External parameters passed to the movie (Director's classic externalParamValue() API). Numbered consecutively from 1; the loop stops at the first missing number.
data-sw-<name> Named external parameter. For example, data-sw-mode="game" makes externalParamValue("mode") return "game". These are merged with sw1sw30.
data-enable-gestures Enable pan/zoom gestures (see Gestures).

<object>

<object
  classid="clsid:166B1BCA-3F9C-11CF-8075-444553540000"
  type="application/x-director"
  width="640"
  height="480"
  data-enable-gestures
>
  <param name="src" value="/movies/game.dcr" />
  <param name="sw1" value="paramValue" />
  <param name="sw2" value="anotherValue" />
  <param name="enableGestures" value="true" />
  <!-- Fallback for non-plugin browsers -->
  <embed src="/movies/game.dcr" type="application/x-director"
         width="640" height="480" />
</object>

Detection fires on any <object> that satisfies one of:

  • classid="clsid:166B1BCA-3F9C-11CF-8075-444553540000" (Shockwave Director, case-insensitive)
  • classid="clsid:7FD1D18D-7787-11D2-B3F7-00600832B7C6" (Director 7+)
  • type="application/x-director" or type="application/x-shockwave-director"
  • A <param name="src"> value with a .dcr, .dxr, or .dir extension

<param> reference

name Description
src URL of the movie (case-insensitive).
sw1sw30 External parameters (same semantics as <embed>).
enableGestures Set to "true" to enable pan/zoom gestures.

You can also put data-sw-<name> attributes directly on the <object> element to pass named external params, just like with <embed>.

Nested <embed> inside <object>

The classic cross-browser pattern nests an <embed> inside an <object>. DirPlayer handles this correctly:

  • When a matching <embed> is found inside an <object>, the entire <object> is replaced (not just the <embed>).
  • Width and height from the <object> take precedence over those on the inner <embed>.
  • data-enable-gestures on the <object> is inherited by the inner <embed>.

External params

Both <embed> and <object> are scanned for sw1sw30 attributes / params. These are forwarded as external parameters to the movie (Lingo's the externalParamName / the externalParamValue). This matches the original Shockwave plugin's external-param convention.


Accessing params from Lingo

External params appear in Director's standard API:

-- Number of params passed by the host page
put externalParamCount()

-- Get the name of param at 1-based index
put externalParamName(1)   -- "sw1", or custom name

-- Get value by name (case-insensitive) or 1-based index
put externalParamValue("sw1")
put externalParamValue(1)

Named params (data-sw-* / <param> with arbitrary names) are accessible by their exact names. The sw1sw30 shorthand params use "sw1", "sw2", … as their keys.

Reserved param names

DirPlayer reads these params itself; they are also visible to Lingo via externalParamValue():

Param Description
_runMode Overrides the value of runMode (e.g. fake "Projector" or "Author" for movies that gate on the hosting environment). See Movie Loading Parameters.
_moviePath Overrides the value of the moviePath / the movieName — useful for movies that hard-check their host site. Label-only, no URL rewrite. See Movie Loading Parameters.
multiuser_websocket_path Path suffix appended to the WebSocket URL for Multiuser connections (see Multiuser).
multiuser_websocket_ssl Force SSL for Multiuser WebSocket connections ("true" / "1", see Multiuser).

Gestures

When data-enable-gestures is present on the <embed> or <object> element (or enableGestures="true" in a <param>), the player gains pan and zoom capabilities:

Interaction Action
Trackpad pinch Zoom in/out, anchored at the pinch centroid
Trackpad two-finger drag Pan the stage
Middle mouse button drag Pan the stage
Pan toggle button (overlay) Lock into single-finger/mouse pan mode
Minimap (overlay) Shows current viewport position; drag to navigate

The pan/zoom state is maintained independently of the movie's own coordinate system. The stage is auto-centered at its natural size until the user deliberately pans or zooms, after which the position is sticky.


Version negotiation

Multiple copies of dirplayer-rs may try to initialise on the same page (e.g. the polyfill loaded by the host page and the Chrome extension's content script). The polyfill avoids that with a small DOM-attribute handshake on <html>:

Attribute Set by Purpose
data-dirplayer-version candidate Semver of the candidate
data-dirplayer-source candidate "polyfill" or "extension"
data-dirplayer-initialized winner Set to "true" once init starts

Higher version wins. On a tie, polyfill wins over extension. Once data-dirplayer-initialized is set, no further candidates can register. Console logs make the negotiation visible:

[DirPlayer] polyfill v1.2.3 takes priority over extension v1.2.0
[DirPlayer] Initializing with polyfill v1.2.3

DOM attributes are used (instead of window globals) because the extension's content script runs in an isolated world and cannot share window with the page — but both worlds see the same DOM.


See also

  • Debug Console Commands — runtime helpers exposed on window after the player is up (listW3dMembers, exportW3dObj, downloadTraceLog, __vm.player_print_filmloop_sprites, …).