Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions ENHANCED_ERROR_HANDLING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Video Adapter Error Handling Improvements

This document describes the enhanced error handling for Cloudinary React Native SDK video adapters.

## Overview

The video adapters (`ExpoAVVideoAdapter`, `ExpoVideoAdapter`, and `FallbackVideoAdapter`) now provide improved error handling with detailed error messages and installation guidance when video libraries are not available.

## New Features

### Enhanced Error Messages

Instead of generic "module not available" errors, the adapters now provide:
- Specific adapter name in error messages
- Clear installation commands
- Detailed error context

### New Method: `getAvailabilityInfo()`

All video adapters now implement an optional `getAvailabilityInfo()` method that returns:

```typescript
{
isAvailable: boolean;
error?: string;
installationCommand?: string;
}
```

### Example Usage

```typescript
import { VideoPlayerFactory } from 'cloudinary-react-native';

const adapter = VideoPlayerFactory.getAvailableAdapter();

// Check availability with detailed information
const info = adapter.getAvailabilityInfo?.();
if (!info?.isAvailable) {
console.log(`Error: ${info.error}`);
console.log(`Install with: ${info.installationCommand}`);
}
```

## Error Message Examples

### Before (Generic)
```
Error: expo-av is not available
```

### After (Detailed)
```
Error: ExpoAVVideoAdapter: Module not found: expo-av. Please install: "npx expo install expo-av"
```

## Benefits

1. **Better Developer Experience**: Clear error messages with actionable solutions
2. **Faster Debugging**: Specific adapter names and installation commands
3. **Production Safety**: Graceful error handling without silent failures
4. **Non-Breaking**: Existing code continues to work without changes

## Installation Commands by Adapter

| Adapter | Installation Command |
|---------|---------------------|
| ExpoAVVideoAdapter | `npx expo install expo-av` |
| ExpoVideoAdapter | `npx expo install expo-video` |
| FallbackVideoAdapter | `npx expo install expo-video expo-av` |

## Migration Guide

No migration is required. The new `getAvailabilityInfo()` method is optional and existing error handling continues to work as before, but with improved error messages.

### Optional: Enhanced Error Handling

You can optionally use the new method for better error handling:

```typescript
// Before
try {
const videoComponent = adapter.renderVideo(props, ref);
} catch (error) {
console.error('Video failed:', error.message);
}

// After (enhanced)
if (!adapter.isAvailable()) {
const info = adapter.getAvailabilityInfo?.();
if (info && !info.isAvailable) {
console.error(`Video adapter error: ${info.error}`);
console.log(`Fix with: ${info.installationCommand}`);
return;
}
}

const videoComponent = adapter.renderVideo(props, ref);
```
35 changes: 33 additions & 2 deletions src/adapters/ExpoAVVideoAdapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,44 @@ export class ExpoAVVideoAdapter implements VideoPlayerAdapter {
return !!(this.expoAVModule && this.expoAVModule.Video);
}

/**
* Get detailed information about adapter availability
* @returns Object containing availability status, error details, and installation guidance
*/
getAvailabilityInfo(): {
isAvailable: boolean;
error?: string;
installationCommand?: string;
} {
if (!this.expoAVModule) {
return {
isAvailable: false,
error: 'Module not found: expo-av',
installationCommand: 'npx expo install expo-av'
};
}

if (!this.expoAVModule.Video) {
return {
isAvailable: false,
error: 'Video component not found in expo-av module',
installationCommand: 'npx expo install expo-av'
};
}

return { isAvailable: true };
}

getAdapterName(): string {
return VideoPlayerType.EXPO_AV;
}

