Skip to content

Error Handling in Fluvie

Comprehensive guide to handling errors during video composition and rendering.

Exception Hierarchy

Fluvie provides a structured exception hierarchy for better error handling and debugging:

FluvieException (base)
├── FFmpegNotFoundException
├── FFmpegExecutionException
├── RenderException
├── FrameCaptureException
├── InvalidConfigurationException
├── AudioProcessingException
├── FileNotFoundException
├── FileIOException
├── VideoProbeException
├── TimelineException
└── UnsupportedPlatformException

All Fluvie exceptions extend FluvieException, allowing you to catch all library errors with a single handler if needed.

Common Error Scenarios

1. FFmpeg Not Found

Exception: FFmpegNotFoundException

When it happens: FFmpeg executable is not installed or not in PATH.

Example:

import 'package:fluvie/fluvie.dart';

try {
  await renderService.execute(...);
} on FFmpegNotFoundException catch (e) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Text('FFmpeg Not Found'),
      content: Text('Please install FFmpeg to use video rendering.\n\n${e.message}'),
      actions: [
        TextButton(
          onPressed: () => launchUrl('https://ffmpeg.org/download.html'),
          child: Text('Download FFmpeg'),
        ),
      ],
    ),
  );
}

Prevention:

// Check FFmpeg availability before rendering
final diagnostics = await FFmpegChecker.check();
if (!diagnostics.isAvailable) {
  print(diagnostics.errorMessage);
  print(diagnostics.installationInstructions);
  return;
}

2. FFmpeg Execution Failures

Exception: FFmpegExecutionException

When it happens: FFmpeg command fails during video encoding.

Example:

try {
  final outputPath = await renderService.execute(...);
} on FFmpegExecutionException catch (e) {
  FluvieLogger.error('FFmpeg failed', module: 'render');
  FluvieLogger.error('Exit code: ${e.exitCode}', module: 'render');
  FluvieLogger.error('Command: ${e.command}', module: 'render');
  FluvieLogger.error('Stderr: ${e.stderr}', module: 'render');

  // Handle specific exit codes
  if (e.exitCode == 1) {
    // Generic error - check stderr for details
  } else if (e.exitCode == 255) {
    // Codec or format error
    showError('Unsupported video format or codec');
  }
}

Common causes: - Unsupported video codecs - Corrupted input files - Insufficient disk space - Invalid FFmpeg arguments

3. Frame Capture Failures

Exception: FrameCaptureException

When it happens: Widget rendering or frame capture fails.

Example:

try {
  final image = await frameSequencer.captureFrame(pixelRatio: 3.0);
} on FrameCaptureException catch (e) {
  print('Frame capture failed: ${e.message}');
  print('Boundary key: ${e.boundaryKey}');
  if (e.frameNumber != null) {
    print('Failed at frame: ${e.frameNumber}');
  }

  // Retry with lower pixel ratio
  try {
    final image = await frameSequencer.captureFrame(pixelRatio: 1.0);
  } catch (retryError) {
    // Still failed - report error
    reportError(retryError);
  }
}

Common causes: - Missing RepaintBoundary widget - Widget tree not properly built - Memory exhaustion - Platform rendering issues

Prevention:

// Ensure RepaintBoundary is properly set up
final boundaryKey = GlobalKey();

RepaintBoundary(
  key: boundaryKey,
  child: VideoComposition(
    fps: 30,
    durationInFrames: 90,
    child: MyContent(),
  ),
)

4. Rendering Failures

Exception: RenderException

When it happens: General rendering pipeline failures.

Example:

try {
  final outputPath = await renderService.execute(
    config: config,
    onFrameUpdate: (frame) {
      print('Rendering frame $frame');
    },
  );
} on RenderException catch (e) {
  if (e.frameNumber != null) {
    print('Rendering failed at frame ${e.frameNumber}');
    // Maybe skip this frame or stop rendering
  }

  // Log error for debugging
  FluvieLogger.error('Render failed: ${e.message}', module: 'render');
  if (e.cause != null) {
    FluvieLogger.error('Caused by: ${e.cause}', module: 'render');
  }
}

5. Configuration Errors

Exception: InvalidConfigurationException

When it happens: Invalid parameters passed to Fluvie APIs.

Example:

try {
  final composition = VideoComposition(
    fps: -1,  // Invalid!
    durationInFrames: 90,
    child: MyContent(),
  );
} on InvalidConfigurationException catch (e) {
  print('Invalid configuration: ${e.message}');
  print('Field: ${e.fieldName}');
  print('Value: ${e.invalidValue}');

  // Show validation error to user
  showSnackBar('${e.fieldName}: ${e.message}');
}

Validation before rendering:

void validateConfig(RenderConfig config) {
  if (config.timeline.fps <= 0) {
    throw InvalidConfigurationException(
      'FPS must be greater than 0',
      fieldName: 'fps',
      invalidValue: config.timeline.fps,
    );
  }

  if (config.timeline.durationInFrames <= 0) {
    throw InvalidConfigurationException(
      'Duration must be greater than 0 frames',
      fieldName: 'durationInFrames',
      invalidValue: config.timeline.durationInFrames,
    );
  }
}

