Browse Source

quiet console and handle media playback errors

imwald
Silberengel 5 months ago
parent
commit
1b2db7f3f7
  1. 68
      src/components/AudioPlayer/index.tsx
  2. 56
      src/components/MediaErrorBoundary.tsx
  3. 42
      src/components/VideoPlayer/index.tsx
  4. 67
      src/lib/error-suppression.ts
  5. 1
      src/main.tsx
  6. 6
      src/services/media-manager.service.ts
  7. 34
      src/services/web.service.ts

68
src/components/AudioPlayer/index.tsx

@ -5,6 +5,7 @@ import mediaManager from '@/services/media-manager.service' @@ -5,6 +5,7 @@ import mediaManager from '@/services/media-manager.service'
import { Pause, Play } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import ExternalLink from '../ExternalLink'
import { MediaErrorBoundary } from '../MediaErrorBoundary'
interface AudioPlayerProps {
src: string
@ -85,36 +86,47 @@ export default function AudioPlayer({ src, className }: AudioPlayerProps) { @@ -85,36 +86,47 @@ export default function AudioPlayer({ src, className }: AudioPlayerProps) {
}
return (
<div
className={cn(
'flex items-center gap-3 py-2 pl-2 pr-4 border rounded-full max-w-md',
className
)}
onClick={(e) => e.stopPropagation()}
<MediaErrorBoundary
fallback={<ExternalLink url={src} />}
onError={(error) => {
// Don't log expected media errors
if (error.name !== 'AbortError' && !error.message.includes('play() request was interrupted')) {
console.warn('Audio player error:', error)
}
setError(true)
}}
>
<audio ref={audioRef} src={src} preload="metadata" onError={() => setError(false)} />
{/* Play/Pause Button */}
<Button size="icon" className="rounded-full shrink-0" onClick={togglePlay}>
{isPlaying ? <Pause fill="currentColor" /> : <Play fill="currentColor" />}
</Button>
{/* Progress Section */}
<div className="flex-1 relative">
<Slider
value={[currentTime]}
max={duration || 100}
step={1}
onValueChange={handleSeek}
hideThumb
enableHoverAnimation
/>
<div
className={cn(
'flex items-center gap-3 py-2 pl-2 pr-4 border rounded-full max-w-md',
className
)}
onClick={(e) => e.stopPropagation()}
>
<audio ref={audioRef} src={src} preload="metadata" onError={() => setError(false)} />
{/* Play/Pause Button */}
<Button size="icon" className="rounded-full shrink-0" onClick={togglePlay}>
{isPlaying ? <Pause fill="currentColor" /> : <Play fill="currentColor" />}
</Button>
{/* Progress Section */}
<div className="flex-1 relative">
<Slider
value={[currentTime]}
max={duration || 100}
step={1}
onValueChange={handleSeek}
hideThumb
enableHoverAnimation
/>
</div>
<div className="text-sm font-mono text-muted-foreground">
{formatTime(Math.max(duration - currentTime, 0))}
</div>
</div>
<div className="text-sm font-mono text-muted-foreground">
{formatTime(Math.max(duration - currentTime, 0))}
</div>
</div>
</MediaErrorBoundary>
)
}

56
src/components/MediaErrorBoundary.tsx

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
import React, { Component, ReactNode } from 'react'
import { AlertTriangle } from 'lucide-react'
interface MediaErrorBoundaryProps {
children: ReactNode
fallback?: ReactNode
onError?: (error: Error) => void
}
interface MediaErrorBoundaryState {
hasError: boolean
error?: Error
}
export class MediaErrorBoundary extends Component<MediaErrorBoundaryProps, MediaErrorBoundaryState> {
constructor(props: MediaErrorBoundaryProps) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error): MediaErrorBoundaryState {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Don't log expected media errors
if (error.name === 'AbortError' ||
error.message.includes('play() request was interrupted') ||
error.message.includes('The play() request was interrupted')) {
return
}
// Log unexpected errors
console.warn('Media error boundary caught error:', error, errorInfo)
this.props.onError?.(error)
}
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback
}
return (
<div className="flex items-center justify-center p-4 bg-muted/50 rounded-lg border border-dashed">
<div className="flex items-center gap-2 text-muted-foreground">
<AlertTriangle className="w-4 h-4" />
<span className="text-sm">Media unavailable</span>
</div>
</div>
)
}
return this.props.children
}
}

42
src/components/VideoPlayer/index.tsx

