diff --git a/@share/components/Video.tsx b/@share/components/Video.tsx index 3b354a8..dc62480 100644 --- a/@share/components/Video.tsx +++ b/@share/components/Video.tsx @@ -1,11 +1,11 @@ -import { Image, type ImageRef } from 'expo-image' -import { memo, useEffect, useRef, useState } from 'react' +import { memo, useEffect, useState } from 'react' import { type ViewStyle } from 'react-native' -import { useSharedValue } from 'react-native-reanimated' +import { useDerivedValue, useSharedValue } from 'react-native-reanimated' import { Canvas, - Image, + Image as SkiaImage, + useAnimatedImageValue, useImage, useVideo } from "@shopify/react-native-skia"; @@ -17,12 +17,13 @@ type Props = { style?: ViewStyle width?: number autoplay?: boolean // 控制动画播放,默认 true -} & React.ComponentProps +} // 默认宽度256半屏宽度 -const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, ...videoProps }: Props) => { +const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true }: Props) => { const [urlFinal, setUrlFinal] = useState('') - const imageRef = useRef(null) + 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` @@ -34,6 +35,15 @@ const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, .. 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', @@ -85,14 +95,8 @@ const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, .. // 控制动画播放/停止 useEffect(() => { - if (imageRef?.current) { - if (autoplay) { - imageRef.current?.startAnimating() - } else { - imageRef.current?.stopAnimating() - } - } - }, [autoplay]) + pausedValue.value = !autoplay + }, [autoplay, pausedValue]) if (!url) return null @@ -122,13 +126,25 @@ const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, .. // 判断是否为图片(需要检查原始 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;对于视频,使用 useVideo + // 对于静态图片,使用 useImage + // 对于动画图片,使用 useAnimatedImageValue(支持 autoplay 控制) + // 对于视频,使用 useVideo // 如果 displayUrl 为 null,hooks 会返回 null - const image = useImage(shouldUseImage ? displayUrl : null) + const staticImage = useImage(shouldUseStaticImage ? displayUrl : null) + const animatedImage = useAnimatedImageValue( + shouldUseAnimatedImage ? displayUrl : null, + shouldUseAnimatedImage ? pausedValue : undefined + ) const videoResult = useVideo( shouldUseVideo ? displayUrl : null, { @@ -137,38 +153,67 @@ const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, .. } ) + // 使用 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 (shouldUseImage && image) { + // 如果是静态图片且已加载 + if (shouldUseStaticImage && staticImage) { return ( - + + + ) } - return ( - // 移除 key 避免组件重建导致闪烁,使用 transition 实现平滑切换 - - ) + // 如果是动画图片(GIF 或动画 WebP) + if (shouldUseAnimatedImage && animatedImage) { + return ( + + + + ) + } + + // 如果是视频,使用 currentFrame SharedValue + if (shouldUseVideo && videoResult && videoFrame) { + return ( + + + + ) + } + + return null } export default memo(VideoBox) diff --git a/public/canvaskit.wasm b/public/canvaskit.wasm new file mode 100644 index 0000000..6b12b96 Binary files /dev/null and b/public/canvaskit.wasm differ