6. Audio Processing Failures

Exception: AudioProcessingException

When it happens: Audio file loading, BPM detection, or processing fails.

Example:

try {
  final metadata = await videoProbeService.probe('song.mp3');
} on AudioProcessingException catch (e) {
  print('Audio processing failed: ${e.message}');
  print('Operation: ${e.operation}');
  print('File: ${e.audioFilePath}');

  // Fall back to no audio
  renderWithoutAudio();
}

Handling missing audio gracefully:

AudioTrack? loadAudio(String path) {
  try {
    return AudioTrack(
      source: AudioSource.file(path),
      volume: 1.0,
    );
  } on FileNotFoundException {
    FluvieLogger.warning('Audio file not found, rendering without audio');
    return null;
  }
}

7. File Not Found Errors

Exception: FileNotFoundException

When it happens: Required files (video, audio, assets) don't exist.

Example:

try {
  final video = VideoSequence(
    source: VideoSource.file('/path/to/video.mp4'),
    startFrame: 0,
    durationInFrames: 90,
  );
} on FileNotFoundException catch (e) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Text('File Not Found'),
      content: Text('Could not find: ${e.filePath}'),
      actions: [
        TextButton(
          onPressed: () => pickFile(),
          child: Text('Choose File'),
        ),
      ],
    ),
  );
}

Validation before use:

import 'dart:io';

Future<void> validateFile(String path) async {
  final file = File(path);
  if (!await file.exists()) {
    throw FileNotFoundException(path);
  }

  // Check file is readable
  try {
    await file.length();
  } catch (e) {
    throw FileIOException(
      'File exists but cannot be read',
      filePath: path,
      operation: 'read',
      cause: e,
    );
  }
}

8. Timeline Configuration Errors

Exception: TimelineException

When it happens: Invalid frame ranges, overlapping sequences, etc.

Example:

try {
  final sequence = Sequence(
    startFrame: 100,
    endFrame: 50,  // Invalid: end before start!
    child: MyWidget(),
  );
} on TimelineException catch (e) {
  print('Timeline error: ${e.message}');
  print('Start frame: ${e.startFrame}');
  print('End frame: ${e.endFrame}');

  // Fix the configuration
  final fixed = Sequence(
    startFrame: min(e.startFrame!, e.endFrame!),
    endFrame: max(e.startFrame!, e.endFrame!),
    child: MyWidget(),
  );
}

Best Practices

1. Always Use Specific Exception Types

Bad:

try {
  await renderService.execute(...);
} catch (e) {
  print('Something went wrong: $e');
}

Good:

try {
  await renderService.execute(...);
} on FFmpegNotFoundException catch (e) {
  handleMissingFFmpeg(e);
} on FFmpegExecutionException catch (e) {
  handleFFmpegFailure(e);
} on FrameCaptureException catch (e) {
  handleFrameCaptureFailure(e);
} on FluvieException catch (e) {
  handleGenericFluvieError(e);
} catch (e, stack) {
  handleUnexpectedError(e, stack);
}

2. Provide User-Friendly Error Messages

Bad:

} on FluvieException catch (e) {
  showDialog(content: Text(e.toString()));  // Too technical
}

Good:

} on FFmpegNotFoundException catch (e) {
  showDialog(
    content: Text('Video rendering requires FFmpeg. Would you like to install it?'),
    actions: [InstallButton()],
  );
} on FrameCaptureException catch (e) {
  showDialog(
    content: Text('Failed to capture video frame. Please try again or reduce quality.'),
  );
}

3. Log Errors for Debugging

import 'package:fluvie/fluvie.dart';

void handleError(FluvieException e, StackTrace? stack) {
  // Log to Fluvie logger
  FluvieLogger.error(e.message, module: 'app');

  // Log cause if available
  if (e.cause != null) {
    FluvieLogger.error('Caused by: ${e.cause}', module: 'app');
  }

  // Log stack trace for debugging
  if (stack != null) {
    FluvieLogger.error('Stack trace:\n$stack', module: 'app');
  }

  // Send to crash reporting service (e.g., Sentry, Firebase Crashlytics)
  crashReporting.recordError(e, stack);
}

4. Retry Logic for Transient Failures

Future<String> renderWithRetry(
  RenderConfig config, {
  int maxAttempts = 3,
}) async {
  for (int attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await renderService.execute(config: config, ...);
    } on FrameCaptureException catch (e) {
      if (attempt == maxAttempts) rethrow;

      FluvieLogger.warning(
        'Frame capture failed (attempt $attempt/$maxAttempts), retrying...',
        module: 'render',
      );

      await Future.delayed(Duration(seconds: 1));
    }
  }

  throw RenderException('Failed after $maxAttempts attempts');
}

5. Graceful Degradation