renderVideo(props: VideoPlayerProps, ref: RefObject<VideoPlayerRef | null>): ReactElement {
if (!this.isAvailable()) {
throw new Error('expo-av is not available');
const info = this.getAvailabilityInfo();
throw new Error(
`ExpoAVVideoAdapter: ${info.error}. Please install: "${info.installationCommand}"`
);
}

const { Video } = this.expoAVModule;
Expand Down Expand Up @@ -80,4 +111,4 @@ export class ExpoAVVideoAdapter implements VideoPlayerAdapter {
return null;
}
}
}
}
50 changes: 45 additions & 5 deletions src/adapters/ExpoVideoAdapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,58 @@ export class ExpoVideoAdapter implements VideoPlayerAdapter {
}

isAvailable(): boolean {
// Check if expo-video module loaded successfully and has Video component
return !!(this.expoVideoModule && this.expoVideoModule.VideoView);
const hasModule = !!this.expoVideoModule;
const hasVideoView = !!(this.expoVideoModule && this.expoVideoModule.VideoView);
const hasCreatePlayer = !!(this.expoVideoModule && this.expoVideoModule.createVideoPlayer);
return hasModule && hasVideoView && hasCreatePlayer;
}

/**
* Get detailed information about adapter availability
* @returns Object containing availability status, error details, and installation guidance
*/
getAvailabilityInfo(): {
isAvailable: boolean;
error?: string;
installationCommand?: string;
} {
if (!this.expoVideoModule) {
return {
isAvailable: false,
error: 'Module not found: expo-video',
installationCommand: 'npx expo install expo-video'
};
}

if (!this.expoVideoModule.VideoView) {
return {
isAvailable: false,
error: 'VideoView component not found in expo-video module',
installationCommand: 'npx expo install expo-video'
};
}

if (!this.expoVideoModule.createVideoPlayer) {
return {
isAvailable: false,
error: 'createVideoPlayer function not found in expo-video module',
installationCommand: 'npx expo install expo-video'
};
}

return { isAvailable: true };
}

getAdapterName(): string {
return VideoPlayerType.EXPO_VIDEO;
}

renderVideo(props: VideoPlayerProps, ref: RefObject<VideoPlayerRef | null>): ReactElement {

if (!this.isAvailable()) {
throw new Error('expo-video is not available');
const info = this.getAvailabilityInfo();
throw new Error(
`ExpoVideoAdapter: ${info.error}. Please install: "${info.installationCommand}"`
);
}

const { VideoView, createVideoPlayer } = this.expoVideoModule;
Expand Down Expand Up @@ -369,4 +409,4 @@ export class ExpoVideoAdapter implements VideoPlayerAdapter {
return null;
}
}
}
}
16 changes: 16 additions & 0 deletions src/adapters/FallbackVideoAdapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ export class FallbackVideoAdapter implements VideoPlayerAdapter {
return VideoPlayerType.FALLBACK;
}

/**
* Get detailed information about adapter availability
* @returns Object containing availability status and installation guidance for video libraries
*/
getAvailabilityInfo(): {
isAvailable: boolean;
error?: string;
installationCommand?: string;
} {
return {
isAvailable: true,
error: this.errorMessage,
installationCommand: 'npx expo install expo-video expo-av'
};
}

