ffmpeg-wasm

Ffmpeg wasm type conversion outputs empty files


I am having a problem with ffmpeg wasm. I am trying to build a quick local compression website using ffmpeg wasm to compress audio, video and images using canvas api.

the flow of the issue is when i select a file that is .mp3, and select the compression/conversion codec to be opus which should produce .ogg file in the end. it ends up producing a 0kb file, which means something has failed silently. i have been searching for quite sometime now, and i dont know what the issue could be. I am not the well versed with ffmpeg

code flow

useFfmpeg hook, initializes and ffmeg instance when needed aka when a compression job that needs ffmpeg is called, then i have a hook called useCompression which is the main orchestrator for the other compression hooks, calls compress audio that is exposed from the useAudioCompression, it gets the compression options from a zustand store that i created, then calls a utility i created and by all means not the best one but it does the job called argsBuilder, it takes all the selected options, and then returns an array that has all the args in order then uses it on the ffmpeg instance

the useAudioCompression hook

"use client"

import { useCallback } from 'react';
import { AudioCompressionOptions } from '@/types';
import { formatFileSizeMB, calculateCompressionStats } from '@/lib/utils/compression-helpers';
import { buildAudioArgs, generateFFmpegFileNames } from '@/lib/utils/ffmpeg-args-builder';
import { logFFmpegDebugInfo, parseFFmpegError } from '@/lib/utils/ffmpeg-error-logger';
import { useFFmpeg } from './useFFmpeg';
import { getThreadConfigInfo } from './threadUtils';
import { useCompressionStore } from '@/store/compression-store';

// Codec to format mapping
const codecToFormat: Record<string, string> = {
  'libmp3lame': 'mp3',
  'mp3': 'mp3',
  'aac': 'aac',
  'libvorbis': 'ogg',
  'libopus': 'ogg',
  'opus': 'ogg',
  'pcm_s16le': 'wav',
  'flac': 'flac',
};

// Format to MIME type mapping
const mimeMap: Record<string, string> = {
  mp3: 'audio/mp3',
  aac: 'audio/aac',
  ogg: 'audio/ogg',
  wav: 'audio/wav',
  flac: 'audio/flac',
  m4a: 'audio/mp4',
};

