# 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