-
Notifications
You must be signed in to change notification settings - Fork 25
Polyfill Embedding
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.DirPlayerAPI - The
window.__dirplayerFlashConfigconfig object - How element detection works
- Accessing params from Lingo
- Gestures
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).
<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.
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).
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.
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
__dirplayerFlashConfigafter 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>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.
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 — everywindow.ruffle*bridge function early-returns on missing instance: setters returnfalse/void, getters returnnull/0/-1/falseas appropriate, andwasm_bindgenexterns markedcatchget a normal "no result"Ok(null)instead of a JS exception. - The
[Flash] disableFlash is set; skipping Ruffle instance for X:Ylog 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
RufflePlayersetup). - 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. Withdata-disable-flash, dirplayer-rs leaves that Ruffle completely alone — no double-load, noRufflePlayer.configmutation, 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 incore/src/avm1/object.rs,core/src/avm1/activation.rs, andcore/src/player.rsthat the Lingo↔Flash bridge depends on:
- A
triggerLingoCallbackOnScriptextern that fires when AS callsExternalInterface.call, routing into dirplayer-rs's WASM-side handler dispatcher (setCallbackflow). Stock Ruffle has no such extern.__dirplayer_ref_<N>object storage at_root.__dirplayer_ref_Nso Lingo'sFlashObjectRefproperty 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.jsscript tag) is not API-compatible with this bridge.getVariableof 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-flashon 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-flashis 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).
The polyfill exposes a small global API:
window.DirPlayer = {
init: () => void;
configureFlash: (config: FlashConfig) => void;
};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).
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>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).
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).
<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>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).
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.
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.
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.
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.
The polyfill replaces two markup patterns:
<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.
| 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. |
sw1 … sw30
|
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 sw1…sw30. |
data-enable-gestures |
Enable pan/zoom gestures (see Gestures). |
<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"ortype="application/x-shockwave-director" - A
<param name="src">value with a.dcr,.dxr, or.dirextension
name |
Description |
|---|---|
src |
URL of the movie (case-insensitive). |
sw1 … sw30
|
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>.
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-gestureson the<object>is inherited by the inner<embed>.
Both <embed> and <object> are scanned for sw1–sw30 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.
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 sw1…sw30 shorthand params use "sw1", "sw2", … as their keys.
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). |
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.
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.
-
Debug Console Commands — runtime
helpers exposed on
windowafter the player is up (listW3dMembers,exportW3dObj,downloadTraceLog,__vm.player_print_filmloop_sprites, …).