export const useAudioCompression = () => {
  const {
    initializeFFmpeg,
    getFFmpegUtils,
    clearError,
    setCompressionProgress
  } = useFFmpeg();
  const { audioOptions } = useCompressionStore();
  const compressAudio = useCallback(async (
    file: File,
  ): Promise<Blob> => {
    clearError();
    setCompressionProgress(0);

    try {
      console.log(' STARTING AUDIO COMPRESSION PROCESS');
      console.log(' Input file details:', {
        name: file.name,
        size: `${formatFileSizeMB(file.size)} MB`,
        type: file.type,
        lastModified: new Date(file.lastModified).toISOString()
      });

      const ffmpegInstance = await initializeFFmpeg();
      const { fetchFile: fetchFileUtil } = await getFFmpegUtils();

      // Determine output format from codec if not explicitly set
      let outputFormat = audioOptions.outputFormat;
      if (!outputFormat && audioOptions.acodec) {
        outputFormat = codecToFormat[audioOptions.acodec] || 'mp3';
      }
      outputFormat = outputFormat || 'mp3';

      console.log(' Format determination:', {
        originalFormat: audioOptions.outputFormat,
        detectedFromCodec: audioOptions.acodec ? codecToFormat[audioOptions.acodec] : null,
        finalFormat: outputFormat,
        codecToFormatMap: codecToFormat
      });

      const { inputFileName, outputFileName } = generateFFmpegFileNames(file.name, outputFormat);

      console.log(' Generated file names:', { inputFileName, outputFileName });

      // Build compression arguments with the computed output format
      const finalOptions = { ...audioOptions, outputFormat };
      const args = buildAudioArgs(finalOptions, inputFileName, outputFileName);

      // Log detailed debug information
      logFFmpegDebugInfo(args, finalOptions, file.name);

      console.log('🔧 Final FFmpeg arguments:', args);
      console.log('⚙️ Thread configuration:', getThreadConfigInfo());
      
      // For debugging: check codec support if using opus/vorbis
      if (audioOptions.acodec === 'opus' || audioOptions.acodec === 'vorbis') {
        console.log(` Codec Analysis - Attempting ${audioOptions.acodec} compression`);
        console.log(' Critical codec checks:', {
          codecNormalization: audioOptions.acodec === 'opus' ? 'opus → libopus' : 'vorbis → libvorbis',
          targetContainer: outputFormat,
          isValidCombination: (audioOptions.acodec === 'opus' && outputFormat === 'ogg') || 
                             (audioOptions.acodec === 'vorbis' && outputFormat === 'ogg'),
          expectedFileExtension: `.${outputFormat}`,
          mimeType: mimeMap[outputFormat]
        });
      }

      console.log(' Writing input file to FFmpeg virtual filesystem...');
      // Write input file to FFmpeg's virtual file system
      await ffmpegInstance.writeFile(inputFileName, await fetchFileUtil(file));
      console.log(' Input file written successfully');
      
      // Reset progress to 0 before starting
      setCompressionProgress(0);
      console.log(' Executing FFmpeg compression...');
      
      await ffmpegInstance.exec(args);

      console.log(' FFmpeg execution completed');

      // Check if the output file exists and has content
      console.log(' Reading compressed output file...');
      const compressedData = await ffmpegInstance.readFile(outputFileName);
      console.log(` Output file analysis:`, {
        outputFileName,
        rawDataSize: compressedData.length,
        sizeInBytes: `${compressedData.length} bytes`,
        sizeInKB: `${(compressedData.length / 1024).toFixed(2)} KB`,
        sizeInMB: `${(compressedData.length / (1024 * 1024)).toFixed(4)} MB`,
        isEmpty: compressedData.length === 0,
        dataType: typeof compressedData,
        isArrayBuffer: compressedData instanceof ArrayBuffer,
        isUint8Array: compressedData instanceof Uint8Array
      });
      
      if (compressedData.length === 0) {
        console.error(' CRITICAL ERROR: FFmpeg produced empty output file!');
        console.log(' Debugging information:');
        console.log('  - Input file size:', file.size, 'bytes');
        console.log('  - Codec used:', audioOptions.acodec);
        console.log('  - Output format:', outputFormat);
        console.log('  - Arguments passed:', args);
        throw new Error(`FFmpeg produced empty output file. This indicates a codec incompatibility or invalid arguments. Codec: ${audioOptions.acodec}, Format: ${outputFormat}`);
      }

      const mimeType = mimeMap[outputFormat] || 'audio/mp3';
      console.log(' MIME type mapping:', {
        outputFormat,
        detectedMimeType: mimeType,
        availableMimeTypes: mimeMap
      });
      
      const compressedBlob = new Blob([compressedData], { type: mimeType });
      console.log(' Blob creation successful:', {
        blobSize: compressedBlob.size,
        blobType: compressedBlob.type,
        compressionRatio: `${((1 - compressedBlob.size / file.size) * 100).toFixed(1)}%`
      });

      // Calculate compression stats
      const stats = calculateCompressionStats(file.size, compressedBlob.size);
      console.log(' Compression statistics:', stats);

      console.log(` Audio compression SUCCESS:`, {
        inputSize: formatFileSizeMB(file.size),
        outputSize: formatFileSizeMB(compressedBlob.size),
        compressionRatio: `${stats.compressionRatio.toFixed(1)}% reduction`,
        format: outputFormat,
        codec: audioOptions.acodec,
        mimeType: mimeType,
        processingTime: 'completed'
      });

      // Cleanup virtual files
      console.log(' Cleaning up virtual files...');
      await ffmpegInstance.deleteFile(inputFileName);
      await ffmpegInstance.deleteFile(outputFileName);
      console.log(' Cleanup completed');

      console.log(` Final compression summary: ${formatFileSizeMB(file.size)} → ${formatFileSizeMB(compressedBlob.size)} (${stats.compressionRatio.toFixed(1)}% smaller)`);

      // Set progress to 100% when complete
      setCompressionProgress(100);

      return compressedBlob;

    } catch (err) {
      // Parse FFmpeg error for better debugging
      const ffmpegError = parseFFmpegError(err);
      console.error('FFmpeg Error Details:', ffmpegError);
      
      const errorMessage = `Audio compression failed: ${ffmpegError.message}`;
      throw new Error(errorMessage);
    } finally {
      // Reset progress after completion
      setTimeout(() => setCompressionProgress(null), 1000);
    }
  }, [initializeFFmpeg, audioOptions, getFFmpegUtils, clearError, setCompressionProgress]);

  return {
    compressAudio
  };
};

the args builder

/**
 * FFmpeg arguments builder with multithreading support
 * Provides optimized command line arguments for different compression types
 */

import { AudioCompressionOptions, CompressionOptions } from '@/types';
import { getFFmpegThreadArgs } from '@/lib/threadUtils';

/**
 * Generates unique file names for FFmpeg operations
 */
