Files
swf-convertion/README.md
T
2026-03-24 18:14:48 -06:00

207 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SWF → MP4 Converter
This project was born from the desire to download a [Strong Bad email](https://homestarrunner.com/sbemails) SWF file, convert it to a video, prevent infinite loops at the end of the main video, and capture any "easter egg" content. Claude was used for the initial generation.
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
This script was intended for a Fedora 43 machine, adjust accordingly.
```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 (051, 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