import { memo, useEffect, useState } from 'react' import { type ViewStyle } from 'react-native' import { useDerivedValue, useSharedValue } from 'react-native-reanimated' import { Canvas, Image as SkiaImage, useAnimatedImageValue, useImage, useVideo } from "@shopify/react-native-skia"; import { videoUrlCache } from '@/utils/storage' type Props = { url?: string needWeb?: boolean style?: ViewStyle width?: number autoplay?: boolean // 控制动画播放,默认 true } // 默认宽度256半屏宽度 const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true }: Props) => { const [urlFinal, setUrlFinal] = useState('') const pausedValue = useSharedValue(!autoplay) const looping = useSharedValue(true) const createUrl = (url: string) => { return `https://modal-dev.bowong.cc/api/custom/video/converter/v2?media_url=${encodeURI(url)}&options=compression_level=3,quality=70,loop=true,resolution=${width}x${width},fps=24` } const isImg = (url: any) => { if (!url) return false const lowerUrl = url.toLowerCase() return lowerUrl?.match(/\.(jpg|jpeg|png|gif|webp|bmp|tiff|svg)(\?.*)?$/i) } // 检测是否为动画图片(GIF 或动画 WebP) const isAnimatedImage = (u?: string | null) => { if (!u) return false const lowerUrl = u.toLowerCase() // GIF 文件通常是动画的 // WebP 可能是动画的,需要通过内容判断,这里先简单判断扩展名 return lowerUrl.includes('.gif') || lowerUrl.includes('.webp') } async function resolveRedirect(url: string) { const res = await fetch(url, { method: 'GET', headers: { Range: 'bytes=0-0', }, }) return res.url } const setRedirectUrl = async (url?: string) => { const isImg2 = isImg(url) if (isImg2) { setUrlFinal(url!) return } try { // 先尝试从缓存获取 const cachedUrl = await videoUrlCache.get(url!, width) // console.log('getRedirectUrl cachedUrl-----------', url, cachedUrl, width) if (cachedUrl) { setUrlFinal(cachedUrl) return } // 缓存未命中,进行网络请求 const webpUrl = createUrl(url!) const finalUrl = await resolveRedirect(webpUrl) // 缓存结果 await videoUrlCache.set(url!, finalUrl, width) // console.log('setRedirectUrl finalUrl-----------', finalUrl) setUrlFinal(finalUrl!) } catch (error) { console.warn('获取视频URL失败:', error) // 错误时尝试使用原始URL setUrlFinal(url!) } } useEffect(() => { if (!url) return setRedirectUrl(url!) // const finalUrl = createUrl(url) return }, [url]) // 控制动画播放/停止 useEffect(() => { pausedValue.value = !autoplay }, [autoplay, pausedValue]) if (!url) return null // 本地文件全部用 video 组件播放 const isLocal = url ? !url.startsWith('http://') && !url.startsWith('https://') : false const isImageFile = url ? isImg(url) : false // 仅当明显是视频地址时才交给 Skia Video 处理,防止非视频资源导致原生 HostFunction 崩溃 const isVideoUrl = (u?: string | null) => { if (!u) return false const lowerUrl = u.toLowerCase() // 目前 Skia Video 只对常见视频格式稳定,这里做一次白名单过滤 return lowerUrl.includes('.mp4') || lowerUrl.includes('.mov') || lowerUrl.includes('.m4v') || lowerUrl.includes('.webm') } // 获取实际使用的URL // 对于本地文件,直接使用 url // 对于远程文件,需要等待 urlFinal 准备好(图片文件除外,因为图片不需要转换) const displayUrl = url ? isLocal ? url : isImageFile ? url // 图片文件直接使用原始 URL : urlFinal || null // 视频文件需要等待转换后的 URL : null // 判断是否为图片(需要检查原始 URL 和最终 URL) const shouldUseImage = isImageFile || (displayUrl ? isImg(displayUrl) : false) // 判断是否为动画图片 const shouldUseAnimatedImage = shouldUseImage && isAnimatedImage(displayUrl) // 判断是否为静态图片 const shouldUseStaticImage = shouldUseImage && !shouldUseAnimatedImage // 只有在明确是视频链接且不是图片时才调用 useVideo,避免触发 Skia.Video 原生异常 const shouldUseVideo = !shouldUseImage && isVideoUrl(displayUrl) // Hooks 必须在顶层调用,不能条件调用 // 对于静态图片,使用 useImage // 对于动画图片,使用 useAnimatedImageValue(支持 autoplay 控制) // 对于视频,使用 useVideo // 如果 displayUrl 为 null,hooks 会返回 null const staticImage = useImage(shouldUseStaticImage ? displayUrl : null) const animatedImage = useAnimatedImageValue( shouldUseAnimatedImage ? displayUrl : null, shouldUseAnimatedImage ? pausedValue : undefined ) const videoResult = useVideo( shouldUseVideo ? displayUrl : null, { paused: pausedValue, looping, } ) // 使用 useDerivedValue 从 videoResult.currentFrame 获取当前帧 // 如果 videoResult 为 null,videoFrame 也会是 null const videoFrame = useDerivedValue(() => { return videoResult?.currentFrame.value ?? null }) if (!url || !displayUrl) return null const canvasStyle = style || {} const canvasWidth = (canvasStyle.width as number) || width const canvasHeight = (canvasStyle.height as number) || width // 如果是静态图片且已加载 if (shouldUseStaticImage && staticImage) { return ( ) } // 如果是动画图片(GIF 或动画 WebP) if (shouldUseAnimatedImage && animatedImage) { return ( ) } // 如果是视频,使用 currentFrame SharedValue if (shouldUseVideo && videoResult && videoFrame) { return ( ) } return null } export default memo(VideoBox)