export const generateFFmpegFileNames = (originalName: string, outputFormat: string) => {
  const timestamp = Date.now();
  const extension = originalName.split('.').pop() || 'tmp';
  
  return {
    inputFileName: `input_${timestamp}.${extension}`,
    outputFileName: `output_${timestamp}.${outputFormat}`
  };
};

/**
 * Gets appropriate MIME type for output format and media type
 */
export const getOutputMimeType = (outputFormat: string, mediaType: 'audio' | 'video' | 'image'): string => {
  switch (mediaType) {
    case 'audio':
      switch (outputFormat) {
        case 'mp3': return 'audio/mp3';
        case 'ogg': return 'audio/ogg';
        case 'wav': return 'audio/wav';
        case 'aac': return 'audio/aac';
        default: return 'audio/mp3';
      }
    case 'video':
      switch (outputFormat) {
        case 'webm': return 'video/webm';
        case 'mkv': return 'video/x-matroska';
        case 'avi': return 'video/x-msvideo';
        default: return 'video/mp4';
      }
    case 'image':
      switch (outputFormat) {
        case 'jpeg': case 'jpg': return 'image/jpeg';
        case 'png': return 'image/png';
        case 'webp': return 'image/webp';
        case 'avif': return 'image/avif';
        default: return 'image/jpeg';
      }
    default:
      return 'application/octet-stream';
  }
};

/**
 * Builds FFmpeg arguments for audio compression with multithreading
 */
export const buildAudioArgs = (
  options:AudioCompressionOptions,
  inputFileName: string,
  outputFileName: string
): string[] => {
  let args = ['-i', inputFileName];
  
  // Add threading arguments for optimal performance
  const threadArgs = getFFmpegThreadArgs('audio');
  // args = args.concat(threadArgs);

  // Use custom args if provided, otherwise build from options
  if (options.customArgs && options.customArgs.length > 0) {
    args = args.concat(options.customArgs);
    args.push(outputFileName);
  } else {
    const outputFormat = options.outputFormat || 'mp3';

    // Format specification first (especially important for ogg container)
    if (outputFormat === 'ogg') {
      args.push('-f', 'ogg');
    }

    // Audio codec - normalize codec names
    let codec = options.acodec;
    if (codec === 'opus') codec = 'libopus';
    if (codec === 'vorbis') codec = 'libvorbis';
    
    if (codec) {
      args.push('-acodec', codec);
    } else {
      args.push('-acodec', outputFormat === 'mp3' ? 'libmp3lame' : 'aac');
    }

    // Sample rate
    if (options.sampleRate) {
      args.push('-ar', options.sampleRate);
    } else {
      args.push('-ar', '44100');
    }

    // Channels
    if (options.channels !== undefined) {
      args.push('-ac', options.channels.toString());
    } else {
      args.push('-ac', '2');
    }

    // Codec-specific quality/bitrate settings
    if (codec === 'libopus') {
      // For Opus, use simpler settings that are more compatible with FFmpeg.wasm
      args.push('-b:a', options.bitrate || '128k');
      // Remove advanced Opus settings that might not be supported
    } else if (codec === 'libvorbis') {
      // For Vorbis, use quality-based encoding
      args.push('-q:a', '5'); // Quality 5 is good for Vorbis (~160kbps)
    } else {
      // For other codecs (AAC, MP3), use bitrate
      if (options.bitrate) {
        args.push('-b:a', options.bitrate);
      } else {
        args.push('-b:a', '128k');
      }
    }
    args.push('-y');
    args.push(outputFileName);
  }

  return args;
};

/**
 * Builds FFmpeg arguments for video compression with multithreading
 */
export const buildVideoArgs = (
  options: CompressionOptions,
  inputFileName: string,
  outputFileName: string
): string[] => {
  let args = ['-i', inputFileName];
  
  // Add threading arguments for optimal performance
  const threadArgs = getFFmpegThreadArgs('video');
  args = args.concat(threadArgs);

  // Use custom args if provided, otherwise build from options
  if (options.customArgs && options.customArgs.length > 0) {
    args = args.concat(options.customArgs);
    args.push(outputFileName);
  } else {
    // Video codec
    if (options.vcodec) {
      args.push('-vcodec', options.vcodec);
    } else {
      args.push('-vcodec', 'libx264');
    }

    // CRF (quality)
    if (options.crf !== undefined) {
      args.push('-crf', options.crf.toString());
    } else {
      args.push('-crf', '27');
    }

    // Preset (ultrafast for better threading performance)
    if (options.preset) {
      args.push('-preset', options.preset);
    } else {
      args.push('-preset', 'ultrafast');
    }

    // Scale/resolution
    if (options.scale) {
      args.push('-vf', options.scale);
    } else if (options.maxWidth) {
      args.push('-vf', `scale='min(${options.maxWidth},iw)':-2`);
    } else {
      args.push('-vf', `scale='min(720,iw)':-2`);
    }

    // Audio codec
    if (options.acodec) {
      args.push('-acodec', options.acodec);
    } else {
      args.push('-acodec', 'aac');
    }

    // Audio bitrate
    if (options.bitrate) {
      args.push('-b:a', options.bitrate);
    } else {
      args.push('-b:a', '48k');
    }

    // Optimization for web
    args.push('-movflags', '+faststart');
    
    args.push(outputFileName);
  }

  return args;
};

