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 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
python3 convert.py --inspect *.swf
Shows version, FPS, frame count, dimensions, and estimated duration for each file.
2. Generate a starter config
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:
python3 map_interactions.py my_interactive.swf
- Click Launch & Start — opens the SWF in Ruffle and starts a timer
- Watch the SWF — when an interactive moment appears, click Record Click
- Coordinates are auto-detected from your mouse position
- Click Save Config — writes interactions to
configs/swf_config.json
4. Convert
# 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
{
"_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:
# 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
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