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)