StreamMuxer follows an MVVM-like structure with a clear separation between the UI and the FFmpeg integration:
- Views: SwiftUI components for interaction and display
- ViewModel: UI state + orchestration of probe/export actions
- Services: process integration with
ffprobeandffmpeg - Models: typed representation of files, streams, capabilities, per-stream settings and export sessions
Configuration never becomes a shell string. The flow is:
UI
↓
StreamSettings (per stream) + ExportSession
↓
FFmpegCommandBuilder → BuiltCommand (executable + [String] arguments)
↓
FFmpegRunner (Process, async)
FFmpegPaths— resolvesffmpeg/ffprobe(app bundle → Homebrew dirs →which).FFmpegCapabilityScanner— startup environment check: versions plusffmpeg -encoders. The UI only offers encoders that are actually present (including VideoToolbox hardware encoders).ProbeService—ffprobe -show_format -show_streams -show_chapters -print_format json, mapped intoMediaStream/MediaFilemodels.FFmpegCommandBuilder— turns anExportSessioninto structured arguments:-mapper included stream, per-stream codec args (-c:v:0,-crf:v:0,-q:v:0,-b:a:1…), container-aware subtitle handling and validation. Also produces the human-readable command preview.FFmpegRunner— runs the job asynchronously, parses-progress pipe:1key/value blocks intoTranscodeProgress(fraction, speed, fps, ETA), captures the full stderr log, and supports cancellation viaProcess.terminate().
- On launch,
FFmpegCapabilityScannerchecks the environment. If FFmpeg/FFprobe are missing, a banner explains what's missing and exports are disabled. - User drops/selects media files.
StreamMuxerViewModelschedules probing withProbeService.- Parsed streams are mapped into
MediaStreammodels; every stream defaults to Copy (no implicit destructive operations). - User chooses per-stream: Copy / Transcode (codec + encoder + quality + speed preset) / Discard.
FFmpegCommandBuildervalidates the configuration against the target container and builds the command (also shown as a preview).FFmpegRunnerexecutes ffmpeg; progress (percentage, processed time, speed, fps, ETA) is streamed back to the UI. The user can cancel; partial output is removed.- On failure, a simplified error is shown and the full FFmpeg log is available via "Show FFmpeg Log".
The UI exposes quality levels (Higher quality / Balanced / Smaller file), not bitrates. Each VideoEncoderInfo translates the level into its own mechanism:
libx264/libx265→ CRF +-presetlibsvtav1→ CRF + numeric presetlibaom-av1→ CRF +-b 0+-cpu-usedh264_videotoolbox/hevc_videotoolbox→-q:v(1–100 scale)
HEVC output into MP4/MOV is tagged hvc1 for Apple-player compatibility.
- MP4/MOV: text subtitles (SRT/ASS/WebVTT…) auto-convert to
mov_text; bitmap subtitles (PGS/VobSub) and attachment/data streams are rejected with a clear message before export. - MKV: everything copies;
mov_textconverts to SRT.
Before export, included streams are sorted in fixed groups:
- video
- audio
- subtitle
- other
This ensures predictable output stream indexes regardless of selection order.
Default output URL suggestion is generated from the first contributing source file:
- Same directory as original file
- Filename suffix
_conf - Extension follows the selected output container
Example:
episode01.mkv+ MKV output ->episode01_conf.mkv
The app uses startAccessingSecurityScopedResource() where needed when reading input/output URLs via system file pickers; access is held for the duration of an export.
- Missing binaries (
ffprobe/ffmpeg) produce a persistent banner and disable exports. - Probe errors surface inline per file section in the stream list.
- Configuration errors (container incompatibilities) surface in the export panel before running anything.
- Export errors are shown via alert with a simplified message; the full FFmpeg stderr log is available in a sheet.
- Cancelled exports remove the partial output file.