203 lines
5.2 KiB
TypeScript
203 lines
5.2 KiB
TypeScript
import { Image as ExpoImage, type ImageRef } from 'expo-image'
|
||
import { memo, useEffect, useRef, useState } from 'react'
|
||
import { View, type ViewStyle } from 'react-native'
|
||
import Video from 'react-native-video'
|
||
import tw from 'twrnc'
|
||
|
||
import { videoUrlCache } from '@/utils/storage'
|
||
import { mp4ToWebpUrl } from '@/utils/uploadFile'
|
||
|
||
import Img from './Img'
|
||
|
||
type Props = {
|
||
className?: string
|
||
url?: string
|
||
placeholderUrl?: string
|
||
needWeb?: boolean
|
||
style?: ViewStyle
|
||
width?: number
|
||
autoplay?: boolean // 控制动画播放,默认 true
|
||
} & React.ComponentProps<typeof Video>
|
||
|
||
// videoUrlCache.clear()
|
||
// ExpoImage.clearDiskCache()
|
||
// ExpoImage.clearMemoryCache()
|
||
// 默认宽度256半屏宽度
|
||
const VideoBox = ({
|
||
className = '',
|
||
url,
|
||
placeholderUrl = '',
|
||
width = 256,
|
||
style,
|
||
autoplay = true,
|
||
...videoProps
|
||
}: Props) => {
|
||
const [urlFinal, setUrlFinal] = useState('')
|
||
const imageRef = useRef<ImageRef | null>(null)
|
||
|
||
const isImg = (url: any) => {
|
||
if (!url) return false
|
||
const lowerUrl = url.toLowerCase()
|
||
return lowerUrl?.match(/\.(jpg|jpeg|png|gif|webp|bmp|tiff|svg)(\?.*)?$/i)
|
||
}
|
||
|
||
// / 本地文件全部用video组件播放
|
||
const isLocal = !url.startsWith('http://') && !url.startsWith('https://')
|
||
const isImageFile = isImg(url)
|
||
|
||
const setRedirectUrl = async (isCancelled: () => boolean, url?: string) => {
|
||
const isImg2 = isImg(url)
|
||
if (isImg2) {
|
||
if (!isCancelled()) {
|
||
setUrlFinal(url!)
|
||
}
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 先尝试从缓存获取
|
||
const cachedUrl = await videoUrlCache.get(url!, width)
|
||
// console.log('getRedirectUrl cachedUrl-----------', url, cachedUrl, width)
|
||
if (cachedUrl) {
|
||
// 只有当请求未被取消时才更新状态
|
||
if (!isCancelled()) {
|
||
setUrlFinal(cachedUrl)
|
||
}
|
||
return
|
||
}
|
||
|
||
const webpUrl = await mp4ToWebpUrl({ videoUrl: url!, width, height: width })
|
||
|
||
// 只有当请求未被取消时才缓存和更新状态
|
||
if (!isCancelled()) {
|
||
// 缓存结果
|
||
await videoUrlCache.set(url!, webpUrl, width)
|
||
|
||
// console.log('setRedirectUrl finalUrl-----------', finalUrl)
|
||
setUrlFinal(webpUrl)
|
||
}
|
||
} catch (error) {
|
||
console.warn('获取视频URL失败:', error)
|
||
// 只有当请求未被取消时才更新状态
|
||
if (!isCancelled()) {
|
||
// 错误时尝试使用原始URL
|
||
setUrlFinal(url!)
|
||
}
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
// 每次url变化强制刷新
|
||
setUrlFinal('')
|
||
if (!url || isLocal) {
|
||
return
|
||
}
|
||
|
||
// 用于标记当前请求是否已过期
|
||
let cancelled = false
|
||
|
||
setTimeout(() => {
|
||
if (!cancelled) {
|
||
setRedirectUrl(() => cancelled, url)
|
||
}
|
||
}, 0)
|
||
|
||
// 清理函数:当 url 变化或组件卸载时,标记当前请求为已取消
|
||
return () => {
|
||
cancelled = true
|
||
}
|
||
}, [url, width])
|
||
|
||
// 控制动画播放/停止
|
||
useEffect(() => {
|
||
// 添加延迟和安全检查,确保 ref 已就绪且组件未卸载
|
||
const timer = setTimeout(() => {
|
||
try {
|
||
if (imageRef?.current) {
|
||
if (autoplay) {
|
||
imageRef.current?.startAnimating()
|
||
} else {
|
||
imageRef.current?.stopAnimating()
|
||
}
|
||
}
|
||
} catch (error) {
|
||
// 忽略动画控制错误,避免崩溃
|
||
console.debug('Animation control failed:', error)
|
||
}
|
||
}, 100)
|
||
|
||
return () => clearTimeout(timer)
|
||
}, [autoplay, urlFinal])
|
||
|
||
if (!url) return null
|
||
|
||
if (isLocal) {
|
||
if (!isImageFile) {
|
||
return (
|
||
<Video
|
||
key={url ?? 'no-url'}
|
||
muted
|
||
repeat
|
||
controls={false}
|
||
paused={false}
|
||
poster={url}
|
||
resizeMode="cover"
|
||
source={{ uri: url }}
|
||
style={style as any}
|
||
viewType={0}
|
||
volume={0}
|
||
{...videoProps}
|
||
/>
|
||
)
|
||
}
|
||
return (
|
||
<ExpoImage
|
||
cachePolicy="disk"
|
||
source={{ uri: url }}
|
||
style={style as any}
|
||
transition={{ duration: 200, effect: 'cross-dissolve' }}
|
||
/>
|
||
)
|
||
}
|
||
|
||
const imageStyle = tw`${className}`
|
||
|
||
const showPlaceholder = !!placeholderUrl
|
||
|
||
return (
|
||
<View style={[style, imageStyle, { overflow: 'hidden' }]}>
|
||
{/* 占位图层 - 加载完成后隐藏 */}
|
||
{showPlaceholder && (
|
||
<Img
|
||
isCompression={false}
|
||
isWebP={true}
|
||
src={placeholderUrl}
|
||
style={{ position: 'absolute', width: '100%', height: '100%' }}
|
||
contentFit={'cover'}
|
||
transition={{ duration: 100, effect: 'curl-up' }}
|
||
/>
|
||
)}
|
||
|
||
{/* 真实图片层 */}
|
||
<ExpoImage
|
||
ref={imageRef}
|
||
// iOS 优化:使用 memory-disk 策略,但设置较低的内存缓存优先级
|
||
cachePolicy="disk"
|
||
// 添加 recyclingKey 帮助内存回收
|
||
recyclingKey={url}
|
||
// iOS 性能优化:允许降采样
|
||
allowDownscaling={true}
|
||
// 设置解码格式(iOS 优化)
|
||
decoding="async"
|
||
contentFit={'cover'}
|
||
source={{ uri: urlFinal }}
|
||
style={style as any}
|
||
autoplay={autoplay}
|
||
transition={{ duration: 200, effect: 'cross-dissolve' }}
|
||
/>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
export default memo(VideoBox)
|