import { isHlsPlaylistUrl, isImage } from '@/lib/url' import { cn } from '@/lib/utils' import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import AudioPlayer from '../AudioPlayer' import VideoPlayer from '../VideoPlayer' import ExternalLink from '../ExternalLink' import LazyMediaTapPlaceholder, { MediaEmbedBlurFrame } from './LazyMediaTapPlaceholder' /** Same rules as the metadata probe, but synchronous so the first paint can show the embed stack. */ function embedMediaTypeHintFromUrl(src: string): 'video' | 'audio' | null { try { const url = new URL(src) const extension = url.pathname.split('.').pop()?.toLowerCase() if ( extension && ['mp3', 'wav', 'flac', 'aac', 'm4a', 'opus', 'wma', 'mka', 'oga'].includes(extension) ) { return 'audio' } if (extension === 'mkv' || extension === 'ogv') { return 'video' } if ( extension && ['mp4', 'webm', 'm4v', 'mov', 'avi', '3gp', '3g2', 'm3u8', 'm3u'].includes(extension) ) { return 'video' } return null } catch { return null } } export default function MediaPlayer({ src, className, mustLoad = false, poster, blurHash, fallbackPageUrl }: { src: string className?: string mustLoad?: boolean poster?: string /** NIP-94 / imeta blurhash for lazy placeholder when poster is missing */ blurHash?: string /** Passed to {@link VideoPlayer} when HLS/video playback fails (e.g. NIP-53 zap.stream join URL). */ fallbackPageUrl?: string }) { const { t } = useTranslation() const { autoLoadMedia } = useContentPolicy() /** Tap-to-load when {@link autoLoadMedia} is off; cleared when policy switches back to never. */ const [userClickedLoad, setUserClickedLoad] = useState(false) const [mediaType, setMediaType] = useState<'video' | 'audio' | null>(null) const [probeFailed, setProbeFailed] = useState(false) const [embedPainted, setEmbedPainted] = useState(false) const readyOnceRef = useRef(false) // imeta `thumb` / `image` are sometimes the same .mp4 as `url` — cannot use that, and it // would hide the blurhash placeholder in LazyMediaTapPlaceholder. const imagePoster = useMemo(() => { const p = poster?.trim() if (!p) return undefined return isImage(p) ? p : undefined }, [poster]) const urlEmbedTypeHint = useMemo(() => embedMediaTypeHintFromUrl(src), [src]) /** Probe result wins when set (e.g. audio-only mp4); URL hint avoids a blank frame before useEffect runs. */ const effectiveMediaType = mediaType ?? urlEmbedTypeHint const showEmbed = mustLoad || autoLoadMedia || userClickedLoad useLayoutEffect(() => { if (!autoLoadMedia) setUserClickedLoad(false) }, [autoLoadMedia]) useEffect(() => { readyOnceRef.current = false setEmbedPainted(false) setMediaType(null) setProbeFailed(false) }, [src]) useEffect(() => { if (!showEmbed) { setMediaType(null) setProbeFailed(false) return } readyOnceRef.current = false setEmbedPainted(false) if (!src) { setProbeFailed(true) return } setProbeFailed(false) setMediaType(null) let cancelled = false try { // Firefox/Chrome do not expose HLS via