/**
 * Builds FFmpeg arguments for image compression with multithreading
 */
export const buildImageArgs = (
  options: CompressionOptions,
  inputFileName: string,
  outputFileName: string
): string[] => {
  let args = ['-i', inputFileName];
  
  // Add threading arguments for optimal performance
  const threadArgs = getFFmpegThreadArgs('image');
  args = args.concat(threadArgs);

  // Use custom args if provided, otherwise build from options
  if (options.customArgs && options.customArgs.length > 0) {
    args = args.concat(options.customArgs);
    args.push(outputFileName);
  } else {
    const outputFormat = options.outputFormat || 'jpeg';

    // Quality setting
    if (options.quality !== undefined) {
      if (outputFormat === 'jpeg' || outputFormat === 'jpg') {
        args.push('-q:v', Math.round(31 - (options.quality * 0.31)).toString());
      } else if (outputFormat === 'webp') {
        args.push('-quality', options.quality.toString());
      }
    } else {
      if (outputFormat === 'jpeg' || outputFormat === 'jpg') {
        args.push('-q:v', '15'); // Good quality/size balance
      } else if (outputFormat === 'webp') {
        args.push('-quality', '75');
      }
    }

    // Resolution scaling
    if (options.maxWidth) {
      args.push('-vf', `scale='min(${options.maxWidth},iw)':-2`);
    }

    args.push(outputFileName);
  }

  return args;
};

the logs I'm getting

 STARTING AUDIO COMPRESSION PROCESS
