Files

325 lines
8.0 KiB
Markdown

# 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:
```json
// package.json
{
"scripts": {
"postinstall": "node copy-ruffle.js"
},
"dependencies": {
"ruffle-mirror": "^0.1.0"
}
}
```
```javascript
// 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
```typescript
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
```typescript
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
```typescript
let mounted = true;
useEffect(() => {
// ... initialization
return () => {
mounted = false;
// ... cleanup
};
}, [swfUrl]);
```
### 2. Initialization Lock
```typescript
const isInitializingRef = useRef(false);
if (isInitializingRef.current) {
console.log('[Ruffle] Already initializing, skipping');
return;
}
isInitializingRef.current = true;
```
### 3. Delay Before Initialization
```typescript
// Wait for any previous cleanup to complete
await new Promise((resolve) => setTimeout(resolve, 100));
if (!mounted) return;
```
### 4. Cleanup Existing Player
```typescript
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
```typescript
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
```typescript
{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
```typescript
{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
```typescript
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](https://ruffle.rs/#compatibility) for details.
## Further Reading
- [Ruffle Official Website](https://ruffle.rs/)
- [Ruffle GitHub](https://github.com/ruffle-rs/ruffle)
- [GamePlayer Component](../components/player-components.md)