@ -3,6 +3,7 @@ import { useContentPolicy } from '@/providers/ContentPolicyProvider' @@ -3,6 +3,7 @@ import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import mediaManager from '@/services/media-manager.service'
import { useEffect, useRef, useState } from 'react'
import ExternalLink from '../ExternalLink'
import { MediaErrorBoundary } from '../MediaErrorBoundary'
export default function VideoPlayer({ src, className }: { src: string; className?: string }) {
const { autoplay } = useContentPolicy()
@ -45,20 +46,31 @@ export default function VideoPlayer({ src, className }: { src: string; className @@ -45,20 +46,31 @@ export default function VideoPlayer({ src, className }: { src: string; className
}
return (
<div ref={containerRef}>
<video
ref={videoRef}
controls
playsInline
className={cn('rounded-lg max-h-[80vh] sm:max-h-[60vh] border', className)}
src={src}
onClick={(e) => e.stopPropagation()}
onPlay={(event) => {
mediaManager.play(event.currentTarget)
}}
muted
onError={() => setError(true)}
/>
</div>
<MediaErrorBoundary
fallback={<ExternalLink url={src} />}
onError={(error) => {
// Don't log expected media errors
if (error.name !== 'AbortError' && !error.message.includes('play() request was interrupted')) {
console.warn('Video player error:', error)
}
setError(true)
}}
>
<div ref={containerRef}>
<video
ref={videoRef}
controls
playsInline
className={cn('rounded-lg max-h-[80vh] sm:max-h-[60vh] border', className)}
src={src}
onClick={(e) => e.stopPropagation()}
onPlay={(event) => {
mediaManager.play(event.currentTarget)
}}
muted
onError={() => setError(true)}
/>
</div>
</MediaErrorBoundary>
)
}

67
src/lib/error-suppression.ts

@ -0,0 +1,67 @@ @@ -0,0 +1,67 @@
/**
* Suppress expected console errors that are not actionable
* This helps reduce noise in the development console
*/
// Track suppressed errors to avoid spam
const suppressedErrors = new Set<string>()
export function suppressExpectedErrors() {
// Override console.error to filter out expected errors
const originalConsoleError = console.error
console.error = (...args: any[]) => {
const message = args.join(' ')
// Suppress favicon 404 errors
if (message.includes('favicon.ico') && message.includes('404')) {
return
}
// Suppress CORS errors for external websites
if (message.includes('CORS policy') && message.includes('Access-Control-Allow-Origin')) {
return
}
// Suppress network errors for external websites
if (message.includes('net::ERR_FAILED') && message.includes('200 (OK)')) {
return
}
// Suppress YouTube API warnings
if (message.includes('Unrecognized feature: \'web-share\'')) {
return
}
// Suppress Canvas2D warnings
if (message.includes('Canvas2D: Multiple readback operations')) {
return
}
// Call original console.error for unexpected errors
originalConsoleError.apply(console, args)
}
// Override console.warn to filter out expected warnings
const originalConsoleWarn = console.warn
console.warn = (...args: any[]) => {
const message = args.join(' ')
// Suppress React DevTools suggestion (only show once)
if (message.includes('Download the React DevTools')) {
if (suppressedErrors.has('react-devtools')) {
return
}
suppressedErrors.add('react-devtools')
}
// Call original console.warn for unexpected warnings
originalConsoleWarn.apply(console, args)
}
}
// Initialize error suppression
if (typeof window !== 'undefined') {
suppressExpectedErrors()
}

1
src/main.tsx

@ -2,6 +2,7 @@ import './i18n' @@ -2,6 +2,7 @@ import './i18n'
import './index.css'
import './polyfill'
import './services/lightning.service'
import './lib/error-suppression'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'

6
src/services/media-manager.service.ts

@ -53,6 +53,12 @@ class MediaManagerService { @@ -53,6 +53,12 @@ class MediaManagerService {
}
play(this.currentMedia).catch((error) => {
// Don't log expected AbortError when media is interrupted
if (error instanceof Error && error.name === 'AbortError') {
// This is expected when media is interrupted by pause() or other media
return
}
// Log other unexpected errors
console.error('Error playing media:', error)
this.currentMedia = null
})

34
src/services/web.service.ts

@ -9,7 +9,26 @@ class WebService { @@ -9,7 +9,26 @@ class WebService {
return await Promise.all(
urls.map(async (url) => {
try {
const res = await fetch(url)
// Add timeout and better error handling for CORS issues
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10000) // 10 second timeout
const res = await fetch(url, {
signal: controller.signal,
mode: 'cors',
credentials: 'omit'
})
clearTimeout(timeoutId)
if (!res.ok) {
// Don't log 404s and CORS errors as they're expected
if (res.status !== 404 && res.status !== 0) {
console.warn(`Failed to fetch metadata for ${url}: ${res.status} ${res.statusText}`)
}
return {}
}
const html = await res.text()
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
@ -24,7 +43,18 @@ class WebService { @@ -24,7 +43,18 @@ class WebService {
?.content
return { title, description, image }
} catch {
} catch (error) {
// Only log unexpected errors, not CORS or network issues
if (error instanceof TypeError && error.message.includes('Failed to fetch')) {
// This is likely a CORS error - don't log it
return {}
}
if (error instanceof Error && error.name === 'AbortError') {
// Timeout - don't log it
return {}
}
// Log other unexpected errors
console.warn(`Unexpected error fetching metadata for ${url}:`, error)
return {}
}
})

Loading…
Cancel
Save