/** * Typed error codes emitted by the Aviation playback system. * * There are two families: * * **Playback codes** flow asynchronously from native and are produced by * {@link mapNativeErrorCode}: * - C state machine: LOAD_ERROR, PLAYBACK_ERROR, BUFFER_ERROR * - iOS DRM adapter: DRM_CERTIFICATE_FAILED, DRM_LICENSE_FAILED, DRM_KEY_EXPIRED * - Android ExoPlayer: PLAYER_ERROR_* (mapped to DECODE_ERROR) * - Network layer: NETWORK_ERROR * - Unrecognized native codes: UNKNOWN * * **Usage codes** are thrown synchronously by the JS API surface when it is * called incorrectly (wrong lifecycle state, missing setup, disposed handle, * etc.). These never carry a `nativeCode` and are never retryable. */ export type AviationErrorCode = // Playback codes (from native, via mapNativeErrorCode) | 'LOAD_ERROR' | 'PLAYBACK_ERROR' | 'BUFFER_ERROR' | 'NETWORK_ERROR' | 'DECODE_ERROR' | 'DRM_LICENSE_FAILED' | 'DRM_CERTIFICATE_FAILED' | 'DRM_KEY_EXPIRED' | 'UNKNOWN' // Usage codes (thrown synchronously by the JS API surface) | 'NOT_READY' | 'INVALID_STATE' | 'DISPOSED' | 'PLAYER_EXISTS' | 'NO_PLAYER'; /** * Set of error codes that are typically transient and worth retrying. * * - NETWORK_ERROR: connectivity blip, CDN hiccup * - BUFFER_ERROR: rebuffer failure, usually recovers on retry * - DRM_LICENSE_FAILED: license server may have been temporarily unavailable */ const RETRYABLE_CODES: ReadonlySet = new Set([ 'NETWORK_ERROR', 'BUFFER_ERROR', 'DRM_LICENSE_FAILED', ]); /** * Structured playback error with a typed code and retryability hint. * * Extends `Error` so it works everywhere a plain error does — * `error.message` still returns the human-readable string. * * @example * ```ts * const { error } = usePlayback(); * if (error) { * switch (error.code) { * case 'DRM_LICENSE_FAILED': * showDrmAuthPrompt(); * break; * case 'NETWORK_ERROR': * if (error.isRetryable) showRetryButton(); * break; * default: * showGenericError(error.message); * } * } * ``` */ export class AviationError extends Error { /** Typed error code for programmatic handling. */ readonly code: AviationErrorCode; /** * The raw error code string from the native layer before mapping. * Useful for debugging platform-specific issues (e.g. `"PLAYER_ERROR_1001"`). * Empty for usage errors thrown by the JS API surface. */ readonly nativeCode: string; /** * Whether this error is typically transient and worth retrying. * `true` for NETWORK_ERROR, BUFFER_ERROR, DRM_LICENSE_FAILED. */ readonly isRetryable: boolean; constructor(code: AviationErrorCode, message: string, nativeCode = '') { super(message); this.name = 'AviationError'; this.code = code; this.nativeCode = nativeCode; this.isRetryable = RETRYABLE_CODES.has(code); } } /** * Map a raw native error code string to a typed AviationErrorCode. * * Native codes arrive as: * - C state machine: "LOAD_ERROR", "PLAYBACK_ERROR", "BUFFER_ERROR" * - iOS DRM adapter: "DRM_CERTIFICATE_FAILED", "DRM_LICENSE_FAILED", "DRM_KEY_EXPIRED" * - Android ExoPlayer: "PLAYER_ERROR_${number}" * - Empty string: no code provided */ export function mapNativeErrorCode(nativeCode: string): AviationErrorCode { switch (nativeCode) { case 'LOAD_ERROR': return 'LOAD_ERROR'; case 'PLAYBACK_ERROR': return 'PLAYBACK_ERROR'; case 'BUFFER_ERROR': return 'BUFFER_ERROR'; case 'NETWORK_ERROR': return 'NETWORK_ERROR'; case 'DRM_LICENSE_FAILED': return 'DRM_LICENSE_FAILED'; case 'DRM_CERTIFICATE_FAILED': return 'DRM_CERTIFICATE_FAILED'; case 'DRM_KEY_EXPIRED': return 'DRM_KEY_EXPIRED'; default: // Android ExoPlayer errors: "PLAYER_ERROR_${errorCode}". The numeric // suffix encodes the failure family (media3 PlaybackException): // 1xxx playback-position, 2xxx source/IO, 3xxx DRM, 4xxx decoder, // 5xxx audio track. if (nativeCode.startsWith('PLAYER_ERROR_')) { const n = Number(nativeCode.slice('PLAYER_ERROR_'.length)); if (n >= 2000 && n < 3000) return 'NETWORK_ERROR'; if (n >= 3000 && n < 4000) return 'DRM_LICENSE_FAILED'; if (n >= 4000) return 'DECODE_ERROR'; return 'PLAYBACK_ERROR'; } return 'UNKNOWN'; } }