DEV Community

monkeymore studio
monkeymore studio

Posted on

Strip the Sound from Any Video in Seconds

Sometimes you need a video without its soundtrack. Maybe the background music is copyrighted and you want to post it somewhere. Maybe there is unwanted conversation in the background of an otherwise perfect shot. Or maybe you just want the visuals without the noise.

Most video editing software makes you import the file, mute the audio track, and re-export the whole thing. That takes time and often re-encodes the video, subtly degrading quality. We built something simpler: a tool that removes the audio track and copies the video stream as-is. No re-encoding. No quality loss. Just a silent video file.

And it all happens in your browser.

Why Remove Audio Locally?

Removing audio is technically one of the easiest video operations. There is no reason to upload a 2GB file to a server just to delete its sound:

  • No upload needed: Your video stays on your disk the entire time.
  • No quality loss: We copy the video stream directly. Every pixel stays exactly the same.
  • Instant processing: Without re-encoding, the operation is essentially just file I/O.
  • Privacy: Nobody else hears your audio, even accidentally.
  • No weird limits: Files up to 2GB, limited only by your browser's memory.

How It Works

Here is the entire flow from upload to silent download:

The Data Model

We keep things simple with a single VideoFile object:

interface VideoFile {
  id: string;
  file: File;
  previewUrl: string;
  processedUrl?: string;
  processedFileName?: string;
  hasAudio?: boolean;
  error?: string;
  processing?: boolean;
  progress?: number;
  videoWidth?: number;
  videoHeight?: number;
  duration?: number;
}
Enter fullscreen mode Exit fullscreen mode

The hasAudio field is tracked so we can show a helpful message if someone uploads a file that is already silent.

Pick Your Output Format

After uploading, you can choose the container format for your muted video:

const OUTPUT_FORMATS = [
  { value: "mp4", label: "MP4", extension: "mp4", mimeType: "video/mp4" },
  { value: "webm", label: "WebM", extension: "webm", mimeType: "video/webm" },
  { value: "mov", label: "MOV", extension: "mov", mimeType: "video/quicktime" },
  { value: "avi", label: "AVI", extension: "avi", mimeType: "video/x-msvideo" },
];
Enter fullscreen mode Exit fullscreen mode

MP4 is the default because it works everywhere. But if you need a different container for compatibility or workflow reasons, you can switch before processing.

The Core Command: Two Flags That Do All the Work

The actual audio removal is almost comically simple:

const ffmpegArgs = [
  "-i", inputName,
  "-c:v", "copy",
  "-an",
  "-y",
  outputName
];

await ffmpeg.exec(ffmpegArgs);
Enter fullscreen mode Exit fullscreen mode

That is it. Two flags:

  • -c:v copy: Copies the video stream without re-encoding. Every frame stays exactly as it was in the original file.
  • -an: Disables audio. This tells FFmpeg to not include any audio tracks in the output.

The result is a video file that contains only the video stream, with zero audio data.

Why -c:v copy Matters

Without this flag, FFmpeg would re-encode the video using its default settings. That means:

  • Longer processing time
  • Potential quality loss
  • Different file size

With -c:v copy, FFmpeg simply reads the compressed video data from the input and writes it directly to the output. The video quality is bit-for-bit identical to the original. Processing time is measured in seconds, not minutes.

The Full Processing Logic

Here is the complete removeAudio function:

const removeAudio = async () => {
  if (!selectedFile || !ffmpegRef.current) return;

  setIsProcessing(true);
  setError(null);
  setSelectedFile(prev => prev ? { ...prev, processing: true, progress: 0, error: undefined } : null);

  try {
    const ffmpeg = ffmpegRef.current;
    const inputExt = selectedFile.file.name.split('.').pop() || 'mp4';
    const inputName = `input.${inputExt}`;
    const outputName = `output_muted.${outputFormat}`;

    const fileArrayBuffer = await selectedFile.file.arrayBuffer();
    await ffmpeg.writeFile(inputName, new Uint8Array(fileArrayBuffer));

    const ffmpegArgs = [
      "-i", inputName,
      "-c:v", "copy",
      "-an",
      "-y",
      outputName
    ];

    await ffmpeg.exec(ffmpegArgs);

    let data: any;
    try {
      data = await ffmpeg.readFile(outputName);
    } catch (readErr) {
      throw new Error("Failed to read output file");
    }

    if (!data || (data instanceof Uint8Array && data.length === 0)) {
      throw new Error("Output file is empty");
    }

    const uint8Data = data instanceof Uint8Array ? data : new Uint8Array();
    const buffer = new ArrayBuffer(uint8Data.length);
    new Uint8Array(buffer).set(uint8Data);

    const outputFormatInfo = OUTPUT_FORMATS.find(f => f.value === outputFormat);
    const blob = new Blob([buffer], { type: outputFormatInfo?.mimeType || "video/mp4" });
    const processedUrl = URL.createObjectURL(blob);
    const baseName = selectedFile.file.name.replace(/\.[^/.]+$/, "");

    setSelectedFile(prev => prev ? {
      ...prev,
      processedUrl,
      processedFileName: `${baseName}_muted.${outputFormatInfo?.extension || outputFormat}`,
      processing: false,
      progress: 100,
    } : null);

    await ffmpeg.deleteFile(inputName);
    await ffmpeg.deleteFile(outputName);
  } catch (err: any) {
    console.error("Processing error:", err);
    setError("Failed to remove audio from video");
    setSelectedFile(prev => prev ? { ...prev, error: "Processing failed", processing: false } : null);
  } finally {
    setIsProcessing(false);
  }
};
Enter fullscreen mode Exit fullscreen mode