renderVideo(props: VideoPlayerProps, _ref: RefObject<VideoPlayerRef | null>): ReactElement {
return React.createElement(View, {
style: [
Expand Down
150 changes: 150 additions & 0 deletions src/adapters/__tests__/ErrorHandling.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { ExpoAVVideoAdapter } from '../ExpoAVVideoAdapter';
import { ExpoVideoAdapter } from '../ExpoVideoAdapter';
import { FallbackVideoAdapter } from '../FallbackVideoAdapter';

describe('Video Adapter Error Handling Improvements', () => {
describe('ExpoAVVideoAdapter', () => {
it('should have getAvailabilityInfo method', () => {
const adapter = new ExpoAVVideoAdapter();

expect(typeof adapter.getAvailabilityInfo).toBe('function');

const info = adapter.getAvailabilityInfo();
expect(info).toHaveProperty('isAvailable');
expect(info).toHaveProperty('installationCommand', 'npx expo install expo-av');

if (!info.isAvailable) {
expect(info).toHaveProperty('error');
expect(typeof info.error).toBe('string');
}
});

it('should throw descriptive error when rendering video with unavailable module', () => {
const adapter = new ExpoAVVideoAdapter();

// Only test if expo-av is actually not available (which it likely isn't in test environment)
if (!adapter.isAvailable()) {
const props = { videoUri: 'test://video.mp4' };
const ref = { current: null };

expect(() => {
adapter.renderVideo(props, ref);
}).toThrow(/ExpoAVVideoAdapter:.*Please install:/);
}
});

it('should provide installation command in error message', () => {
const adapter = new ExpoAVVideoAdapter();

if (!adapter.isAvailable()) {
const info = adapter.getAvailabilityInfo();
expect(info.installationCommand).toBe('npx expo install expo-av');
}
});
});

describe('ExpoVideoAdapter', () => {
it('should have getAvailabilityInfo method', () => {
const adapter = new ExpoVideoAdapter();

expect(typeof adapter.getAvailabilityInfo).toBe('function');

const info = adapter.getAvailabilityInfo();
expect(info).toHaveProperty('isAvailable');
expect(info).toHaveProperty('installationCommand', 'npx expo install expo-video');

if (!info.isAvailable) {
expect(info).toHaveProperty('error');
expect(typeof info.error).toBe('string');
}
});

it('should throw descriptive error when rendering video with unavailable module', () => {
const adapter = new ExpoVideoAdapter();

// Only test if expo-video is actually not available (which it likely isn't in test environment)
if (!adapter.isAvailable()) {
const props = { videoUri: 'test://video.mp4' };
const ref = { current: null };

expect(() => {
adapter.renderVideo(props, ref);
}).toThrow(/ExpoVideoAdapter:.*Please install:/);
}
});
});

describe('FallbackVideoAdapter', () => {
it('should always be available and provide installation guidance', () => {
const adapter = new FallbackVideoAdapter('Custom error message');

expect(adapter.isAvailable()).toBe(true);

const info = adapter.getAvailabilityInfo();
expect(info).toEqual({
isAvailable: true,
error: 'Custom error message',
installationCommand: 'npx expo install expo-video expo-av'
});
});

it('should use default error message when none provided', () => {
const adapter = new FallbackVideoAdapter();

const info = adapter.getAvailabilityInfo();
expect(info).toEqual({
isAvailable: true,
error: 'No video player available',
installationCommand: 'npx expo install expo-video expo-av'
});
});
});

describe('Error Message Format', () => {
it('should include adapter name in error messages', () => {
const expoAVAdapter = new ExpoAVVideoAdapter();
const expoVideoAdapter = new ExpoVideoAdapter();

if (!expoAVAdapter.isAvailable()) {
const props = { videoUri: 'test://video.mp4' };
const ref = { current: null };

expect(() => {
expoAVAdapter.renderVideo(props, ref);
}).toThrow(/ExpoAVVideoAdapter:/);
}

if (!expoVideoAdapter.isAvailable()) {
const props = { videoUri: 'test://video.mp4' };
const ref = { current: null };

expect(() => {
expoVideoAdapter.renderVideo(props, ref);
}).toThrow(/ExpoVideoAdapter:/);
}
});

it('should include installation commands in error messages', () => {
const expoAVAdapter = new ExpoAVVideoAdapter();
const expoVideoAdapter = new ExpoVideoAdapter();

if (!expoAVAdapter.isAvailable()) {
const props = { videoUri: 'test://video.mp4' };
const ref = { current: null };

expect(() => {
expoAVAdapter.renderVideo(props, ref);
}).toThrow(/npx expo install expo-av/);
}

if (!expoVideoAdapter.isAvailable()) {
const props = { videoUri: 'test://video.mp4' };
const ref = { current: null };

expect(() => {
expoVideoAdapter.renderVideo(props, ref);
}).toThrow(/npx expo install expo-video/);
}
});
});
});
Loading