8.0 KiB
Ruffle Player Implementation
Comprehensive documentation for the Ruffle Flash emulator component.
Overview
Ruffle is a Flash Player emulator written in Rust and compiled to WebAssembly. It allows Flash (.swf) content to run in modern browsers without plugins.
Location: frontend/src/components/player/RufflePlayer.tsx
Architecture
RufflePlayer Component
├── Script Loading (GET /ruffle/ruffle.js)
├── Player Initialization (RufflePlayer.newest())
├── Configuration (scale, quality, etc.)
├── Cleanup Strategy (destroy on unmount)
└── UI States (loading, error, success)
/ruffle is served from different places depending on how the app is
running. In development, Vite serves frontend/public/ruffle directly. In a
container, Ruffle is installed into config.ruffleDataPath — /app/data/ruffle
by default, overridable with RUFFLE_DATA_PATH — on the persisted data
volume, and Express serves that directory at /ruffle ahead of the copy
baked into frontend/dist. This is what lets an admin's in-app Ruffle update
survive the container being recreated: the persisted install shadows the
bundled one instead of being overwritten by it.
Installation
Ruffle files are copied from npm package during installation:
// package.json
{
"scripts": {
"postinstall": "node copy-ruffle.js"
},
"dependencies": {
"ruffle-mirror": "^0.1.0"
}
}
// copy-ruffle.js
const fs = require('fs-extra');
const path = require('path');
const source = path.join(__dirname, 'node_modules', 'ruffle-mirror');
const dest = path.join(__dirname, 'public', 'ruffle');
fs.copySync(source, dest);
console.log('✅ Ruffle files copied to public/ruffle/');
Component Props
export interface RufflePlayerProps {
swfUrl: string; // URL to .swf file
width?: string | number; // Player width (default: '100%')
height?: string | number; // Player height (default: '100%')
className?: string; // Additional CSS classes
onLoadError?: (error: Error) => void; // Error callback
onLoadSuccess?: () => void; // Success callback
}
Ruffle Configuration
player.config = {
autoplay: 'auto', // Auto-start playback
backgroundColor: '#000000', // Black background
letterbox: 'on', // Add letterboxing for aspect ratio
unmuteOverlay: 'visible', // Show unmute button if needed
contextMenu: true, // Enable right-click menu
showSwfDownload: false, // Hide download option
upgradeToHttps: window.location.protocol === 'https:',
warnOnUnsupportedContent: true,
logLevel: 'error', // Only log errors
publicPath: '/ruffle/', // Path to Ruffle files
scale: 'showAll', // Show all content, maintain aspect
forceScale: true, // Prevent runtime scale changes
quality: 'high', // High quality rendering
allowScriptAccess: 'sameDomain',
salign: '', // Center alignment
wmode: 'opaque', // Opaque mode
};
Scale Modes
Ruffle supports multiple scale modes:
showAll (Default)
- Shows entire content
- Maintains aspect ratio
- Adds letterboxing if needed
- Best for most games
noscale
- No scaling
- 1:1 pixel mapping
- May cut off content
exactfit
- Scales to fill container
- Ignores aspect ratio
- May distort content
noborder
- Scales to fill container
- Maintains aspect ratio
- Crops content if needed
Cleanup Strategy
The component implements a robust cleanup strategy to prevent memory leaks:
1. Mount Flag
let mounted = true;
useEffect(() => {
// ... initialization
return () => {
mounted = false;
// ... cleanup
};
}, [swfUrl]);
2. Initialization Lock
const isInitializingRef = useRef(false);
if (isInitializingRef.current) {
console.log('[Ruffle] Already initializing, skipping');
return;
}
isInitializingRef.current = true;
3. Delay Before Initialization
// Wait for any previous cleanup to complete
await new Promise((resolve) => setTimeout(resolve, 100));
if (!mounted) return;
4. Cleanup Existing Player
if (rufflePlayerRef.current) {
const oldPlayer = rufflePlayerRef.current;
rufflePlayerRef.current = null;
if (typeof oldPlayer.destroy === 'function') {
oldPlayer.destroy();
} else {
oldPlayer.remove();
}
// Wait for Ruffle's internal cleanup
await new Promise((resolve) => setTimeout(resolve, 200));
}
5. Unmount Cleanup
return () => {
mounted = false;
isInitializingRef.current = false;
const playerToCleanup = player || rufflePlayerRef.current;
if (playerToCleanup) {
if (typeof playerToCleanup.destroy === 'function') {
playerToCleanup.destroy();
} else {
playerToCleanup.remove();
}
}
rufflePlayerRef.current = null;
// Clear container
if (containerRef.current) {
while (containerRef.current.firstChild) {
containerRef.current.removeChild(containerRef.current.firstChild);
}
}
};
Loading States
Loading UI
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-75 z-10">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500 mx-auto mb-4"></div>
<p className="text-white">Loading Flash game...</p>
<p className="text-xs text-gray-400 mt-2">Powered by Ruffle</p>
</div>
</div>
)}
Error UI
{error && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90 z-10">
<div className="text-center max-w-md p-6">
<div className="text-red-500 text-5xl mb-4">⚠️</div>
<h3 className="text-xl font-bold text-white mb-2">Failed to Load Game</h3>
<p className="text-gray-300 mb-4">{error}</p>
<div className="bg-gray-800 rounded p-3 mb-4">
<p className="text-sm text-gray-400 text-left">
<strong>URL:</strong> {swfUrl}
</p>
</div>
<p className="text-sm text-gray-400">
This game may require additional files or may not be compatible with Ruffle yet.
</p>
</div>
</div>
)}
Usage Example
import { RufflePlayer } from '@/components/player/RufflePlayer';
function FlashGame() {
return (
<RufflePlayer
swfUrl="/proxy/games/example-game.swf"
width="100%"
height="600px"
onLoadSuccess={() => {
console.log('Game loaded successfully');
}}
onLoadError={(error) => {
console.error('Failed to load game:', error);
}}
/>
);
}
Common Issues
Issue: Game doesn't scale properly
Solution: Use scale: 'showAll' and forceScale: true
Issue: Multiple player instances
Solution: Component includes cleanup delays and initialization locks
Issue: Memory leaks on navigation
Solution: Component destroys player and clears container on unmount
Issue: Black screen on load
Solution: Check browser console for errors, ensure SWF URL is correct
Browser Console Logging
The component includes extensive logging for debugging:
[Ruffle] Waiting for any previous cleanup to complete...
[Ruffle] Cleaning up existing player instance
[Ruffle] Waiting for cleanup to complete...
[Ruffle] Loading SWF: /proxy/games/example.swf
[Ruffle] SWF loaded successfully
[Ruffle] Cleanup: Component unmounting
[Ruffle] Cleanup: Removing player instance
Performance Considerations
- Initial Load: WebAssembly compilation on first load
- Memory: Depends on SWF complexity (typically 50-200MB)
- CPU: Real-time emulation requires modern CPU
- Compatibility: Some ActionScript 3 features may not be supported
Ruffle Limitations
Not all Flash content is supported:
- Some ActionScript 3 features are incomplete
- 3D content may have issues
- Video playback may be limited
- Some external API calls won't work
Check Ruffle Compatibility for details.