A few defensive touches worth noting:

  • We handle both Uint8Array and string responses from ffmpeg.readFile(), just in case.
  • We validate that the output file is not empty before presenting it to the user.
  • The output Blob gets the correct MIME type so browsers handle previews and downloads properly.

Loading FFmpeg on Demand

As with all our tools, FFmpeg loads lazily:

// utils/ffmpegLoader.ts
import { fetchFile, toBlobURL } from "@ffmpeg/util";

let ffmpeg: any = null;
let fetchFileFn: any = null;

export async function loadFFmpeg() {
  if (ffmpeg) return { ffmpeg, fetchFile: fetchFileFn };

  const { FFmpeg } = await import("@ffmpeg/ffmpeg");

  ffmpeg = new FFmpeg();
  fetchFileFn = fetchFile;

  const baseURL = "https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/umd";

  await ffmpeg.load({
    coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
    wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"),
  }, {
    corePath: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
  });

  return { ffmpeg, fetchFile };
}
Enter fullscreen mode Exit fullscreen mode

Dynamic import keeps the initial bundle lean. Blob URLs avoid CORS headaches. Caching the instance means subsequent operations start faster.

Preview the Result

After processing, we show the muted video with a built-in player so you can verify it is actually silent:

{selectedFile.processedUrl && (
  <div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
    <div className="flex items-center gap-3 mb-4">
      <VolumeX className="w-8 h-8 text-green-600" />
      <div>
        <h3 className="font-medium text-green-800">Audio Removed Successfully!</h3>
        <p className="text-sm text-green-600">{selectedFile.processedFileName}</p>
      </div>
    </div>

    <div className="bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden">
      <video src={selectedFile.processedUrl} controls className="w-full max-h-64 mx-auto" />
    </div>

    <p className="text-sm text-gray-500 mt-2 text-center">
      Preview: This video should have no sound
    </p>
  </div>
)}
Enter fullscreen mode Exit fullscreen mode

The "Preview: This video should have no sound" text is a small but important UX touch. It sets the right expectation — if the user hears something, they know something went wrong.

Cleanup

We clean up object URLs and virtual filesystem entries:

const reset = useCallback(() => {
  if (selectedFile) {
    URL.revokeObjectURL(selectedFile.previewUrl);
    if (selectedFile.processedUrl) URL.revokeObjectURL(selectedFile.processedUrl);
  }
  setSelectedFile(null);
  setError(null);
  setOutputFormat("mp4");
}, [selectedFile]);
Enter fullscreen mode Exit fullscreen mode

And after each operation:

await ffmpeg.deleteFile(inputName);
await ffmpeg.deleteFile(outputName);
Enter fullscreen mode Exit fullscreen mode

What We Learned

This was one of the simpler tools to build, but it still taught us a few things:

  • -an is surprisingly final: Unlike muting in a video editor, -an actually removes the audio track entirely. The output file contains zero audio data. This makes the file slightly smaller and ensures there is absolutely no risk of accidental sound leakage.
  • Container conversion happens for free: Because we are copying the video stream, changing the output container (e.g., MP4 to MOV) is trivial. FFmpeg just repackages the same video data into a different file format.
  • -c:v copy does not always work: If the source video uses a codec that is not compatible with the target container, the copy will fail. For example, a VP8 video inside a WebM container cannot be directly copied into an MP4 container because MP4 does not natively support VP8. In practice, most user-uploaded videos are H.264, which works in all our supported containers.
  • Users want confirmation: We added the "This video should have no sound" preview message because early testers were unsure whether the tool had actually worked. Visual confirmation that the audio was gone turned out to be more important than we expected.

Give It a Try

Got a video with audio you want gone? Strip it out right now. No upload, no account, no waiting.

šŸ‘‰ Remove Audio from Video

Upload your video, pick your format, and download the silent version. The video quality stays exactly the same — only the sound disappears.

Top comments (0)