The trick with MOV-to-MP4: most modern MOVs already contain H.264 video, so MP4 conversion is just a container swap — instant, no quality loss. Re-encoding is only needed for ProRes, DNxHD, or other pro codecs. Detecting which case you're in is the difference between a 1-second conversion and a 5-minute one.
Method 1: ffmpeg-python with stream-copy (the fast path)
If the MOV's video and audio are already in MP4-compatible codecs (H.264 + AAC), -c copy just rewraps the streams — no re-encoding, no quality loss, takes seconds.
pip install ffmpeg-python
apt install ffmpeg # or brew install ffmpeg
import ffmpeg
import subprocess
import json
def get_codecs(in_path: str) -> tuple[str, str]:
"""Return (video_codec, audio_codec) for a media file."""
probe = ffmpeg.probe(in_path)
video = next((s for s in probe["streams"] if s["codec_type"] == "video"), None)
audio = next((s for s in probe["streams"] if s["codec_type"] == "audio"), None)
return (video["codec_name"] if video else "", audio["codec_name"] if audio else "")
def mov_to_mp4(in_path: str, out_path: str) -> None:
vcodec, acodec = get_codecs(in_path)
if vcodec in ("h264", "hevc") and acodec in ("aac", "mp3"):
# Fast path: stream copy
(
ffmpeg
.input(in_path)
.output(out_path, c="copy", movflags="+faststart")
.overwrite_output()
.run(quiet=True)
)
else:
# Slow path: re-encode
(
ffmpeg
.input(in_path)
.output(
out_path,
vcodec="libx264",
acodec="aac",
crf=23,
preset="medium",
movflags="+faststart",
)
.overwrite_output()
.run(quiet=True)
)
mov_to_mp4("recording.mov", "recording.mp4")
Three things to know:
- movflags=+faststart moves the MP4 metadata to the beginning so the file starts playing before fully downloaded. Always use this for web video.
- CRF (Constant Rate Factor) 18-28 — 18 is visually lossless, 23 is web-standard, 28 is small file. Lower = better quality.
- preset ultrafast → veryslow — slower presets give smaller files at the same quality. 'medium' is a good default.
Method 2: moviepy (ergonomic, always re-encodes)
moviepy wraps FFmpeg with a Pythonic API. Easier than ffmpeg-python for compositional work but always re-encodes — slower than stream-copy.
pip install moviepy
apt install ffmpeg
from moviepy.editor import VideoFileClip
def mov_to_mp4(in_path: str, out_path: str, crf: int = 23) -> None:
with VideoFileClip(in_path) as clip:
clip.write_videofile(
out_path,
codec="libx264",
audio_codec="aac",
ffmpeg_params=["-crf", str(crf), "-movflags", "+faststart"],
preset="medium",
threads=4,
)
mov_to_mp4("recording.mov", "recording.mp4")
moviepy is the right choice when you need to trim, concatenate, add audio, overlay text, etc. For pure container/codec conversion, ffmpeg-python is faster.
Method 3: ChangeThisFile API (no FFmpeg)
If you don't want FFmpeg in your container, the API runs it server-side. Free tier covers 1,000 conversions/month.
import requests
API_KEY = "ctf_sk_your_key_here"
def mov_to_mp4(in_path: str, out_path: str) -> None:
with open(in_path, "rb") as f:
response = requests.post(
"https://changethisfile.com/v1/convert",
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": f},
data={"source": "mov", "target": "mp4"},
timeout=300, # videos can take a while
)
response.raise_for_status()
with open(out_path, "wb") as out:
out.write(response.content)
mov_to_mp4("recording.mov", "recording.mp4")
The API auto-detects whether stream-copy is possible. For H.264 MOVs, conversion is near-instant. For ProRes or other pro codecs, it re-encodes server-side. Either way, output is web-ready (faststart, AAC audio).
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| ffmpeg-python (stream-copy path) | Most cases — instant for H.264 MOVs | Need to detect codec first |
| moviepy | When you need to edit (trim, overlay, concat) too | Always re-encodes — slower |
| ChangeThisFile API | No FFmpeg in your environment | Network call, file size limit (25MB free) |
Production tips
- Always probe first. Check video/audio codecs with ffprobe (or ffmpeg.probe) before deciding to re-encode. Stream-copy is 100x faster when it works.
- Use +faststart for web delivery. Without faststart, browsers wait for the entire MP4 to download before playing. With it, playback starts after the first chunk.
- CRF 23 is the right web default. Visually indistinguishable from source for most content. Drop to 20 for archival; bump to 28 for tiny files.
- preset='medium' is the right default. 'fast' for live processing; 'slow' or 'veryslow' for once-and-done archival. Each preset roughly doubles encode time and saves ~15% file size.
- Bound concurrency. FFmpeg pegs all cores during encoding. For batch jobs, run one or two conversions at a time per host.
- Set a long timeout. A 1GB video can take 5-10 minutes to re-encode. 300s minimum on the API client.
For most MOV files (iPhone recordings, screen captures), stream-copy converts in seconds without quality loss. For ProRes or pro codecs, re-encode at CRF 23. For environments without FFmpeg, the API. Free tier covers 1,000 conversions/month.