E:\Projects\quick-compression\lib\useAudioCompression.ts:52  Input file details: {name: 'file_example_MP3_5MG.mp3', size: '5.07 MB MB', type: 'audio/mpeg', lastModified: '2025-10-08T22:57:49.687Z'}
E:\Projects\quick-compression\lib\useAudioCompression.ts:69 🔄 Format determination: {originalFormat: undefined, detectedFromCodec: 'ogg', finalFormat: 'ogg', codecToFormatMap: {…}}
E:\Projects\quick-compression\lib\useAudioCompression.ts:78  Generated file names: {inputFileName: 'input_1760199936545.mp3', outputFileName: 'output_1760199936545.ogg'}
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:223  FFmpeg Comprehensive Debug Information
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:224  Input File: file_example_MP3_5MG.mp3
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:225  Raw Compression Options: {
  "bitrate": "128k",
  "sampleRate": "44100",
  "channels": 2,
  "acodec": "opus",
  "outputFormat": "ogg"
}
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:226  Complete FFmpeg Arguments: (14) ['-i', 'input_1760199936545.mp3', '-f', 'ogg', '-acodec', 'libopus', '-ar', '44100', '-ac', '2', '-b:a', '128k', '-y', 'output_1760199936545.ogg']
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:227  Detected Output Format: ogg
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:228  Video Codec: not specified
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:229  Audio Codec: opus
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:230  Audio Bitrate: 128k
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:231  Sample Rate: 44100
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:232  Channels: 2
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:233  Custom Args: none
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:236  System Information
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:237 User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:238 Platform: Win32
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:239 Available Memory: 8
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:240 Hardware Concurrency: 12
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:299  OPUS CODEC ANALYSIS:
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:300   - Using libopus encoder
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:301   - Target container: ogg
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:302   - Bitrate mode active: true
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:303   - Quality mode active: false
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:304   - Both modes active (PROBLEMATIC): false
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:329  No obvious compatibility issues detected
E:\Projects\quick-compression\lib\useAudioCompression.ts:87 🔧 Final FFmpeg arguments: (14) ['-i', 'input_1760199936545.mp3', '-f', 'ogg', '-acodec', 'libopus', '-ar', '44100', '-ac', '2', '-b:a', '128k', '-y', 'output_1760199936545.ogg']
E:\Projects\quick-compression\lib\useAudioCompression.ts:88  Thread configuration: Device: desktop | Cores: 12 | Recommended Threads: 9 | Max Concurrent: 6
E:\Projects\quick-compression\lib\useAudioCompression.ts:92  Codec Analysis - Attempting opus compression
E:\Projects\quick-compression\lib\useAudioCompression.ts:93  Critical codec checks: {codecNormalization: 'opus → libopus', targetContainer: 'ogg', isValidCombination: true, expectedFileExtension: '.ogg', mimeType: 'audio/ogg'}
E:\Projects\quick-compression\lib\useAudioCompression.ts:103  Writing input file to FFmpeg virtual filesystem...
E:\Projects\quick-compression\lib\useAudioCompression.ts:106  Input file written successfully
E:\Projects\quick-compression\lib\useAudioCompression.ts:110  Executing FFmpeg compression...
E:\Projects\quick-compression\lib\useAudioCompression.ts:114  FFmpeg execution completed
E:\Projects\quick-compression\lib\useAudioCompression.ts:117  Reading compressed output file...
E:\Projects\quick-compression\lib\useAudioCompression.ts:119  Output file analysis: {outputFileName: 'output_1760199936545.ogg', rawDataSize: 0, sizeInBytes: '0 bytes', sizeInKB: '0.00 KB', sizeInMB: '0.0000 MB', …}
E:\Projects\quick-compression\lib\useAudioCompression.ts:133  Debugging information:
E:\Projects\quick-compression\lib\useAudioCompression.ts:134   - Input file size: 5319693 bytes
E:\Projects\quick-compression\lib\useAudioCompression.ts:135   - Codec used: opus
E:\Projects\quick-compression\lib\useAudioCompression.ts:136   - Output format: ogg
E:\Projects\quick-compression\lib\useAudioCompression.ts:137   - Arguments passed: (14) ['-i', 'input_1760199936545.mp3', '-f', 'ogg', '-acodec', 'libopus', '-ar', '44100', '-ac', '2', '-b:a', '128k', '-y', 'output_1760199936545.ogg']
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:21  FFmpeg Comprehensive Error Analysis
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:22  Raw error object: Error: FFmpeg produced empty output file. This indicates a codec incompatibility or invalid arguments. Codec: opus, Format: ogg
    at useAudioCompression.useCallback[compressAudio] (E:\Projects\quick-compression\lib\useAudioCompression.ts:138:15)
    at async handleAudioCompression (E:\Projects\quick-compression\lib\useCompression.tsx:46:22)
    at async compressFile (E:\Projects\quick-compression\app\page.tsx:107:26)
    at async handleCompressFile (E:\Projects\quick-compression\app\page.tsx:153:5)
    at async handleRetryFile (E:\Projects\quick-compression\app\page.tsx:171:5)
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:23  Error string: Error: FFmpeg produced empty output file. This indicates a codec incompatibility or invalid arguments. Codec: opus, Format: ogg
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:24  Error message: FFmpeg produced empty output file. This indicates a codec incompatibility or invalid arguments. Codec: opus, Format: ogg
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:25  Error type: object
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:26  Error stack: Error: FFmpeg produced empty output file. This indicates a codec incompatibility or invalid arguments. Codec: opus, Format: ogg
    at useAudioCompression.useCallback[compressAudio] (webpack-internal:///(app-pages-browser)/./lib/useAudioCompression.ts:125:27)
    at async handleAudioCompression (webpack-internal:///(app-pages-browser)/./lib/useCompression.tsx:53:28)
    at async compressFile (webpack-internal:///(app-pages-browser)/./app/page.tsx:93:34)
    at async handleCompressFile (webpack-internal:///(app-pages-browser)/./app/page.tsx:131:9)
    at async handleRetryFile (webpack-internal:///(app-pages-browser)/./app/page.tsx:147:9)
E:\Projects\quick-compression\lib\utils\ffmpeg-error-logger.ts:30  All error properties:

i would really appreaciate it if someone would navigate me through this issue, and if anyone would like extra context feel free to tell me what extra context do you need and i would be happy to add it

I tried playing around with the args, read somewhere that ffmpeg wasm doesnt support the syntax of libopus and i should use opus, and the other way around, conflicts between bitrate setting and quality setting


Solution

  • Could it be that this is a known bug with ffmpeg.wasm? Other folks are also getting 0-byte empty files when attempting to convert and mp3 to ogg when using the libopus codec. You could try their suggestion, which is to use libvorbis intead. To do so, you could change the Zustand store from:

    {
      acodec: "opus",
      outputFormat: "ogg",
      bitrate: "128k",
      // ... other settings
    }
    

    to:

    {
      acodec: "vorbis",
      outputFormat: "ogg",
      bitrate: "128k",
      // ... other settings
    }