MOV is Apple's QuickTime container format — universally supported on Mac, but often problematic on Windows and Android browsers. Converting to MP4 (H.264 + AAC) gives you universal browser and device compatibility. In .NET, this is an FFmpeg problem: there is no managed .NET library that transcodes H.265/HEVC or ProRes (common MOV codecs) without calling FFmpeg. Xabe.FFmpeg is the standard .NET wrapper.

Method 1: Xabe.FFmpeg (H.264 output, typed .NET API)

Xabe.FFmpeg provides a fluent typed API over FFmpeg. Configure video codec, CRF (quality), audio codec, and resolution through C# properties — no raw argument strings required for most use cases.

dotnet add package Xabe.FFmpeg
dotnet add package Xabe.FFmpeg.Downloader  # optional: auto-download FFmpeg
using Xabe.FFmpeg;
using Xabe.FFmpeg.Downloader;

public class MovToMp4Service
{
    public MovToMp4Service()
    {
        // Point to FFmpeg if not on PATH:
        // FFmpeg.SetExecutablesPath("/usr/local/bin");
    }

    /// <summary>Converts MOV to MP4 using H.264 video and AAC audio.</summary>
    /// <param name="crf">Constant Rate Factor: 18=near-lossless, 23=default, 28=smaller file.</param>
    public async Task ConvertAsync(
        string inputPath,
        string outputPath,
        int crf = 23,
        CancellationToken ct = default)
    {
        var mediaInfo = await FFmpeg.GetMediaInfo(inputPath, ct);

        var videoStream = mediaInfo.VideoStreams.FirstOrDefault();
        var audioStream = mediaInfo.AudioStreams.FirstOrDefault();

        var conversion = FFmpeg.Conversions.New()
            .SetOutput(outputPath)
            .SetOverwriteOutput(true)
            // Use fast start for progressive web playback
            .AddParameter("-movflags faststart");

        if (videoStream != null)
        {
            videoStream
                .SetCodec(VideoCodec.h264)
                .SetBitrate(videoStream.Bitrate);  // preserve source bitrate

            // CRF controls quality: lower = better quality + bigger file
            conversion.AddStream(videoStream)
                      .AddParameter($"-crf {crf}");
        }

        if (audioStream != null)
        {
            audioStream.SetCodec(AudioCodec.aac);
            conversion.AddStream(audioStream);
        }

        await conversion.Start(ct);
    }

    /// <summary>Download FFmpeg binaries if not installed.</summary>
    public static async Task EnsureFFmpegAsync(string dir)
    {
        FFmpeg.SetExecutablesPath(dir);
        await FFmpegDownloader.GetLatestVersion(FFmpegVersion.Official, dir);
    }
}

// Usage
var service = new MovToMp4Service();
await service.ConvertAsync("clip.mov", "clip.mp4", crf: 23);

CRF guide: 18 = near-lossless (large files), 23 = default quality (good balance), 28 = smaller files with visible quality loss. For distribution, 22–24 is the standard range. -movflags faststart moves the MP4 metadata to the front of the file so browsers can start playing before the full download completes.

Method 2: ChangeThisFile API (HttpClient, no FFmpeg binary)

POST the MOV file, receive MP4. FFmpeg runs server-side. Free tier: 1,000 conversions/month.

# Verify with curl first
curl -X POST https://changethisfile.com/v1/convert \
  -H "Authorization: Bearer ctf_sk_your_key" \
  -F "file=@clip.mov" \
  -F "target=mp4" \
  --output clip.mp4
using System.Net.Http;
using System.Net.Http.Headers;

// Register in Program.cs:
// builder.Services.AddHttpClient("ctf", c =>
// {
//     c.BaseAddress = new Uri("https://changethisfile.com");
//     c.Timeout = TimeSpan.FromMinutes(10); // long for large video files
// });

public class VideoConvertService
{
    private readonly HttpClient _http;
    private const string ApiKey = "ctf_sk_your_key_here";

    public VideoConvertService(IHttpClientFactory factory)
        => _http = factory.CreateClient("ctf");

    public async Task ConvertMovToMp4Async(
        string movPath,
        string mp4Path,
        CancellationToken ct = default)
    {
        await using var fileStream = File.OpenRead(movPath);
        using var form = new MultipartFormDataContent();

        var fileContent = new StreamContent(fileStream);
        fileContent.Headers.ContentType =
            new MediaTypeHeaderValue("video/quicktime");
        form.Add(fileContent, "file", Path.GetFileName(movPath));
        form.Add(new StringContent("mp4"), "target");

        using var request = new HttpRequestMessage(HttpMethod.Post, "/v1/convert")
        {
            Content = form,
            Headers = { Authorization =
                new AuthenticationHeaderValue("Bearer", ApiKey) }
        };

        // Use ResponseHeadersRead to stream without buffering the whole MP4
        var response = await _http.SendAsync(
            request, HttpCompletionOption.ResponseHeadersRead, ct);
        response.EnsureSuccessStatusCode();

        await using var outStream = File.Create(mp4Path);
        var contentStream = await response.Content.ReadAsStreamAsync(ct);
        await contentStream.CopyToAsync(outStream, ct);
    }
}

When to use each

ApproachBest forTradeoff
Xabe.FFmpegOn-premise servers, large files, full codec control (CRF, resolution, filters)FFmpeg binary required; Windows path quirks; CPU-intensive
ChangeThisFile APIContainers without FFmpeg, short clips under 25MB, free tier25MB upload limit on free tier; network upload time for large files

Production tips

  • Add -movflags faststart. For MP4 files served over HTTP, this moves the moov atom to the front of the file so the browser can start playing immediately. Without it, the browser must download the entire file before starting playback.
  • Use IHttpClientFactory for the API. Register a named client with a 10-minute timeout (video transcoding is slow). Inject IHttpClientFactory — never new HttpClient() in a service.
  • Stream the response with ResponseHeadersRead. MP4 output can be hundreds of megabytes. HttpCompletionOption.ResponseHeadersRead + CopyToAsync avoids buffering the whole file in memory.
  • Pass CancellationToken to Xabe.FFmpeg.Start(). It propagates to the underlying FFmpeg process — if the request is cancelled, FFmpeg terminates and the output file is cleaned up.
  • Limit concurrent FFmpeg conversions. Video transcoding is CPU-bound. Limit concurrency to CPU count with Parallel.ForEachAsync(maxDegreeOfParallelism: Environment.ProcessorCount) for batch jobs.
  • Pre-validate the file is actually a video. MOV files can contain audio-only tracks. Check mediaInfo.VideoStreams.Any() before queuing a conversion job.

For on-premise servers where FFmpeg is present, Xabe.FFmpeg gives you full control over codec, quality, and resolution with a clean .NET API. For containerized services or serverless functions where you don't want to manage an FFmpeg binary, the ChangeThisFile API handles MOV-to-MP4 with a single HttpClient call. Free tier: 1,000 conversions/month.