Future<String> renderVideo(VideoComposition composition) async {
  // Try with high quality first
  try {
    return await renderService.execute(
      config: RenderConfig(
        width: 1920,
        height: 1080,
        ...
      ),
    );
  } on FrameCaptureException {
    // Fall back to lower quality
    FluvieLogger.warning('Falling back to 720p rendering');
    return await renderService.execute(
      config: RenderConfig(
        width: 1280,
        height: 720,
        ...
      ),
    );
  }
}

6. Validate Early

class SafeVideoComposition extends StatelessWidget {
  final int fps;
  final int durationInFrames;
  final Widget child;

  const SafeVideoComposition({
    required this.fps,
    required this.durationInFrames,
    required this.child,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    // Validate parameters early
    if (fps <= 0) {
      throw InvalidConfigurationException(
        'FPS must be greater than 0',
        fieldName: 'fps',
        invalidValue: fps,
      );
    }

    if (durationInFrames <= 0) {
      throw InvalidConfigurationException(
        'Duration must be greater than 0',
        fieldName: 'durationInFrames',
        invalidValue: durationInFrames,
      );
    }

    return VideoComposition(
      fps: fps,
      durationInFrames: durationInFrames,
      child: child,
    );
  }
}

Error Recovery Strategies

Strategy 1: Restart Rendering

Future<String?> renderWithRestart(RenderConfig config) async {
  try {
    return await renderService.execute(config: config, ...);
  } on RenderException catch (e) {
    if (e.frameNumber != null && e.frameNumber! > 10) {
      // Rendering failed mid-way - restart from scratch
      FluvieLogger.warning('Restarting render from frame 0');

      // Clear any partial output
      await cleanupPartialRender();

      // Try again
      return await renderService.execute(config: config, ...);
    }
    rethrow;
  }
}

Strategy 2: Skip Problematic Frames

Future<String> renderWithSkip(RenderConfig config) async {
  final failedFrames = <int>[];

  try {
    return await renderService.execute(
      config: config,
      onFrameUpdate: (frame) {
        // Track progress
      },
    );
  } on FrameCaptureException catch (e) {
    if (e.frameNumber != null) {
      failedFrames.add(e.frameNumber!);

      if (failedFrames.length > 10) {
        throw RenderException(
          'Too many failed frames: $failedFrames',
        );
      }

      // Continue with next frame
      return renderNextFrame();
    }
    rethrow;
  }
}

Strategy 3: Reduce Quality

Future<String> renderWithQualityReduction(RenderConfig config) async {
  try {
    return await renderService.execute(config: config, ...);
  } on FrameCaptureException {
    FluvieLogger.warning('Reducing pixel ratio due to capture failure');

    // Modify config to use lower pixel ratio
    final lowerQualityConfig = config.copyWith(
      pixelRatio: config.pixelRatio / 2,
    );

    return await renderService.execute(config: lowerQualityConfig, ...);
  }
}

Testing Error Handling

import 'package:flutter_test/flutter_test.dart';
import 'package:fluvie/fluvie.dart';

void main() {
  group('Error Handling', () {
    test('throws InvalidConfigurationException for negative FPS', () {
      expect(
        () => VideoComposition(
          fps: -1,
          durationInFrames: 90,
          child: Container(),
        ),
        throwsA(isA<InvalidConfigurationException>()),
      );
    });

    test('throws FileNotFoundException for missing file', () async {
      expect(
        () async => VideoSequence(
          source: VideoSource.file('/nonexistent/video.mp4'),
          startFrame: 0,
          durationInFrames: 90,
        ),
        throwsA(isA<FileNotFoundException>()),
      );
    });

    test('provides detailed error information', () async {
      try {
        throw FFmpegExecutionException(
          'Codec not supported',
          exitCode: 1,
          stderr: 'Unknown codec: abc123',
          command: 'ffmpeg -i input.mp4 ...',
        );
      } on FFmpegExecutionException catch (e) {
        expect(e.message, contains('Codec not supported'));
        expect(e.exitCode, equals(1));
        expect(e.stderr, contains('Unknown codec'));
        expect(e.command, contains('ffmpeg'));
      }
    });
  });
}

Debugging Tips

1. Enable Verbose Logging

// Enable Fluvie logging
FluvieLogger.setLevel(LogLevel.debug);

// This will print detailed error information
try {
  await renderService.execute(...);
} on FluvieException catch (e, stack) {
  FluvieLogger.error(e.toString(), module: 'app');
  FluvieLogger.error('Stack: $stack', module: 'app');
}

2. Inspect FFmpeg Commands

} on FFmpegExecutionException catch (e) {
  // Print the exact FFmpeg command that failed
  print('Failed FFmpeg command:');
  print(e.command);

  // Try running it manually to see the full error
  print('\nRun this command manually to debug:');
  print(e.command);
}

3. Check System Resources

import 'dart:io';

void checkResources() {
  // Check disk space
  final tempDir = Directory.systemTemp;
  final stat = tempDir.statSync();
  print('Temp dir: ${tempDir.path}');

  // Check available memory (platform-specific)
  // On Linux: cat /proc/meminfo
  // On macOS: vm_stat
}

Last Updated: 2025-12-29

Remember: Good error handling makes the difference between a frustrating user experience and a robust, reliable application!