203 lines
5.8 KiB
Markdown
203 lines
5.8 KiB
Markdown
# SWF → MP4 Converter
|
||
|
||
Converts SWF (Flash) files to MP4 videos on Fedora, with:
|
||
- **Automatic loop detection** — stops recording after 5 seconds of a repeated loop
|
||
- **Interactive branch capture** — records separate MP4s for each clickable path
|
||
- **Batch processing** — handles entire directories of SWF files
|
||
- **GUI interaction mapper** — click-record tool to find interaction coordinates
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
```
|
||
swf_converter/
|
||
├── convert.py # Main entry point — run this
|
||
├── recorder.py # FFmpeg + Ruffle orchestration
|
||
├── loop_detector.py # Frame-hash loop detection + FFmpeg freeze detection
|
||
├── interaction.py # xdotool click injection + interaction recording
|
||
├── config_manager.py # JSON config loading and validation
|
||
├── swf_inspector.py # Reads SWF binary headers (FPS, frame count, dimensions)
|
||
├── map_interactions.py # GUI tool to record interaction coordinates
|
||
├── install.sh # Fedora dependency installer
|
||
├── tests.py # Unit tests
|
||
└── configs/
|
||
└── swf_config.json # Per-file settings and interaction definitions
|
||
```
|
||
|
||
---
|
||
|
||
## Installation
|
||
|
||
```bash
|
||
bash install.sh
|
||
```
|
||
|
||
This installs: `ffmpeg`, `xdotool`, `ImageMagick`, `scrot`, `python3-tkinter`,
|
||
`Xvfb`, and downloads the latest **Ruffle** binary to `~/.local/bin/ruffle`.
|
||
|
||
---
|
||
|
||
## Quick Start
|
||
|
||
### 1. Inspect your SWF files
|
||
|
||
```bash
|
||
python3 convert.py --inspect *.swf
|
||
```
|
||
|
||
Shows version, FPS, frame count, dimensions, and estimated duration for each file.
|
||
|
||
### 2. Generate a starter config
|
||
|
||
```bash
|
||
python3 convert.py --generate-config *.swf
|
||
```
|
||
|
||
Creates `configs/swf_config.json` pre-populated with each SWF's metadata.
|
||
Edit it to add `end_seconds` and interaction points.
|
||
|
||
### 3. Map interaction points (optional)
|
||
|
||
For interactive SWFs, use the GUI tool to record where/when clicks happen:
|
||
|
||
```bash
|
||
python3 map_interactions.py my_interactive.swf
|
||
```
|
||
|
||
1. Click **Launch & Start** — opens the SWF in Ruffle and starts a timer
|
||
2. Watch the SWF — when an interactive moment appears, click **Record Click**
|
||
3. Coordinates are auto-detected from your mouse position
|
||
4. Click **Save Config** — writes interactions to `configs/swf_config.json`
|
||
|
||
### 4. Convert
|
||
|
||
```bash
|
||
# Single file
|
||
python3 convert.py my.swf
|
||
|
||
# Directory of SWF files
|
||
python3 convert.py ./swf_files/ -o ./output/
|
||
|
||
# Dry run (shows what would happen without recording)
|
||
python3 convert.py ./swf_files/ --dry-run
|
||
|
||
# Override loop grace period
|
||
python3 convert.py *.swf --loop-grace 8.0
|
||
```
|
||
|
||
---
|
||
|
||
## Output Structure
|
||
|
||
For a SWF called `lesson.swf` with two interaction points:
|
||
|
||
```
|
||
output/
|
||
├── lesson_base.mp4 ← Linear content, no clicks
|
||
├── lesson_button_yes.mp4 ← Branch: click "Yes" at t=12.5s
|
||
└── lesson_button_no.mp4 ← Branch: click "No" at t=12.5s
|
||
```
|
||
|
||
---
|
||
|
||
## Configuration Reference
|
||
|
||
```json
|
||
{
|
||
"_defaults": {
|
||
"emulator": "ruffle", // "ruffle" or "lightspark"
|
||
"loop_detection": "hash", // "hash", "freeze", or "none"
|
||
"loop_grace": 5.0, // Seconds after loop detected before stopping
|
||
"max_duration": 600.0, // Hard cap in seconds
|
||
"fps": 30, // Capture frame rate
|
||
"crf": 18, // FFmpeg quality (0–51, lower=better)
|
||
"audio": true, // Capture audio via PulseAudio
|
||
"startup_delay": 3.0 // Wait after launching emulator
|
||
},
|
||
|
||
"my_animation.swf": {
|
||
"end_seconds": 42.0, // Force-stop at this timestamp
|
||
"interactions": [
|
||
{
|
||
"t": 12.5, // Seconds from SWF start
|
||
"x": 320, // X coordinate in window
|
||
"y": 240, // Y coordinate in window
|
||
"label": "button_yes", // Output filename suffix
|
||
"button": 1, // 1=left, 2=middle, 3=right
|
||
"double": false // Double-click?
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Loop Detection Methods
|
||
|
||
| Method | How it works | Best for |
|
||
|---------|-------------|----------|
|
||
| `hash` | Compares MD5 hashes of downsampled screenshots every 0.4s. Detects repeated frame sequences. | Most animations |
|
||
| `freeze` | Runs FFmpeg `freezedetect` on the raw recording as post-processing. Catches visual stills. | Animations that fade to a static frame |
|
||
| `none` | No loop detection — relies on `end_seconds` or `max_duration`. | When you know exact duration |
|
||
|
||
All methods add a configurable **grace period** (default 5 seconds) after detection,
|
||
so the loop point itself is visible in the output.
|
||
|
||
---
|
||
|
||
## Headless / Server Use
|
||
|
||
If running without a desktop display:
|
||
|
||
```bash
|
||
# Start virtual display
|
||
Xvfb :99 -screen 0 1024x768x24 &
|
||
export DISPLAY=:99
|
||
|
||
# Start virtual audio sink
|
||
pulseaudio --start
|
||
pactl load-module module-null-sink sink_name=virtual
|
||
|
||
# Convert
|
||
python3 convert.py ./swf_files/ -o ./output/
|
||
```
|
||
|
||
---
|
||
|
||
## Running Tests
|
||
|
||
```bash
|
||
python3 -m pytest tests.py -v
|
||
# or
|
||
python3 tests.py
|
||
```
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
**"No Flash emulator found"**
|
||
→ Download Ruffle from https://github.com/ruffle-rs/ruffle/releases and place at `~/.local/bin/ruffle`
|
||
|
||
**"Could not find emulator window"**
|
||
→ Ensure a display is available (`echo $DISPLAY` should return `:0` or `:99`)
|
||
→ Try increasing `startup_delay` in config (some SWFs load slowly)
|
||
|
||
**Audio missing from output**
|
||
→ Ensure PulseAudio is running: `pulseaudio --start`
|
||
→ Or disable audio: set `"audio": false` in config
|
||
|
||
**Loop not detected**
|
||
→ Try `"loop_detection": "freeze"` for animations that end on a static frame
|
||
→ Or set `"end_seconds"` manually after inspecting the SWF
|
||
|
||
**SWF doesn't work in Ruffle**
|
||
→ Set `"emulator": "lightspark"` in config for that file
|
||
→ Install Lightspark: `sudo dnf install lightspark`
|
||
|
||
**xdotool clicks not registering**
|
||
→ Check window focus — the Ruffle window must be in the foreground
|
||
→ Use `map_interactions.py` to verify coordinates match what you see on screen
|